WklGatewayClient: VBA class for the data service
WklGatewayClient lets you call the commands of the data service's (Wkl.Data.Gateway) administration
interface from VBA: issue keys, set up the second factor, test connections, and more. It also
serves as a template for any VBA application that talks to the service over HTTP.
An Access application reads and edits lists (data sources, rules, people, keys) as linked tables through the service's ODBC driver. Anything that returns a secret or affects more than a single row is a command, and that's what this class is for. The entity sets and commands themselves are described in HTTP: administration (reference) (data service documentation).
Supported environment
Microsoft Access from Microsoft 365, 32-bit and 64-bit, in an ACCDB, an ACCDE, and the Access Runtime. The administration app calls its commands through this class.
Prerequisites
| Prerequisite | Used for |
|---|---|
| Microsoft Access from Microsoft 365, 32-bit or 64-bit | see Supported environment |
A reference to WKL Office JSON 1.1 (Wkl.Office.JSON.dll registered for your Office bitness; up to release 1.0.3, WKL Access JSON 1.0 with Wkl.Access.JSON.dll) | parsing responses as JDoc and building requests |
WinHTTP (WinHttp.WinHttpRequest.5.1, part of Windows) | HTTP; late-bound, so no reference is needed |
A service with an administration store, and a person with the admin permission | the commands |
Add the class to a project
- In the VBA editor, choose File > Import File and import
WklGatewayClient.cls(a class module, found in the repository undersource/classes/) and, if you want the examples,WklGatewayClientExamples.bas(underclient/). - Choose Tools > References and select “WKL Office JSON 1.1” (“WKL Access JSON 1.0” up to release 1.0.3).
- Compile the project (Debug > Compile).
The files use the format the VBA editor exports and imports. Access's LoadFromText expects a
different format for a class module (without VERSION 1.0 CLASS … END and without VB_Name); given
the export format, it creates a standard module instead. client\tests\Run-AccessTests.ps1 shows how
to convert between the two.
Usage
Use one instance for the application's entire session, because the instance holds the second-factor session.
Private m_Client As WklGatewayClient Public Function Gateway() As WklGatewayClient If m_Client Is Nothing Then Set m_Client = New WklGatewayClient m_Client.BaseUrl = "https://data.example.com" m_Client.SignIn = gwPersonalKey ' Key from Windows Credential Manager End If Set Gateway = m_Client End Function Sub Example() Dim answer As JDoc On Error GoTo ErrorHandler Set answer = Gateway().IssueKey("Jane Doe", Label:="Laptop", Delivery:="link") Debug.Print answer.GetString("$.link") FinalHandler: Exit Sub ErrorHandler: MsgBox Err.Description, vbExclamation ' Error numbers: see section "Errors" Resume FinalHandler End Sub
WklGatewayClientExamples.bas contains every command once, with error handling by error number.
Sign-in
SignIn | First factor | Taken from |
|---|---|---|
gwPersonalKey (default) | personal key (wkl_…) in X-Api-Key | the Key property; otherwise Windows Credential Manager (StoreKey, ForgetKey, HasStoredKey) |
gwWindows | the process's Windows account (Negotiate) | — |
gwSqlLogin | SQL login (HTTP Basic) | UserName, Password |
gwServiceKey | service key | the environment variable named in KeyVariable |
Every sign-in (personal key, service key, password, and Windows account) is sent only over https
or to the local computer; the class refuses an http address of another computer with
gwErrConfiguration before sending anything. StoreKey saves
the key under the same target the ODBC driver uses for an administration connection to the same
address (Wkl.Data.OData.Odbc:(persönlicher Schlüssel)@<BaseUrl>/admin/v1/odata/), so one saved key
serves both the linked tables and the class. The key is never stored in the database file.
Second factor
If the service requires a second factor (a 401 response with Wkl-Totp), the class asks for the
code, exchanges it for a session, and repeats the request. It gets the code from these sources, in
this order:
- The
TotpCodeproperty (used once), for code that prompts for the code itself. - The
CodeRequired(Message, Code, Cancel)event of an instance declaredWithEvents. - An
InputBox(which also works in the Runtime); turn it off withAskWithInputBox = False.
If the code is wrong, the class asks again, showing the service's message, up to three times.
OpenSession starts the session explicitly, and ForgetSession discards it. The ODBC driver keeps
its own session. To make one code work for both, the application passes SessionToken to the
driver with the SESSION= keyword of a connection it opens itself (this is what the
administration app does). Without that handover, the driver and the class each
ask for a code once.
Interrupted connection
If the connection drops without a response, the class retries the request after 1 second and again
after 3 seconds. Every write request carries an Idempotency-Key, and the service answers a retry
from memory instead of writing a second time. The service remembers the key for 10 minutes and only
in memory, so if it restarts between the dropped connection and the retry, a request can arrive
twice (service behavior as described in HTTP: administration (reference), section “Many operations at once” (data service documentation)). A request that times out (TimeoutSeconds, 60 by default)
isn't retried: the service might still carry it out, and the class has no way of knowing. In that
case, read the data back to check whether the change went through.
What the class protects, and what it doesn't
- It sends keys, passwords, and the Windows sign-in only over HTTPS or to the local computer; otherwise, it refuses with
gwErrConfiguration. WinHTTP validates the server certificate against the Windows certificate store, and an invalid certificate makes the request fail; the class never turns that check off. - It keeps
KeyandPasswordonly in its instance's memory. A key is saved only when you callStoreKey, and then only in the user's Windows Credential Manager. - A service key (
gwServiceKey) is never stored in code or in the database. The class reads it at run time from the environment variable named inKeyVariable. Anyone who can read that variable (any process running as that user) has the key.
Reference
| Member | Description |
|---|---|
BaseUrl | the service's address: scheme, host, and port, without a path |
SignIn, Key, KeyVariable, UserName, Password | sign-in; see above |
TotpCode, AskWithInputBox, CodeRequired | second factor |
TimeoutSeconds | time limit per request, in seconds |
LastStatus, LastMessage | HTTP status and message of the last response |
HasSession, SessionExpiresAt, OpenSession, ForgetSession | the second-factor session |
SessionToken | the session itself, for handing over to the ODBC driver in the same process (SESSION=); keep it in memory only |
CredentialTarget, StoreKey, ForgetKey, HasStoredKey | the key in Windows Credential Manager. ForgetKey and HasStoredKey return False only when no key is stored; if Credential Manager itself fails, both raise gwErrConfiguration |
IssueKey(Person, [Label], [ExpiresAt], [ReadOnly], [Databases], [FromNetworks], [ExceptNetworks], [TotpExempt], [Delivery], [OnDemand]) | issues a key; Delivery is link (default) or key; OnDemand = dormant until activated. With key, the response contains the key in plain text, so it ends up in your VBA code: never display, store, or log it. That's why the administration app only ever uses link |
ActivateKey(KeyId, Hours) / DeactivateKey(KeyId) | activates an on-demand key for Hours hours, or makes it dormant again immediately; see HTTP: sign-in (reference), section “On-demand keys” (data service documentation) |
ExplainSignIn(Person, OriginNames, OriginAddresses, [Scope], [OriginCountries], [OriginOperators], [OriginClients], [OriginTimes]) | what the sign-in policy says for the person, by sign-in method and origin, with the rules responsible (rules), the rules on trial (tried), and those an exception lifts (spared); an empty address means an unknown origin. The per-origin fields for country (TH), network operator (AS3320), program, and time (UTC) ask “what if …”; left empty, the service determines them itself |
SignInProfile(Person, [Days]) | what is usual for the person at sign-in over the last Days days (1 to 3,650, default 90): total, activeDays, and, under dimensions, the values per country, network, network operator, program, method, and hour, with count and first and last day. Successful sign-ins only; requires the sign-in-events right (otherwise 403); an unknown person returns 404 |
SaveSignInRule(Id, Scope, Method, Effect, Label, [FromNetworks], [ExceptNetworks], [Persons], [ExceptPersons], [WindowsGroups], [ExceptWindowsGroups], [Countries], [ExceptCountries], [Operators], [ExceptOperators], [Clients], [ExceptClients], [Hours], [ExceptHours], [Exceptions], [Mode]) | creates a sign-in rule with all its conditions and exceptions in a single change (Id 0), or replaces it as a whole, so any list you leave out ends up empty, and without Mode the rule is enforced. Lists for and against: networks, people (by name), Windows groups, countries, network operators, programs, time windows; an empty list of people means “every caller.” Exceptions: an array of Scripting.Dictionary with the lists persons, windowsGroups, countries, from, operators, clients, hours, the end until (a Date in UTC), and label. Mode: Enforce or Report (try only). Response: id |
EnrollTotp(Person, [Delivery]), ResetTotp(Person) | sets up or removes the second factor |
OwnKeys([DataSource]), IssueOwnKey([Label], [ExpiresAt], [ReadOnly], [Databases], [DataSource]), RevokeOwnKey(Id, [DataSource]) | lists, issues (response key, shown once), and revokes your own keys; see HTTP: sign-in (reference), section “Self-service” (data service documentation). Requires signing in with gwWindows or gwSqlLogin; for a SQL login, DataSource names the data source the login is checked against |
TestConnection(DataSource), ReloadCatalog([DataSource]) | tests a data source (answer reachable; when it fails, also message, number, and failure, the kind of error independent of the server's language); discards the cached catalog |
ServiceAccount() | the account the service signs in with at databases when it uses Windows sign-in: account (as a SQL Server on the same machine sees it), networkAccount (on another machine), kind, domain; the name the database administrator creates the login for |
SetGeoIpSource(Source, UpdateDays, [Account], [LicenseKey], [RemoveLicenseKey]) | chooses the source of the geolocation database: none, dbip-lite, or maxmind (with account ID and license key); UpdateDays 0 to 90, where 0 means on command only. Without LicenseKey, the stored key is kept; the service never returns it |
UpdateGeoIp() | loads both geolocation files now and puts them into use without a restart; response files with kind, buildDate, and changed per file |
UploadGeoIp(FileName, [Source], [Force]) | hands a geolocation file to the service (.mmdb, .mmdb.gz, or MaxMind's .tar.gz) for a server without internet access; Source sets the attribution, Force also accepts an older file |
ExportLoad([Days]) | the load log of the last Days days (default 30) in the wkl-load-export/1 format, without persons, keys, addresses, or values; save it with JDoc.Save |
CheckStore(), BackupStore(), ResealStore() | checks the store, backs it up (to the sicherungen folder next to the store; response file, schemaVersion, bytes), and reseals it |
ReadSet(SetName, [Filter], [OrderBy], [Top]) | reads an entity set; rows under $.value |
CallProcedure(DataSource, Name, Name1, Value1, Name2, Value2, …) | calls a stored procedure, with arguments in pairs and names without @; an omitted argument takes the procedure's default value. Values: text, numbers (Currency and Decimal exact), Boolean, Date (sent as ISO 8601 text, which SQL Server converts to the parameter type), and Null. Response: the first result set under $.data ($.data[0].<column>) or affectedRowCount. If the procedure itself raises a message (THROW, or RAISERROR with text), it comes back as gwErrRejected with that text. Contract: HTTP: native API (reference), section “Calling procedures” (data service documentation); requires a rule with procedures and POST |
Execute(Method, Path, [Body]) | any other request; the response comes back as a JDoc |
Errors
Every error is raised with a fixed number (WklGatewayError) and a German description, which ends with
the service's message when there is one.
| Constant | Number | Meaning |
|---|---|---|
gwErrUnreachable | vbObjectError + 2001 | no connection, even after retrying |
gwErrTimeout | + 2002 | no response within TimeoutSeconds |
gwErrUnauthenticated | + 2003 | 401: key, account, or password rejected, or no key available |
gwErrSecondFactor | + 2004 | a second factor is required but wasn't provided, or it was rejected |
gwErrForbidden | + 2005 | 403 |
gwErrNotFound | + 2006 | 404 |
gwErrBadRequest | + 2007 | 400 |
gwErrConflict | + 2008 | 409, 412: changed in the meantime |
gwErrRejected | + 2009 | 422: rejected by the service; for a message the application raises in the database itself, the description ends with that message |
gwErrTooMany | + 2010 | 429 |
gwErrService | + 2011 | 5xx |
gwErrAnswer | + 2012 | the response isn't valid JSON |
gwErrConfiguration | + 2013 | the class's settings contradict each other |
Limitations
- The driver and the class keep separate second-factor sessions unless the application hands the session over with
SESSION=. - Each response is read in full within the time limit. For large amounts of data, use linked tables (through the ODBC driver), not this class.