|
| 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