|
| 1 | +--- |
| 2 | +name: a2a-workflow |
| 3 | +description: Use when working with SafeguardJava application-to-application credential retrieval, brokering, or A2A events. |
| 4 | +--- |
| 5 | + |
| 6 | +# A2A Workflow |
| 7 | + |
| 8 | +Safeguard Application-to-Application (A2A) is the SDK path for non-interactive |
| 9 | +automation that needs privileged credentials without a user login prompt. In |
| 10 | +SafeguardJava, A2A is certificate-authenticated, API-key-scoped, and exposed |
| 11 | +through `Safeguard.A2A.getContext(...)` plus `ISafeguardA2AContext`. Use it for |
| 12 | +service accounts, scheduled jobs, brokers, and other integrations that need to |
| 13 | +retrieve or rotate credentials directly from Safeguard. |
| 14 | + |
| 15 | +## 1. What A2A is |
| 16 | + |
| 17 | +The main A2A surface is `com.oneidentity.safeguard.safeguardjava.ISafeguardA2AContext`. |
| 18 | +`SafeguardA2AContext` maintains two internal REST clients: |
| 19 | + |
| 20 | +- an A2A client rooted at `https://<appliance>/service/a2a/v<apiVersion>` |
| 21 | +- a core client rooted at `https://<appliance>/service/core/v<apiVersion>` |
| 22 | + |
| 23 | +The A2A client is used for credential retrieval and brokering (`Credentials`, |
| 24 | +`AccessRequests`), while the core client is used to enumerate `A2ARegistrations` |
| 25 | +and their `RetrievableAccounts`. |
| 26 | + |
| 27 | +Important design constraints from the SDK: |
| 28 | + |
| 29 | +- A2A uses certificate auth plus an `Authorization: A2A <apiKey>` header |
| 30 | +- `Service.A2A` is **not** valid with `ISafeguardConnection` |
| 31 | +- API keys are stored as `char[]` in the public interfaces |
| 32 | +- context objects must be cleaned up with `dispose()` |
| 33 | + |
| 34 | +## 2. Setup flow (certificate registration, API key creation) |
| 35 | + |
| 36 | +The repository samples document the expected appliance-side setup: |
| 37 | + |
| 38 | +1. **Trust the certificate chain** |
| 39 | + - `Samples/CertificateConnect/README.md` calls out uploading the root/intermediate CA chain |
| 40 | + - the certificate must be trusted by Safeguard before certificate auth or A2A will work |
| 41 | +2. **Register the application / certificate identity** |
| 42 | + - create the application user or mapping that represents the calling service |
| 43 | + - certificate identity can come from a file, keystore alias, in-memory bytes, or a Windows thumbprint |
| 44 | +3. **Create A2A registrations** |
| 45 | + - `Samples/A2ARetrievalExample/README.md` calls out configuring retrieval registrations |
| 46 | + - each retrievable credential gets an API key that scopes access to that item |
| 47 | +4. **Distribute the client certificate and API key securely** |
| 48 | + - keep certificate passwords, API keys, and retrieved secrets in `char[]` |
| 49 | + - never check PFX/JKS files, passwords, or API keys into the repo |
| 50 | + |
| 51 | +### Choosing a `getContext(...)` overload |
| 52 | + |
| 53 | +`Safeguard.A2A.getContext(...)` supports the same certificate sources the rest of the SDK uses: |
| 54 | + |
| 55 | +- keystore path + alias |
| 56 | +- PKCS12 / PFX file path |
| 57 | +- in-memory certificate bytes |
| 58 | +- Windows certificate thumbprint (Windows only) |
| 59 | +- optional `apiVersion` |
| 60 | +- either `ignoreSsl` or a custom `HostnameVerifier` |
| 61 | + |
| 62 | +Examples from the actual factories in `Safeguard.java`: |
| 63 | + |
| 64 | +```java |
| 65 | +ISafeguardA2AContext fromFile = Safeguard.A2A.getContext( |
| 66 | + appliance, |
| 67 | + certificatePath, |
| 68 | + certificatePassword, |
| 69 | + null, |
| 70 | + true); |
| 71 | + |
| 72 | +ISafeguardA2AContext fromKeystore = Safeguard.A2A.getContext( |
| 73 | + appliance, |
| 74 | + keystorePath, |
| 75 | + keystorePassword, |
| 76 | + certificateAlias, |
| 77 | + null, |
| 78 | + true); |
| 79 | +``` |
| 80 | + |
| 81 | +Windows thumbprint overloads require `SunMSCAPI` to be available. The SDK throws a |
| 82 | +`SafeguardForJavaException` on non-Windows platforms or when the provider is missing. |
| 83 | + |
| 84 | +## 3. Credential retrieval (programmatic access) |
| 85 | + |
| 86 | +### Enumerate retrievable accounts |
| 87 | + |
| 88 | +`ISafeguardA2AContext.getRetrievableAccounts()` first queries `Core/A2ARegistrations`, |
| 89 | +then enumerates each registration's `RetrievableAccounts` endpoint. The returned |
| 90 | +`IA2ARetrievableAccount` objects include application name, asset/account details, |
| 91 | +and the API key as `char[]`. |
| 92 | + |
| 93 | +```java |
| 94 | +List<IA2ARetrievableAccount> accounts = a2aContext.getRetrievableAccounts(); |
| 95 | +for (IA2ARetrievableAccount account : accounts) { |
| 96 | + System.out.println(account.getAssetName() + " -> " + account.getAccountName()); |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +There is also a filtered form: |
| 101 | + |
| 102 | +```java |
| 103 | +List<IA2ARetrievableAccount> accounts = |
| 104 | + a2aContext.getRetrievableAccounts("AccountName eq 'admin'"); |
| 105 | +``` |
| 106 | + |
| 107 | +The filter is passed server-side as the `filter` query parameter on each registration lookup. |
| 108 | + |
| 109 | +### Retrieve a password |
| 110 | + |
| 111 | +This is the path shown in `Samples/A2ARetrievalExample/.../A2ARetrievalExample.java`: |
| 112 | + |
| 113 | +```java |
| 114 | +char[] password = a2aContext.retrievePassword(apiKey); |
| 115 | +try { |
| 116 | + usePassword(password); |
| 117 | +} finally { |
| 118 | + java.util.Arrays.fill(password, '\0'); |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +Internally, the SDK sends: |
| 123 | + |
| 124 | +- `GET Credentials` |
| 125 | +- query parameter `type=Password` |
| 126 | +- header `Authorization: A2A <apiKey>` |
| 127 | +- client certificate on the TLS connection |
| 128 | + |
| 129 | +### Set a password |
| 130 | + |
| 131 | +The setter method name is capitalized in this SDK: |
| 132 | + |
| 133 | +```java |
| 134 | +a2aContext.SetPassword(apiKey, newPassword); |
| 135 | +``` |
| 136 | + |
| 137 | +That becomes `PUT Credentials/Password` with a JSON string body. |
| 138 | + |
| 139 | +### Retrieve or set an SSH private key |
| 140 | + |
| 141 | +```java |
| 142 | +char[] privateKey = a2aContext.retrievePrivateKey(apiKey, KeyFormat.OpenSsh); |
| 143 | +a2aContext.SetPrivateKey(apiKey, privateKey, passphrase, KeyFormat.OpenSsh); |
| 144 | +``` |
| 145 | + |
| 146 | +The getter uses `GET Credentials?type=PrivateKey&keyFormat=<format>`. |
| 147 | +The setter uses `PUT Credentials/SshKey` with a serialized `SshKey` payload. |
| 148 | + |
| 149 | +### Retrieve API key secrets |
| 150 | + |
| 151 | +```java |
| 152 | +List<IApiKeySecret> secrets = a2aContext.retrieveApiKeySecret(apiKey); |
| 153 | +``` |
| 154 | + |
| 155 | +This uses `GET Credentials?type=ApiKey` and maps the response into `ApiKeySecret` |
| 156 | +objects with `clientId`, `clientSecret`, and related metadata. |
| 157 | + |
| 158 | +## 4. Brokering |
| 159 | + |
| 160 | +SafeguardJava supports brokering through: |
| 161 | + |
| 162 | +- `IBrokeredAccessRequest` |
| 163 | +- `BrokeredAccessRequest` |
| 164 | +- `BrokeredAccessRequestType` |
| 165 | +- `ISafeguardA2AContext.brokerAccessRequest(...)` |
| 166 | + |
| 167 | +The test harness in `tests/safeguardjavaclient/.../SafeguardTests.java` builds a |
| 168 | +`BrokeredAccessRequest`, sets `AccountId`, `AssetId`, `ForUserId`, and an access type, |
| 169 | +then calls `brokerAccessRequest(...)`. |
| 170 | + |
| 171 | +Minimal pattern: |
| 172 | + |
| 173 | +```java |
| 174 | +IBrokeredAccessRequest request = new BrokeredAccessRequest(); |
| 175 | +request.setForUserId(forUserId); |
| 176 | +request.setAssetId(assetId); |
| 177 | +request.setAccountId(accountId); |
| 178 | +request.setAccessType(BrokeredAccessRequestType.Password); |
| 179 | +request.setReasonComment("Created by service broker"); |
| 180 | + |
| 181 | +String result = a2aContext.brokerAccessRequest(apiKey, request); |
| 182 | +``` |
| 183 | + |
| 184 | +What the SDK enforces before sending `POST AccessRequests`: |
| 185 | + |
| 186 | +- either `ForUserId` or `ForUserName` must be set |
| 187 | +- either `AssetId` or `AssetName` must be set |
| 188 | +- `apiKey` and `accessRequest` cannot be null |
| 189 | +- the SDK stamps the request version from the active `apiVersion` |
| 190 | + |
| 191 | +`BrokeredAccessRequest` also exposes optional fields for emergency access, reason |
| 192 | +codes, ticket numbers, `RequestedFor`, and day/hour/minute duration values. |
| 193 | + |
| 194 | +## 5. Event listeners / SignalR |
| 195 | + |
| 196 | +A2A supports SignalR listeners through the same `ISafeguardEventListener` interface used |
| 197 | +for standard Safeguard events. |
| 198 | + |
| 199 | +### Context-based listeners |
| 200 | + |
| 201 | +From `ISafeguardA2AContext`: |
| 202 | + |
| 203 | +- `getA2AEventListener(char[] apiKey, ISafeguardEventHandler handler)` |
| 204 | +- `getA2AEventListener(List<char[]> apiKeys, ISafeguardEventHandler handler)` |
| 205 | +- `getPersistentA2AEventListener(char[] apiKey, ISafeguardEventHandler handler)` |
| 206 | +- `getPersistentA2AEventListener(List<char[]> apiKeys, ISafeguardEventHandler handler)` |
| 207 | + |
| 208 | +`SafeguardA2AContext` automatically registers these event names on the listener: |
| 209 | + |
| 210 | +- `AssetAccountPasswordUpdated` |
| 211 | +- `AssetAccountSshKeyUpdated` |
| 212 | +- `AccountApiKeySecretUpdated` |
| 213 | + |
| 214 | +Basic usage: |
| 215 | + |
| 216 | +```java |
| 217 | +ISafeguardEventHandler handler = (eventName, eventBody) -> { |
| 218 | + System.out.println(eventName); |
| 219 | + System.out.println(eventBody); |
| 220 | +}; |
| 221 | + |
| 222 | +ISafeguardEventListener listener = |
| 223 | + a2aContext.getPersistentA2AEventListener(apiKey, handler); |
| 224 | +listener.start(); |
| 225 | +``` |
| 226 | + |
| 227 | +### Factory-based listeners |
| 228 | + |
| 229 | +If you do not want to build the context yourself, `Safeguard.A2A.Event` exposes many |
| 230 | +`getPersistentA2AEventListener(...)` overloads that accept certificate file, keystore, |
| 231 | +thumbprint, or in-memory certificate inputs directly. |
| 232 | + |
| 233 | +### Reconnect behavior |
| 234 | + |
| 235 | +`PersistentSafeguardA2AEventListener` extends `PersistentSafeguardEventListenerBase`. |
| 236 | +When the internal SignalR listener disconnects, the base class: |
| 237 | + |
| 238 | +1. disposes the broken listener |
| 239 | +2. recreates it from the A2A context |
| 240 | +3. re-registers handlers |
| 241 | +4. sleeps 5 seconds between failed reconnect attempts |
| 242 | + |
| 243 | +This is the right choice for long-running services. |
| 244 | + |
| 245 | +Non-persistent listeners do **not** recover from long outages. |
| 246 | +The interface documentation calls out the 30+ second outage case explicitly. |
| 247 | + |
| 248 | +## 6. Error scenarios and troubleshooting |
| 249 | + |
| 250 | +Common failures are visible directly in the public methods: |
| 251 | + |
| 252 | +- `ObjectDisposedException` if you call the context after `dispose()` |
| 253 | +- `ArgumentException` for null `apiKey`, null password/private-key arguments, or an empty API key list |
| 254 | +- `SafeguardForJavaException("Unable to connect to web service ...")` when the HTTP client returns null |
| 255 | +- `SafeguardForJavaException("Error returned from Safeguard API, Error: <status> <body>")` for non-2xx responses |
| 256 | +- `SafeguardForJavaException("You must specify a user...")` or `("You must specify an asset...")` during brokering |
| 257 | +- `SafeguardForJavaException("Error parsing JSON response")` or serialization failures when payload conversion breaks |
| 258 | +- `SafeguardForJavaException("Missing SunMSCAPI provider...")` for Windows thumbprint usage without the provider |
| 259 | +- `SafeguardForJavaException("Not implemented. This function is only available on the Windows platform.")` for thumbprint overloads on non-Windows hosts |
| 260 | + |
| 261 | +Troubleshooting checklist: |
| 262 | + |
| 263 | +1. verify the certificate chain is trusted by Safeguard |
| 264 | +2. confirm the certificate maps to the intended application identity |
| 265 | +3. confirm the API key belongs to the registration you expect |
| 266 | +4. use `getRetrievableAccounts()` to prove what the certificate can actually see |
| 267 | +5. switch from a transient listener to a persistent listener if outages matter |
| 268 | +6. avoid `ignoreSsl=true` outside lab scenarios |
| 269 | +7. clear API keys, passwords, and retrieved secrets from memory when finished |
0 commit comments