Skip to content

Commit 5af6e1f

Browse files
author
Baton Admin
committed
chore: update connector skills via baton-admin
1 parent 17c1ec8 commit 5af6e1f

File tree

1 file changed

+228
-0
lines changed

1 file changed

+228
-0
lines changed
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
# build-provisioning
2+
3+
Implementing Grant, Revoke, and account operations.
4+
5+
---
6+
7+
## Grant Interface
8+
9+
```go
10+
func (g *groupBuilder) Grant(ctx context.Context, principal *v2.Resource,
11+
entitlement *v2.Entitlement) ([]*v2.Grant, annotations.Annotations, error)
12+
```
13+
14+
**Parameters:**
15+
- `principal` - Who is receiving the grant (usually a user)
16+
- `entitlement` - What permission is being granted
17+
18+
---
19+
20+
## Grant Implementation
21+
22+
```go
23+
func (g *groupBuilder) Grant(ctx context.Context, principal *v2.Resource,
24+
entitlement *v2.Entitlement) ([]*v2.Grant, annotations.Annotations, error) {
25+
26+
// 1. Validate principal type
27+
if principal.Id.ResourceType != "user" {
28+
return nil, nil, fmt.Errorf("baton-myservice: only users can be granted group membership")
29+
}
30+
31+
// 2. Extract IDs - use ExternalId for the native system identifier
32+
groupID := entitlement.Resource.Id.Resource
33+
34+
// Get native user ID from ExternalId (required for provisioning)
35+
externalId := principal.GetExternalId()
36+
if externalId == nil {
37+
return nil, nil, fmt.Errorf("baton-myservice: principal missing external ID")
38+
}
39+
nativeUserID := externalId.Id
40+
41+
// 3. Call API to add membership using native ID
42+
err := g.client.AddGroupMember(ctx, groupID, nativeUserID)
43+
if err != nil {
44+
// 4. Handle "already exists" as success (idempotency)
45+
if isAlreadyExistsError(err) {
46+
grant := sdkGrant.NewGrant(entitlement.Resource, entitlement.Slug, principal.Id)
47+
return []*v2.Grant{grant}, annotations.New(&v2.GrantAlreadyExists{}), nil
48+
}
49+
return nil, nil, fmt.Errorf("baton-myservice: failed to add group member: %w", err)
50+
}
51+
52+
// 5. Return the created grant
53+
grant := sdkGrant.NewGrant(entitlement.Resource, entitlement.Slug, principal.Id)
54+
return []*v2.Grant{grant}, nil, nil
55+
}
56+
```
57+
58+
**Note on ExternalId:** During sync, set `WithExternalID()` with the native system identifier. During provisioning, retrieve it via `GetExternalId()` to make API calls. See `concepts-identifiers.md` for details.
59+
60+
---
61+
62+
## Revoke Interface
63+
64+
```go
65+
func (g *groupBuilder) Revoke(ctx context.Context, grant *v2.Grant) (annotations.Annotations, error)
66+
```
67+
68+
**Parameters:**
69+
- `grant` - Contains both principal and entitlement info
70+
71+
---
72+
73+
## Revoke Implementation
74+
75+
```go
76+
func (g *groupBuilder) Revoke(ctx context.Context, grant *v2.Grant) (annotations.Annotations, error) {
77+
78+
// 1. Extract IDs from grant - use ExternalId for native identifier
79+
groupID := grant.Entitlement.Resource.Id.Resource
80+
81+
// Get native user ID from ExternalId
82+
externalId := grant.Principal.GetExternalId()
83+
if externalId == nil {
84+
return nil, fmt.Errorf("baton-myservice: principal missing external ID")
85+
}
86+
nativeUserID := externalId.Id
87+
88+
// 2. Call API to remove membership using native ID
89+
err := g.client.RemoveGroupMember(ctx, groupID, nativeUserID)
90+
if err != nil {
91+
// 3. Handle "not found" as success (idempotency)
92+
if isNotFoundError(err) {
93+
return annotations.New(&v2.GrantAlreadyRevoked{}), nil
94+
}
95+
return nil, fmt.Errorf("baton-myservice: failed to remove group member: %w", err)
96+
}
97+
98+
return nil, nil
99+
}
100+
```
101+
102+
---
103+
104+
## Idempotency Requirements
105+
106+
**Grant must handle "already exists":**
107+
```go
108+
if isAlreadyExistsError(err) {
109+
// Return success with the grant
110+
grant := sdkGrant.NewGrant(...)
111+
return []*v2.Grant{grant}, nil, nil
112+
}
113+
```
114+
115+
**Revoke must handle "not found":**
116+
```go
117+
if isNotFoundError(err) {
118+
// Return success - desired state achieved
119+
return nil, nil
120+
}
121+
```
122+
123+
**Rationale:** Operations may be retried. Failing on "already done" causes unnecessary retry storms.
124+
125+
---
126+
127+
## Entity Source Pattern (CRITICAL)
128+
129+
In Grant/Revoke, data comes from two sources. Use the right one.
130+
131+
**Principal** provides context (who):
132+
```go
133+
// Context (workspace, org, tenant) comes from principal
134+
workspaceID := principal.ParentResourceId.Resource
135+
136+
// Native user ID for API calls comes from ExternalId
137+
externalId := principal.GetExternalId()
138+
nativeUserID := externalId.Id
139+
```
140+
141+
**Entitlement** provides target (what):
142+
```go
143+
// The permission/role being granted comes from entitlement
144+
roleID := entitlement.Resource.Id.Resource
145+
groupID := entitlement.Resource.Id.Resource
146+
```
147+
148+
**WRONG - caused 3 production reverts:**
149+
```go
150+
// Getting workspace from entitlement instead of principal
151+
workspaceID := entitlement.Resource.ParentResourceId.Resource // WRONG!
152+
```
153+
154+
**ALSO WRONG - missing ExternalId:**
155+
```go
156+
// Using ResourceId instead of native ID may not work for API calls
157+
userID := principal.Id.Resource // May not be what target API expects
158+
```
159+
160+
---
161+
162+
## AccountManager Interface
163+
164+
For creating/deleting user accounts:
165+
166+
```go
167+
type AccountManager interface {
168+
CreateAccount(ctx context.Context, resource *v2.AccountInfo) (
169+
*v2.CreateAccountResponse, annotations.Annotations, error)
170+
DeleteResource(ctx context.Context, resourceId *v2.ResourceId) (
171+
annotations.Annotations, error)
172+
}
173+
```
174+
175+
---
176+
177+
## CreateAccount Implementation
178+
179+
```go
180+
func (u *userBuilder) CreateAccount(ctx context.Context, accountInfo *v2.AccountInfo) (
181+
*v2.CreateAccountResponse, annotations.Annotations, error) {
182+
183+
// 1. Extract account info
184+
email := accountInfo.Email
185+
login := accountInfo.Login
186+
187+
// 2. Create user via API
188+
newUser, err := u.client.CreateUser(ctx, email, login)
189+
if err != nil {
190+
return nil, nil, fmt.Errorf("baton-myservice: failed to create user: %w", err)
191+
}
192+
193+
// 3. Build resource for the new user
194+
resource, err := rs.NewUserResource(
195+
newUser.Name,
196+
userResourceType,
197+
newUser.ID,
198+
[]rs.UserTraitOption{rs.WithEmail(email, true)},
199+
)
200+
if err != nil {
201+
return nil, nil, err
202+
}
203+
204+
return &v2.CreateAccountResponse{
205+
Resource: resource,
206+
}, nil, nil
207+
}
208+
```
209+
210+
---
211+
212+
## Capability Declaration
213+
214+
Provisioning capabilities must be declared:
215+
216+
```go
217+
func (c *Connector) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error) {
218+
return &v2.ConnectorMetadata{
219+
DisplayName: "My Service",
220+
Capabilities: []v2.ConnectorCapability{
221+
v2.ConnectorCapability_CONNECTOR_CAPABILITY_SYNC,
222+
v2.ConnectorCapability_CONNECTOR_CAPABILITY_PROVISIONING,
223+
},
224+
}, nil
225+
}
226+
```
227+
228+
Resource types also declare capabilities in `baton_capabilities.json` (auto-generated).

0 commit comments

Comments
 (0)