Skip to content

Commit 0f1452f

Browse files
committed
direct: grants: derive revokes from plan changes
Build direct grants updates from the planned keyed diffs so removed principals can be revoked without rereading current grants. This keeps the reconciliation path smaller while preserving the existing privilege update semantics.
1 parent 1d69520 commit 0f1452f

File tree

2 files changed

+197
-91
lines changed

2 files changed

+197
-91
lines changed

bundle/direct/dresources/grants.go

Lines changed: 93 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,18 @@ import (
77
"slices"
88
"strings"
99

10+
"github.com/databricks/cli/libs/structs/structpath"
1011
"github.com/databricks/cli/libs/structs/structvar"
1112
"github.com/databricks/databricks-sdk-go"
1213
"github.com/databricks/databricks-sdk-go/service/catalog"
1314
)
1415

16+
const (
17+
grantsNodeSuffix = ".grants"
18+
grantsPrincipalKey = "principal"
19+
grantsFullNameError = "internal error: grants full_name must be resolved before deployment"
20+
)
21+
1522
var grantResourceToSecurableType = map[string]string{
1623
"catalogs": "catalog",
1724
"schemas": "schema",
@@ -27,9 +34,9 @@ type GrantsState struct {
2734
}
2835

2936
func PrepareGrantsInputConfig(inputConfig any, node string) (*structvar.StructVar, error) {
30-
baseNode, ok := strings.CutSuffix(node, ".grants")
37+
baseNode, ok := strings.CutSuffix(node, grantsNodeSuffix)
3138
if !ok {
32-
return nil, fmt.Errorf("internal error: node %q does not end with .grants", node)
39+
return nil, fmt.Errorf("internal error: node %q does not end with %s", node, grantsNodeSuffix)
3340
}
3441

3542
resourceType, err := extractGrantResourceType(node)
@@ -49,7 +56,7 @@ func PrepareGrantsInputConfig(inputConfig any, node string) (*structvar.StructVa
4956

5057
// Backend sorts privileges, so we sort here as well.
5158
for i := range *grantsPtr {
52-
sortPriviliges((*grantsPtr)[i].Privileges)
59+
sortPrivileges((*grantsPtr)[i].Privileges)
5360
}
5461

5562
return &structvar.StructVar{
@@ -77,7 +84,7 @@ func (*ResourceGrants) PrepareState(state *GrantsState) *GrantsState {
7784
}
7885

7986
func grantKey(x catalog.PrivilegeAssignment) (string, string) {
80-
return "principal", x.Principal
87+
return grantsPrincipalKey, x.Principal
8188
}
8289

8390
func (*ResourceGrants) KeyedSlices() map[string]any {
@@ -107,23 +114,16 @@ func (r *ResourceGrants) DoRead(ctx context.Context, id string) (*GrantsState, e
107114
}
108115

109116
func (r *ResourceGrants) DoCreate(ctx context.Context, state *GrantsState) (string, *GrantsState, error) {
110-
err := r.applyGrants(ctx, state, nil)
117+
err := r.updateGrants(ctx, state, nil)
111118
if err != nil {
112119
return "", nil, err
113120
}
114121

115122
return makeGrantsID(state.SecurableType, state.FullName), nil, nil
116123
}
117124

118-
func (r *ResourceGrants) DoUpdate(ctx context.Context, _ string, state *GrantsState, _ Changes) (*GrantsState, error) {
119-
if state.FullName == "" {
120-
return nil, errors.New("internal error: grants full_name must be resolved before deployment")
121-
}
122-
currentAssignments, err := r.listGrants(ctx, state.SecurableType, state.FullName)
123-
if err != nil {
124-
return nil, err
125-
}
126-
return nil, r.applyGrants(ctx, state, currentAssignments)
125+
func (r *ResourceGrants) DoUpdate(ctx context.Context, _ string, state *GrantsState, changes Changes) (*GrantsState, error) {
126+
return nil, r.updateGrants(ctx, state, changes)
127127
}
128128

129129
func (r *ResourceGrants) DoDelete(ctx context.Context, id string) error {
@@ -132,13 +132,33 @@ func (r *ResourceGrants) DoDelete(ctx context.Context, id string) error {
132132
return nil
133133
}
134134

135-
func (r *ResourceGrants) applyGrants(ctx context.Context, state *GrantsState, currentAssignments []catalog.PrivilegeAssignment) error {
135+
func (r *ResourceGrants) updateGrants(ctx context.Context, state *GrantsState, changes Changes) error {
136+
req, err := buildGrantUpdateRequest(state, changes)
137+
if err != nil {
138+
return err
139+
}
140+
_, err = r.client.Grants.Update(ctx, req)
141+
return err
142+
}
143+
144+
func buildGrantUpdateRequest(state *GrantsState, changes Changes) (catalog.UpdatePermissions, error) {
136145
if state.FullName == "" {
137-
return errors.New("internal error: grants full_name must be resolved before deployment")
146+
return catalog.UpdatePermissions{}, errors.New(grantsFullNameError)
138147
}
148+
removedPrincipals, err := removedGrantPrincipals(changes, state.EmbeddedSlice)
149+
if err != nil {
150+
return catalog.UpdatePermissions{}, err
151+
}
152+
return catalog.UpdatePermissions{
153+
SecurableType: state.SecurableType,
154+
FullName: state.FullName,
155+
Changes: buildGrantChanges(state.EmbeddedSlice, removedPrincipals),
156+
}, nil
157+
}
139158

140-
var changes []catalog.PermissionsChange
141-
for _, ga := range state.EmbeddedSlice {
159+
func buildGrantChanges(desiredAssignments []catalog.PrivilegeAssignment, removedPrincipals []string) []catalog.PermissionsChange {
160+
changes := make([]catalog.PermissionsChange, 0, len(desiredAssignments)+len(removedPrincipals))
161+
for _, ga := range desiredAssignments {
142162
change := catalog.PermissionsChange{
143163
Principal: ga.Principal,
144164
Add: ga.Privileges,
@@ -152,17 +172,22 @@ func (r *ResourceGrants) applyGrants(ctx context.Context, state *GrantsState, cu
152172
}
153173
changes = append(changes, change)
154174
}
155-
changes = append(changes, removedPrincipalChanges(currentAssignments, state.EmbeddedSlice)...)
156-
157-
_, err := r.client.Grants.Update(ctx, catalog.UpdatePermissions{
158-
SecurableType: state.SecurableType,
159-
FullName: state.FullName,
160-
Changes: changes,
161-
})
162-
return err
175+
for _, principal := range removedPrincipals {
176+
changes = append(changes, catalog.PermissionsChange{
177+
Principal: principal,
178+
Add: nil,
179+
Remove: []catalog.Privilege{catalog.PrivilegeAllPrivileges},
180+
ForceSendFields: nil,
181+
})
182+
}
183+
return changes
163184
}
164185

165-
func removedPrincipalChanges(currentAssignments, desiredAssignments []catalog.PrivilegeAssignment) []catalog.PermissionsChange {
186+
func removedGrantPrincipals(changes Changes, desiredAssignments []catalog.PrivilegeAssignment) ([]string, error) {
187+
if len(changes) == 0 {
188+
return nil, nil
189+
}
190+
166191
desiredPrincipals := make(map[string]struct{}, len(desiredAssignments))
167192
for _, assignment := range desiredAssignments {
168193
if assignment.Principal == "" {
@@ -171,32 +196,52 @@ func removedPrincipalChanges(currentAssignments, desiredAssignments []catalog.Pr
171196
desiredPrincipals[assignment.Principal] = struct{}{}
172197
}
173198

174-
currentAssignments = slices.Clone(currentAssignments)
175-
slices.SortFunc(currentAssignments, func(a, b catalog.PrivilegeAssignment) int {
176-
return strings.Compare(a.Principal, b.Principal)
177-
})
178-
179-
var changes []catalog.PermissionsChange
180-
for _, assignment := range currentAssignments {
181-
if assignment.Principal == "" {
199+
removedPrincipals := make(map[string]struct{})
200+
for pathString, change := range changes {
201+
if change == nil || change.Remote == nil {
182202
continue
183203
}
184-
if _, ok := desiredPrincipals[assignment.Principal]; ok {
204+
principal, ok, err := grantPrincipalFromPath(pathString)
205+
if err != nil {
206+
return nil, fmt.Errorf("internal error: parsing grants change path %q: %w", pathString, err)
207+
}
208+
if !ok {
185209
continue
186210
}
187-
if len(assignment.Privileges) == 0 {
211+
if _, ok := desiredPrincipals[principal]; ok {
188212
continue
189213
}
190-
privileges := slices.Clone(assignment.Privileges)
191-
sortPriviliges(privileges)
192-
changes = append(changes, catalog.PermissionsChange{
193-
Principal: assignment.Principal,
194-
Add: nil,
195-
Remove: privileges,
196-
ForceSendFields: nil,
197-
})
214+
removedPrincipals[principal] = struct{}{}
198215
}
199-
return changes
216+
217+
result := make([]string, 0, len(removedPrincipals))
218+
for principal := range removedPrincipals {
219+
result = append(result, principal)
220+
}
221+
if len(result) == 0 {
222+
return nil, nil
223+
}
224+
slices.Sort(result)
225+
return result, nil
226+
}
227+
228+
func grantPrincipalFromPath(pathString string) (string, bool, error) {
229+
path, err := structpath.ParsePath(pathString)
230+
if err != nil {
231+
return "", false, err
232+
}
233+
if path == nil {
234+
return "", false, nil
235+
}
236+
segments := path.AsSlice()
237+
if len(segments) == 0 {
238+
return "", false, nil
239+
}
240+
key, value, ok := segments[0].KeyValue()
241+
if !ok || key != grantsPrincipalKey {
242+
return "", false, nil
243+
}
244+
return value, true, nil
200245
}
201246

202247
func (r *ResourceGrants) listGrants(ctx context.Context, securableType, fullName string) ([]catalog.PrivilegeAssignment, error) {
@@ -218,8 +263,7 @@ func (r *ResourceGrants) listGrants(ctx context.Context, securableType, fullName
218263
if assignment.Principal == "" {
219264
continue
220265
}
221-
privs := make([]catalog.Privilege, len(assignment.Privileges))
222-
copy(privs, assignment.Privileges)
266+
privs := slices.Clone(assignment.Privileges)
223267
assignments = append(assignments, catalog.PrivilegeAssignment{
224268
Principal: assignment.Principal,
225269
Privileges: privs,
@@ -234,7 +278,7 @@ func (r *ResourceGrants) listGrants(ctx context.Context, securableType, fullName
234278
return assignments, nil
235279
}
236280

237-
func sortPriviliges(privileges []catalog.Privilege) {
281+
func sortPrivileges(privileges []catalog.Privilege) {
238282
slices.Sort(privileges)
239283
}
240284

0 commit comments

Comments
 (0)