Skip to content

Commit 38b829d

Browse files
authored
Merge pull request #223 from petrsnd/skills-harmonization
Skills harmonization: rename architecture, add api-patterns, build-and-release, a2a-workflow
2 parents c9f4a82 + d955e92 commit 38b829d

5 files changed

Lines changed: 893 additions & 118 deletions

File tree

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
---
2+
name: a2a-workflow
3+
description: Use when working with Safeguard A2A certificate auth, credential retrieval, brokering, or A2A event listeners.
4+
---
5+
6+
# A2A Workflow
7+
Safeguard A2A (Application-to-Application) is the SDK surface for unattended,
8+
certificate-based integrations. In this repo, `Safeguard.A2A.GetContext(...)`
9+
creates an `ISafeguardA2AContext` that talks to `/service/A2A/v<apiVersion>/...`
10+
with a client certificate plus an A2A API key. It is used for retrieving passwords,
11+
SSH keys, API key secrets, brokering access requests, and subscribing to A2A
12+
credential-change events.
13+
## 1. What A2A is (one-paragraph context)
14+
15+
A2A is the automation-friendly side of Safeguard. Unlike normal `Safeguard.Connect()`
16+
flows that authenticate a user and then send bearer tokens, A2A uses a client
17+
certificate to establish trust and an `Authorization: A2A <apiKey>` header to scope
18+
credential retrieval or brokering to a configured registration. The SDK models this
19+
with `ISafeguardA2AContext`, `SafeguardA2AContext`, `A2ARetrievableAccount`,
20+
`BrokeredAccessRequest`, and `ApiKeySecret`.
21+
## 2. Setup flow (certificate registration, API key creation)
22+
23+
The repository does not create A2A registrations for you; that setup happens on the
24+
Safeguard appliance. The codebase shows the expected appliance-side shape.
25+
26+
### Appliance-side prerequisites
27+
28+
1. Register a client certificate for the integration.
29+
2. Create one or more A2A registrations tied to that certificate user.
30+
3. Assign retrievable accounts and/or brokering rights.
31+
4. Generate the A2A API key that will be used for retrieval or brokering.
32+
33+
### SDK context creation patterns
34+
35+
`Safeguard.A2A.GetContext(...)` supports the same three certificate sources used by
36+
other certificate-based SDK entry points:
37+
38+
- certificate store thumbprint
39+
- PFX/PKCS#12 file + password
40+
- in-memory certificate bytes + password
41+
42+
Examples:
43+
44+
```csharp
45+
using var context = Safeguard.A2A.GetContext(
46+
appliance,
47+
thumbprint,
48+
apiVersion: 4,
49+
ignoreSsl: true);
50+
```
51+
52+
```csharp
53+
using var context = Safeguard.A2A.GetContext(
54+
appliance,
55+
certificatePath,
56+
certificatePassword,
57+
apiVersion: 4,
58+
ignoreSsl: true);
59+
```
60+
61+
```csharp
62+
var bytes = File.ReadAllBytes(certificatePath);
63+
using var context = Safeguard.A2A.GetContext(
64+
appliance,
65+
bytes,
66+
certificatePassword,
67+
apiVersion: 4,
68+
ignoreSsl: true);
69+
```
70+
71+
Validation-callback overloads also exist when you want custom certificate validation
72+
instead of `ignoreSsl`.
73+
74+
### Enumerating registrations and API keys
75+
76+
`Samples\SampleA2aService\SampleService.cs` shows a practical discovery flow:
77+
78+
1. Create a normal certificate-backed `ISafeguardConnection`
79+
2. Query `Service.Core` `A2ARegistrations`
80+
3. Filter by `CertificateUserThumbprint`
81+
4. Query `A2ARegistrations/{id}/RetrievableAccounts`
82+
5. Read the returned `ApiKey` values and cache them as `SecureString`
83+
84+
That sample uses:
85+
86+
```csharp
87+
var a2AJson = _connection.InvokeMethod(
88+
Service.Core,
89+
Method.Get,
90+
"A2ARegistrations",
91+
parameters: new Dictionary<string, string>
92+
{
93+
["filter"] = $"CertificateUserThumbprint ieq '{thumbprint}'",
94+
});
95+
```
96+
97+
Important setup notes pulled from the repo:
98+
99+
- `ISafeguardA2AContext.GetRetrievableAccounts()` is documented as a Safeguard v2.8+
100+
feature that must be enabled in the A2A configuration.
101+
- The sample comments note that enumerating registrations by certificate user may
102+
require auditor permission.
103+
- `SampleA2aService` throws if no API keys are found after enumeration.
104+
105+
## 3. Credential retrieval (programmatic access)
106+
107+
### Primary retrieval methods on `ISafeguardA2AContext`
108+
109+
| Method | Purpose |
110+
|---|---|
111+
| `GetRetrievableAccounts()` / `GetRetrievableAccounts(filter)` | Discover accessible accounts and API keys |
112+
| `RetrievePassword(apiKey)` | Fetch a password as `SecureString` |
113+
| `SetPassword(apiKey, password)` | Rotate/update a password |
114+
| `RetrievePrivateKey(apiKey, keyFormat)` | Fetch an SSH private key |
115+
| `SetPrivateKey(apiKey, privateKey, password, keyFormat)` | Upload/update an SSH private key |
116+
| `RetrieveApiKeySecret(apiKey)` | Fetch API key secret material as `IList<ApiKeySecret>` |
117+
118+
### Transport details
119+
120+
`SafeguardA2AContext` uses these routes internally:
121+
122+
- `GET Core/A2ARegistrations`
123+
- `GET Core/A2ARegistrations/{id}/RetrievableAccounts`
124+
- `GET A2A/Credentials?type=Password`
125+
- `PUT A2A/Credentials/Password`
126+
- `GET A2A/Credentials?type=PrivateKey&keyFormat=<format>`
127+
- `PUT A2A/Credentials/SshKey?keyFormat=<format>`
128+
- `GET A2A/Credentials?type=ApiKey`
129+
130+
The context sends:
131+
132+
- `Accept: application/json`
133+
- `Authorization: A2A <apiKey>` when an API key is required
134+
- the client certificate on the TLS connection
135+
136+
### Typical password retrieval pattern
137+
138+
```csharp
139+
using var context = Safeguard.A2A.GetContext(appliance, thumbprint, apiVersion: 4, ignoreSsl: true);
140+
using var password = context.RetrievePassword(apiKey.ToSecureString());
141+
142+
var clearText = password.ToInsecureString();
143+
```
144+
145+
### Discovering retrievable accounts
146+
147+
`Test\SafeguardDotNetA2aTool` uses `GetRetrievableAccounts()` in two modes:
148+
149+
- no filter -> enumerate everything visible to the registration
150+
- SCIM-style filter -> e.g. `AccountName eq 'admin'`
151+
152+
The filter is applied server-side to each registration's retrievable-accounts endpoint.
153+
154+
### API key secret retrieval
155+
156+
`RetrieveApiKeySecret()` returns `ApiKeySecret` objects whose `ClientSecret` is a
157+
`SecureString`. Dispose them when you are done.
158+
159+
### Secure disposal expectations
160+
161+
- `ApiKeySecret` implements `IDisposable`
162+
- `A2ARetrievableAccount.ApiKey` is treated as sensitive data
163+
- certificate passwords and retrieved credentials should stay in `SecureString`
164+
- top-level services such as `SampleA2aService` dispose listeners, connections, and
165+
A2A contexts during shutdown
166+
167+
## 4. Brokering (if supported)
168+
169+
Yes. `ISafeguardA2AContext.BrokerAccessRequest()` posts a `BrokeredAccessRequest`
170+
object to `A2A/AccessRequests`.
171+
172+
### Minimum required fields
173+
174+
`SafeguardA2AContext` enforces these before the HTTP call:
175+
176+
- one of `ForUserId` or `ForUserName`
177+
- one of `AssetId` or `AssetName`
178+
179+
If either is missing, it throws `SafeguardDotNetException` before contacting the
180+
server.
181+
182+
### Useful optional fields from `BrokeredAccessRequest`
183+
184+
- `AccessType` (`Password`, `Ssh`, `Rdp`)
185+
- `AccountId` / `AccountName`
186+
- `AccountAssetId` / `AccountAssetName`
187+
- `ReasonCodeId` / `ReasonCode`
188+
- `ReasonComment`
189+
- `TicketNumber`
190+
- `RequestedFor`
191+
- `RequestedDuration`
192+
193+
`Test\SafeguardDotNetAccessRequestBrokerTool` shows the intended calling pattern:
194+
195+
```csharp
196+
using var context = CreateA2AContext(opts);
197+
var accessRequest = GetBrokeredAccessRequestObject(opts);
198+
var json = context.BrokerAccessRequest(opts.ApiKey.ToSecureString(), accessRequest);
199+
```
200+
201+
The tool accepts either IDs or names for user, asset, account, and reason code, then
202+
maps numeric strings to the `*Id` properties.
203+
204+
## 5. Event listeners / SignalR (if supported)
205+
206+
Yes. A2A supports both non-persistent and persistent SignalR listeners.
207+
208+
### Context-based listener APIs
209+
210+
| Method | Recovery behavior |
211+
|---|---|
212+
| `GetA2AEventListener(apiKey, handler)` | Does **not** recover from a 30+ second outage |
213+
| `GetA2AEventListener(apiKeys, handler)` | Same, but for multiple API keys |
214+
| `GetPersistentA2AEventListener(apiKey, handler)` | Reconnects automatically |
215+
| `GetPersistentA2AEventListener(apiKeys, handler)` | Reconnects automatically |
216+
217+
### Static helper APIs
218+
219+
`Safeguard.A2A.Event.GetPersistentA2AEventListener(...)` exposes the same persistent
220+
listener pattern directly from:
221+
222+
- thumbprint-based certificate auth
223+
- certificate file + password
224+
- in-memory certificate bytes + password
225+
- optional validation-callback overloads
226+
- single API key or multiple API keys
227+
228+
### Event names actually registered
229+
230+
The implementation registers handlers for three A2A events:
231+
232+
- `AssetAccountPasswordUpdated`
233+
- `AssetAccountSshKeyUpdated`
234+
- `AccountApiKeySecretUpdated`
235+
236+
This is worth knowing because some XML comments still describe only the password
237+
update event.
238+
239+
### Reconnect behavior
240+
241+
Persistent A2A listeners are backed by `PersistentSafeguardA2AEventListener`, which
242+
inherits the shared reconnect loop from `PersistentSafeguardEventListenerBase`:
243+
244+
- reconnect work runs in the background
245+
- failures log a warning and sleep for 5 seconds
246+
- the listener retries until reconnection succeeds or stop/dispose is requested
247+
248+
Use the persistent variant for Windows services, daemon-style workloads, or anything
249+
that must survive appliance/network interruptions.
250+
251+
### Sample patterns in this repo
252+
253+
- `Samples\SampleA2aService\SampleService.cs` starts one persistent listener per API key
254+
- `Test\SafeguardDotNetEventTool` supports single-key, multi-key, and "discover all keys"
255+
flows before starting a listener
256+
- both patterns call `Start()` explicitly after creating the listener
257+
258+
## 6. Error scenarios and troubleshooting
259+
260+
### Common argument/setup failures
261+
262+
- No certificate input -> the CLI tools throw `InvalidOperationException("Must specify CertificateFile or Thumbprint")`
263+
- Null `apiKey`, `password`, `privateKey`, or `accessRequest` -> `ArgumentException`
264+
- Empty API key set for multi-key listeners -> `ArgumentException`
265+
- Missing user or asset in brokered requests -> `SafeguardDotNetException`
266+
267+
### HTTP/API failures
268+
269+
`SafeguardA2AContext.ApiRequest()` throws `SafeguardDotNetException` when Safeguard
270+
returns a non-success HTTP status. The exception includes status and raw response
271+
content, so inspect `HttpStatusCode`, `ErrorMessage`, and `Response`.
272+
273+
### Timeouts and retries
274+
275+
- one-shot retrieval/brokering calls do **not** include an automatic retry loop
276+
- timeout surfaces as `SafeguardDotNetException("Request timeout to ...")`
277+
- retry policy for `RetrievePassword()`, `RetrievePrivateKey()`, or `BrokerAccessRequest()`
278+
is the caller's responsibility
279+
- only persistent SignalR listeners auto-reconnect
280+
281+
### SSL troubleshooting
282+
283+
- `ignoreSsl` bypasses certificate validation and is intended for dev/test only
284+
- production integrations should prefer trusted certificates or a validation callback
285+
- the same certificate source pattern is reused across normal SDK connections and A2A
286+
287+
### Registration discovery troubleshooting
288+
289+
If registration enumeration fails in the sample flow, check:
290+
291+
- whether the certificate thumbprint matches the A2A registration's certificate user
292+
- whether the registration has retrievable accounts configured
293+
- whether the appliance version/config supports `GetRetrievableAccounts()`
294+
- whether the caller has permission to enumerate `A2ARegistrations`
295+
296+
If you only need retrieval and already have a valid API key, skip the Core registration
297+
lookup and call the A2A context methods directly.

0 commit comments

Comments
 (0)