Skip to content

Commit c8e2f4e

Browse files
author
Baton Admin
committed
chore: add CLAUDE.md for AI assistants via baton-admin
1 parent e583078 commit c8e2f4e

File tree

1 file changed

+248
-0
lines changed

1 file changed

+248
-0
lines changed

CLAUDE.md

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
# CLAUDE.md
2+
3+
Instructions for AI assistants working with this Baton connector.
4+
5+
## Additional Documentation
6+
7+
Detailed connector documentation is in `.claude/skills/connector/`:
8+
- `INDEX.md` - Skills overview and selection guide
9+
- `concepts-identifiers.md` - ResourceId vs ExternalId (CRITICAL for provisioning)
10+
- `build-provisioning.md` - Grant/Revoke implementation patterns
11+
- `build-pagination.md` - Pagination strategies
12+
- `ref-unused-features.md` - SDK feature usage notes
13+
14+
**Change-Type Specific Guidance**: See `CHANGE_TYPES.md` for guidance based on what type of change you're making (SDK upgrade, pagination fix, panic fix, provisioning, etc.).
15+
16+
## What This Is
17+
18+
A ConductorOne Baton connector that syncs identity and access data from a downstream service. Connectors implement the `ResourceSyncer` interface to expose users, groups, roles, and their relationships.
19+
20+
## Build & Test
21+
22+
```bash
23+
go build ./cmd/baton-* # Build connector
24+
go test ./... # Run tests
25+
go test -v ./... -count=1 # Verbose, no cache
26+
```
27+
28+
## Architecture
29+
30+
**SDK Inversion of Control:** The connector implements interfaces; the SDK orchestrates execution.
31+
32+
**Four Sync Phases (SDK-driven):**
33+
1. `ResourceType()` - Declare what resource types exist
34+
2. `List()` - Fetch all resources of each type
35+
3. `Entitlements()` - Fetch available permissions per resource
36+
4. `Grants()` - Fetch who has what access
37+
38+
**Key Interfaces:**
39+
- `ResourceSyncer` - Main interface for sync (List, Entitlements, Grants)
40+
- `ResourceBuilder` - Creates resources with traits
41+
- `ConnectorBuilder` - Wires everything together
42+
43+
## Critical Patterns
44+
45+
### Pagination Termination
46+
47+
```go
48+
// WRONG - stops after first page
49+
return resources, "", nil, nil
50+
51+
// CORRECT - pass through API's token
52+
if resp.NextPage == "" {
53+
return resources, "", nil, nil
54+
}
55+
return resources, resp.NextPage, nil, nil
56+
```
57+
58+
### Entity Sources in Grant/Revoke
59+
60+
```go
61+
func (g *groupBuilder) Grant(ctx context.Context, principal *v2.Resource,
62+
entitlement *v2.Entitlement) ([]*v2.Grant, annotations.Annotations, error) {
63+
64+
// WHO - get native ID from ExternalId (required for API calls)
65+
externalId := principal.GetExternalId()
66+
if externalId == nil {
67+
return nil, nil, fmt.Errorf("baton-service: principal missing external ID")
68+
}
69+
nativeUserID := externalId.Id // Use this for API calls
70+
71+
// Fallback: principal.Id.Resource if you set ExternalId to same value during sync
72+
73+
// WHAT - from entitlement
74+
groupID := entitlement.Resource.Id.Resource
75+
76+
// CONTEXT (workspace/org) - from principal, NOT entitlement
77+
workspaceID := principal.ParentResourceId.Resource // CORRECT
78+
// workspaceID := entitlement.Resource.ParentResourceId.Resource // WRONG
79+
}
80+
```
81+
82+
**Note on ExternalId:** During sync, set `WithExternalID()` with the native system identifier. During Grant/Revoke, retrieve it via `GetExternalId()` to make API calls. ConductorOne assigns its own resource IDs that differ from the target system's native IDs.
83+
84+
### HTTP Response Safety
85+
86+
```go
87+
resp, err := client.Do(req)
88+
if err != nil {
89+
if resp != nil { // resp may be nil on network errors
90+
defer resp.Body.Close()
91+
}
92+
return fmt.Errorf("baton-service: request failed: %w", err)
93+
}
94+
defer resp.Body.Close() // After error check
95+
```
96+
97+
### Error Handling
98+
99+
```go
100+
// Always include connector prefix and use %w
101+
return fmt.Errorf("baton-service: failed to list users: %w", err)
102+
103+
// Never swallow errors
104+
if err != nil {
105+
log.Println(err) // WRONG - continues with bad state
106+
return nil, "", nil, err // CORRECT - propagate
107+
}
108+
```
109+
110+
### JSON Type Safety
111+
112+
```go
113+
// WRONG - fails if API returns {"id": 12345}
114+
type Group struct {
115+
ID string `json:"id"`
116+
}
117+
118+
// CORRECT - handles both string and number
119+
type Group struct {
120+
ID json.Number `json:"id"`
121+
}
122+
123+
// Usage
124+
groupID := group.ID.String()
125+
```
126+
127+
For complex cases, use a custom unmarshaler:
128+
129+
```go
130+
type FlexibleID string
131+
132+
func (f *FlexibleID) UnmarshalJSON(data []byte) error {
133+
var s string
134+
if json.Unmarshal(data, &s) == nil {
135+
*f = FlexibleID(s)
136+
return nil
137+
}
138+
var n int64
139+
if json.Unmarshal(data, &n) == nil {
140+
*f = FlexibleID(strconv.FormatInt(n, 10))
141+
return nil
142+
}
143+
return fmt.Errorf("id must be string or number")
144+
}
145+
```
146+
147+
### Grant Idempotency
148+
149+
```go
150+
// Grant "already exists" = success, not error
151+
if isAlreadyExistsError(err) {
152+
return nil, annotations.New(&v2.GrantAlreadyExists{}), nil
153+
}
154+
155+
// Revoke "not found" = success, not error
156+
if isNotFoundError(err) {
157+
return annotations.New(&v2.GrantAlreadyRevoked{}), nil
158+
}
159+
```
160+
161+
**Key point:** "Already exists" and "already revoked" are NOT errors - return `nil` error with the annotation.
162+
163+
### Resource Cleanup
164+
165+
```go
166+
// Connectors that create clients MUST close them
167+
func (c *Connector) Close() error {
168+
if c.client != nil {
169+
return c.client.Close()
170+
}
171+
return nil
172+
}
173+
```
174+
175+
## Common Mistakes
176+
177+
1. **Pagination bugs** - Infinite loop (hardcoded token) or early termination (always empty token)
178+
2. **Entity confusion** - Getting workspace from entitlement instead of principal
179+
3. **Swapped arguments** - Multiple string params in wrong order (check API docs!)
180+
4. **Nil pointer panic** - Accessing resp.Body when resp is nil
181+
5. **Error swallowing** - Logging but not returning errors
182+
6. **Unstable IDs** - Using email instead of stable API ID
183+
7. **JSON type mismatch** - API returns number, Go expects string (use json.Number)
184+
8. **Wrong trait type** - Using User for service accounts (use App)
185+
9. **Pagination bag not init** - Forgetting to initialize bag on first call
186+
10. **Resource leak** - Close() returns nil without closing client connections
187+
11. **Non-idempotent grants** - Returning error on "already exists" instead of success
188+
12. **Missing ExternalId** - Not setting WithExternalID() during sync; provisioning then fails
189+
13. **Scientific notation** - Using `%v` with large numeric IDs produces `1.23e+15` instead of `1234567890123456`
190+
191+
## Resource Types
192+
193+
| Type | Trait | Typical Use |
194+
|------|-------|-------------|
195+
| User | `TRAIT_USER` | Human identities |
196+
| Group | `TRAIT_GROUP` | Collections with membership |
197+
| Role | `TRAIT_ROLE` | Permission sets |
198+
| App | `TRAIT_APP` | Service accounts, API keys |
199+
200+
## What NOT to Do
201+
202+
- Don't buffer all pages in memory (OOM risk)
203+
- Don't ignore context cancellation
204+
- Don't log secrets
205+
- Don't hardcode API URLs (breaks testing)
206+
- Don't use `%v` for error wrapping (use `%w`)
207+
- Don't return nil from Close() if you created clients
208+
- Don't return error on "already exists" for Grant operations
209+
- Don't use `%v` or `%g` with large numeric IDs (use `%d` or `strconv` to avoid scientific notation)
210+
211+
## SDK Features: Usage Notes
212+
213+
**Required for provisioning:**
214+
- `WithExternalID()` - REQUIRED for Grant/Revoke to work. Stores the native system ID that provisioning operations need to call the target API. During Grant, retrieve via `principal.GetExternalId().Id`.
215+
216+
**Rarely used (but valid):**
217+
- `WithMFAStatus()`, `WithSSOStatus()` - Only relevant for IDP connectors
218+
- `WithStructuredName()` - Rarely needed; DisplayName usually sufficient
219+
- Complex user profile fields beyond basics - Only if downstream needs them
220+
221+
## Testing
222+
223+
Connectors should support:
224+
- `--base-url` flag for mock server testing
225+
- `--insecure` flag for self-signed certs in tests
226+
227+
## File Structure
228+
229+
```
230+
cmd/baton-*/main.go # Entry point
231+
pkg/connector/connector.go # ConnectorBuilder implementation
232+
pkg/connector/*_builder.go # ResourceSyncer implementations
233+
pkg/client/client.go # API client
234+
```
235+
236+
## Debugging
237+
238+
```bash
239+
# Run with debug logging
240+
LOG_LEVEL=debug ./baton-* --config-file=config.yaml
241+
242+
# Output to specific file
243+
./baton-* --file=sync.c1z
244+
245+
# Inspect output
246+
baton resources --file=sync.c1z
247+
baton grants --file=sync.c1z
248+
```

0 commit comments

Comments
 (0)