vendor/- Third-party dependencies managed by Go modules
- Configuration fields in config.go should include a WithDisplayName
- Field relationships should be defined in the config.go file
- Secrets must use
field.WithIsSecret(true)
Resource types that do not list entitlements or grants should have the SkipEntitlementsAndGrants annotation in the ResourceType definition.
All connectors should be considered potentially in-use, and the data they expose should be considered a stable API.
- NEVER remove a resource type
- NEVER change how resource IDs are calculated - the ID of a given resource must remain stable across all versions of the connector
- NEVER change resource type IDs (e.g., changing
Id: "user"toId: "account") - EXERCISE EXTREME CAUTION when filtering out previously included resources
- EXERCISE EXTREME CAUTION when changing how any values associated with a resource are calculated
- NEVER remove an entitlement
- NEVER change entitlement slugs (e.g.,
"member"to"membership") - NEVER change permission entitlement names
Resources implementing the User trait may have an associated user profile, typically set using WithUserProfile. All changes to user profiles must remain backwards compatible:
- NEVER remove keys from user profiles
- NEVER change the type of a value in a user profile
- NEVER change how values are represented in a user profile - eg
aliceshould always bealice, notAliceoralice@example.com
In Grant/Revoke operations, context (workspace/org/tenant) must come from principal, not entitlement.
Flag this pattern:
entitlement.Resource.ParentResourceId // WRONG for contextCorrect pattern:
principal.ParentResourceId // Context comes from principalTwo failure modes - both cause data loss or infinite loops:
Early termination (stops too soon):
return resources, "", nil, nil // Always empty token - misses pagesInfinite loop (never stops):
return resources, "next", nil, nil // Hardcoded token - runs forever
return resources, nextPage, nil, nil // If nextPage never becomes ""Verify:
- Token comes from API response, not hardcoded
- There's a path where token becomes "" (termination condition)
- API's "no more pages" signal is correctly detected
When err != nil, the response resp may be nil. Accessing resp.Body or resp.StatusCode without nil check causes panics.
Flag this pattern:
resp, err := client.Do(req)
if err != nil {
log.Printf("status: %d", resp.StatusCode) // PANIC if resp nil
}Or this:
resp, err := client.Do(req)
defer resp.Body.Close() // PANIC - defer before error check
if err != nil {Direct type assertions without ok check can panic.
Flag this pattern:
value := data["key"].(string) // PANIC if wrong type or missingCorrect pattern:
value, ok := data["key"].(string)
if !ok { ... }Logging errors but continuing execution causes silent data loss.
Flag this pattern:
if err != nil {
log.Println("error:", err)
// No return - continues with bad state
}Errors should use %w for wrapping to preserve error chain, and include connector prefix.
Flag this pattern:
fmt.Errorf("failed: %v", err) // %v breaks error chain
fmt.Errorf("failed: %w", err) // Missing connector prefixCorrect pattern:
fmt.Errorf("baton-service: failed to list users: %w", err)Resource IDs must be stable across syncs. Using mutable fields like email as ID causes duplicate resources.
Flag this pattern:
rs.NewUserResource(name, userType, user.Email, ...) // Email can changeCorrect pattern:
rs.NewUserResource(name, userType, user.ID, ...) // Stable API IDAPIs may return numbers where code expects strings (or vice versa). Causes unmarshaling failures.
Flag this pattern:
type Group struct {
ID string `json:"id"` // Fails if API returns {"id": 12345}
}Correct pattern:
type Group struct {
ID json.Number `json:"id"` // Handles both "12345" and 12345
}AWS accounts, service accounts, and machine identities must use App trait, not User trait.
Flag this pattern:
// Suspicious: "account" with User trait
rs.NewUserResource(name, accountType, ...) // Should this be App?Ask: Is this a human who logs in? If no, use App trait.
Pagination bag must be initialized on first call or it panics.
Flag this pattern:
bag, _ := parsePageToken(pt.Token, &v2.ResourceId{})
token := bag.Current().Token // PANIC if first callCorrect pattern:
bag, _ := parsePageToken(pt.Token, &v2.ResourceId{})
if bag.Current() == nil {
bag.Push(pagination.PageState{ResourceTypeID: resourceType.Id})
}Connectors that create clients (HTTP, database, etc.) must implement io.Closer.
Flag this pattern:
func (c *Connector) Close() error {
return nil // NOT closing client resources
}Correct pattern:
func (c *Connector) Close() error {
if c.client != nil {
return c.client.Close()
}
return nil
}Detection:
- Check if connector creates clients in
New() - Verify
Close()actually closes them
Grant/Revoke should succeed if the state already matches.
Flag this pattern:
if err != nil && strings.Contains(err.Error(), "already exists") {
return nil, nil, err // WRONG - should succeed
}Correct pattern:
if err != nil && strings.Contains(err.Error(), "already exists") {
return nil, annotations.New(&v2.GrantAlreadyExists{}), nil
}Resources used in provisioning must have ExternalId set during sync.
Flag this pattern:
// Sync creates resource without ExternalId
rs.NewUserResource(name, userType, id, traits) // Missing WithExternalIDCorrect pattern:
rs.NewUserResource(name, userType, id, traits,
rs.WithExternalID(&v2.ExternalId{Id: nativeAPIId}),
)Also flag in Grant/Revoke:
// Using ResourceId instead of ExternalId for API calls
userID := principal.Id.Resource // May not be native IDCorrect pattern:
externalId := principal.GetExternalId()
if externalId == nil {
return nil, nil, fmt.Errorf("baton-myservice: principal missing external ID")
}
nativeUserID := externalId.Id // Use for API callsAccount/entitlement provisioning must use correct context.
Verify:
- Provisioning operations use principal's context (org/workspace), not entitlement's
- De-provisioning checks if resource exists before attempting delete
- Role/group membership changes are idempotent
Resources used in provisioning must have ExternalId set during sync.
Flag this pattern:
// Sync creates resource without ExternalId
rs.NewUserResource(name, userType, id, traits) // Missing WithExternalIDCorrect pattern:
rs.NewUserResource(name, userType, id, traits,
rs.WithExternalID(&v2.ExternalId{Id: nativeAPIId}),
)Also flag in Grant/Revoke:
// Using ResourceId instead of ExternalId for API calls
userID := principal.Id.Resource // May not be native IDCorrect pattern:
externalId := principal.GetExternalId()
if externalId == nil {
return nil, nil, fmt.Errorf("baton-myservice: principal missing external ID")
}
nativeUserID := externalId.Id // Use for API callsLarge numeric IDs can be formatted as scientific notation (e.g., 1.23456789e+15), breaking resource matching.
Flag this pattern:
// WRONG - fmt default formatting may use scientific notation for large numbers
id := fmt.Sprintf("%v", numericID)
id := fmt.Sprintf("%g", float64(numericID))Correct pattern:
// Use %d for integers, %s for strings, or strconv
id := fmt.Sprintf("%d", numericID)
id := strconv.FormatInt(numericID, 10)
id := strconv.FormatUint(uint64(numericID), 10)Also watch for: JSON unmarshaling large numbers into float64 which loses precision and may format as scientific notation.
# Entity confusion
entitlement\.Resource\.ParentResourceId
# Always-empty pagination
return .*, "", nil, nil
# Nil pointer risk
resp\.StatusCode|resp\.Body.*err
# Defer before check
defer.*Close\(\).*\n.*if err
# Type assertion without ok
\.\([A-Za-z]+\)[^,]
# Error swallowing
log\.Print.*err.*\n[^r]*$
# Wrong error verb
fmt\.Errorf.*%v.*err
# Resource leak (Close returns nil without closing)
func \(.*\) Close\(\).*error.*\{[\s]*return nil
# Scientific notation risk - %v or %g with numeric IDs
fmt\.Sprintf\("%[vg]".*[Ii][Dd]
Before approving:
- No changes to resource type IDs
- No changes to entitlement slugs
- No changes to resource ID derivation
- Context (workspace/org) comes from principal, not entitlement
- In Grant: principal = WHO, entitlement = WHAT
- API arguments in correct order (check API docs for multi-string params)
- Token comes from API response, not hardcoded
- Has path where token becomes "" (termination)
- Bag initialized on first call (
if bag.Current() == nil)
- HTTP response nil checks in error paths
- All type assertions use two-value form (
x, ok := ...) - ParentResourceId checked for nil before access
- Errors returned, not just logged
- Error messages include connector prefix (
baton-service:) - Uses
%wnot%vfor error wrapping
- API response IDs use
json.Numbernotstring(if API inconsistent) - Service accounts/machine identities use App trait, not User
- Resource IDs use stable API identifiers (not email)
- Large numeric IDs use
%dorstrconv, not%v(avoid scientific notation)
- Secrets not logged
- Context passed to all API calls
- Close() properly closes all client connections created in New()
- Grant/Revoke handles "already exists" as success (idempotent)
- If fixing a bug, does the fix preserve existing behavior for unaffected cases?
- If changing API mapping, are all downstream consumers considered?