|
| 1 | +# concepts-identifiers |
| 2 | + |
| 3 | +How identifiers work in connectors. Four "ExternalId" concepts exist - only one matters. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## The Four ExternalId Concepts |
| 8 | + |
| 9 | +| Name | Type | Used by C1? | |
| 10 | +|------|------|-------------| |
| 11 | +| `connector_v2.Resource.ExternalId` | proto field | **YES - provisioning** | |
| 12 | +| `ConnectorResource.ExternalId` | v1 model string | YES - v1 sync only | |
| 13 | +| `ConnectorV2Resource.ExternalID()` | method | YES - returns ResourceId | |
| 14 | +| `SourceConnectorIds[conn_id]` | map value | YES - sync + provisioning | |
| 15 | + |
| 16 | +**Key insight:** The SDK's `WithExternalID()` function sets the native system identifier that provisioning operations need to call the target API. See "WithExternalID - REQUIRED for Provisioning" section below. |
| 17 | + |
| 18 | +--- |
| 19 | + |
| 20 | +## What Actually Matters: ResourceId |
| 21 | + |
| 22 | +When creating resources, the `objectID` parameter becomes the matching key: |
| 23 | + |
| 24 | +```go |
| 25 | +// This is what matters for sync and provisioning |
| 26 | +resource, err := rs.NewUserResource( |
| 27 | + user.DisplayName, // displayName |
| 28 | + userResourceType, // type -> "user" |
| 29 | + user.ID, // objectID -> becomes ResourceId.Resource |
| 30 | + traitOptions, |
| 31 | +) |
| 32 | +``` |
| 33 | + |
| 34 | +The SDK serializes this as `"type::id"` (e.g., `"user::12345"`) and stores it in `SourceConnectorIds`. |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## Sync Flow |
| 39 | + |
| 40 | +``` |
| 41 | +Connector: NewUserResource(name, type, "12345") |
| 42 | + | |
| 43 | + v |
| 44 | +Resource.Id = ResourceId{ResourceType: "user", Resource: "12345"} |
| 45 | + | |
| 46 | + v |
| 47 | +SDK: ResourceIDToString(Resource.Id) = "user::12345" |
| 48 | + | |
| 49 | + v |
| 50 | +C1 Database: AppResource.SourceConnectorIds["conn_id"] = "user::12345" |
| 51 | +``` |
| 52 | + |
| 53 | +--- |
| 54 | + |
| 55 | +## Provisioning Flow |
| 56 | + |
| 57 | +``` |
| 58 | +C1 Database: SourceConnectorIds["conn_id"] = "user::12345" |
| 59 | + | |
| 60 | + v |
| 61 | +SDK: ParseV2ExternalID("user::12345") |
| 62 | + | |
| 63 | + v |
| 64 | +ResourceId{ResourceType: "user", Resource: "12345"} |
| 65 | + | |
| 66 | + v |
| 67 | +Connector.Grant() receives this ResourceId |
| 68 | +``` |
| 69 | + |
| 70 | +--- |
| 71 | + |
| 72 | +## ID Stability Requirements |
| 73 | + |
| 74 | +**IDs must be stable across syncs.** If you change how IDs are calculated: |
| 75 | +- C1 sees old resources as deleted |
| 76 | +- C1 sees new resources as created |
| 77 | +- Grant history is lost |
| 78 | +- Access reviews break |
| 79 | + |
| 80 | +```go |
| 81 | +// WRONG: ID changes based on data availability |
| 82 | +id := user.Id |
| 83 | +if id == "" { |
| 84 | + id = user.Email // Different format! |
| 85 | +} |
| 86 | + |
| 87 | +// WRONG: Using mutable values |
| 88 | +rs.NewUserResource(name, type, user.Email, ...) // Email can change |
| 89 | + |
| 90 | +// CORRECT: Use immutable system ID |
| 91 | +rs.NewUserResource(name, type, user.Id, ...) |
| 92 | +``` |
| 93 | + |
| 94 | +--- |
| 95 | + |
| 96 | +## WithExternalID - REQUIRED for Provisioning |
| 97 | + |
| 98 | +```go |
| 99 | +// ExternalId stores the native system identifier for provisioning |
| 100 | +rs.WithExternalID(&v2.ExternalId{ |
| 101 | + Id: user.NativeAPIId, // The ID the target API expects |
| 102 | + Link: fmt.Sprintf("https://admin.example.com/users/%s", user.NativeAPIId), |
| 103 | +}) |
| 104 | +``` |
| 105 | + |
| 106 | +**Why it matters:** ConductorOne assigns its own resource IDs (`match_baton_id`) that differ from the target system's native IDs. During provisioning, the SDK passes `ExternalId` to Grant/Revoke operations so the connector can call the target API. |
| 107 | + |
| 108 | +**During sync:** Set `WithExternalID()` with the native identifier |
| 109 | +**During provisioning:** Retrieve via `principal.GetExternalId().Id` |
| 110 | + |
| 111 | +```go |
| 112 | +// In Grant operation |
| 113 | +externalId := principal.GetExternalId() |
| 114 | +if externalId == nil { |
| 115 | + return nil, nil, fmt.Errorf("baton-myservice: principal missing external ID") |
| 116 | +} |
| 117 | +nativeUserID := externalId.Id // Use this to call target API |
| 118 | +``` |
| 119 | + |
| 120 | +--- |
| 121 | + |
| 122 | +## match_baton_id (Advanced) |
| 123 | + |
| 124 | +For pre-created manual resources that need to merge with connector-discovered resources: |
| 125 | + |
| 126 | +```go |
| 127 | +// Connector sets RawId annotation |
| 128 | +rs.WithAnnotation(&v2.RawId{Id: user.Email}) |
| 129 | +``` |
| 130 | + |
| 131 | +C1 matches this against `AppResource.MatchBatonId` for manually-managed resources. |
| 132 | + |
| 133 | +Most connectors don't need this. Only use when: |
| 134 | +- External resources are provisioned outside C1 |
| 135 | +- HR systems create accounts before connectors discover them |
| 136 | +- Pre-staging resources before first sync |
0 commit comments