diff --git a/pkg/connector/groups.go b/pkg/connector/groups.go index 8d52d21b..e39dec45 100644 --- a/pkg/connector/groups.go +++ b/pkg/connector/groups.go @@ -3,9 +3,12 @@ package connector import ( "context" "fmt" + "net/http" "slices" "strings" + "errors" + "github.com/conductorone/baton-databricks/pkg/databricks" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" @@ -19,6 +22,7 @@ import ( ) const groupMemberEntitlement = "member" +const groupManagerEntitlement = "roles/group.manager" type groupBuilder struct { client *databricks.Client @@ -133,7 +137,7 @@ func (g *groupBuilder) List(ctx context.Context, parentResourceID *v2.ResourceId // Group can have members, which represent membership entitlements, // it can have permissions assigned to it, which represent role permissions entitlements, // and it can also have entitlements assigned to it, which are represented in role resource type. -func (g *groupBuilder) Entitlements(_ context.Context, resource *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { +func (g *groupBuilder) Entitlements(ctx context.Context, resource *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { var rv []*v2.Entitlement var workspaceId string @@ -157,7 +161,7 @@ func (g *groupBuilder) Entitlements(_ context.Context, resource *v2.Resource, _ // role permissions entitlements // get all assignable roles for this specific group resource - roles, _, err := g.client.ListRoles(context.Background(), workspaceId, GroupsType, groupId.Resource) + roles, _, err := g.client.ListRoles(ctx, workspaceId, GroupsType, groupId.Resource) if err != nil { return nil, "", nil, fmt.Errorf("databricks-connector: failed to list roles for group %s: %w", groupId.Resource, err) } @@ -299,8 +303,9 @@ func (g *groupBuilder) Grant(ctx context.Context, principal *v2.Resource, entitl workspaceId = parentGroupId.Resource } - // If the entitlement is a member entitlement - if entitlement.Slug == groupMemberEntitlement { + membershipEntitlementID := ent.NewEntitlementID(entitlement.Resource, groupMemberEntitlement) + managerEntitlementID := ent.NewEntitlementID(entitlement.Resource, groupManagerEntitlement) + if entitlement.Id == membershipEntitlementID { group, _, err := g.client.GetGroup(ctx, workspaceId, groupId.Resource) if err != nil { return nil, fmt.Errorf("databricks-connector: failed to get group %s: %w", groupId.Resource, err) @@ -311,7 +316,7 @@ func (g *groupBuilder) Grant(ctx context.Context, principal *v2.Resource, entitl l.Info( "databricks-connector: group already has the member added", zap.String("principal_id", principal.Id.Resource), - zap.String("entitlement", entitlement.Slug), + zap.String("entitlement", groupMemberEntitlement), ) return nil, nil @@ -342,10 +347,21 @@ func (g *groupBuilder) Grant(ctx context.Context, principal *v2.Resource, entitl return nil, fmt.Errorf("databricks-connector: failed to prepare principal id: %w", err) } + role := groupManagerEntitlement + if workspaceId == "" && entitlement.Id != managerEntitlementID { + return nil, fmt.Errorf("databricks-connector: only group manager entitlement is supported for role permissions for group %s", groupId.Resource) + } else if workspaceId != "" { + role = entitlement.Slug + } + + if role == "" { + return nil, fmt.Errorf("databricks-connector: role is empty") + } + found := false for i, ruleSet := range ruleSets { - if ruleSet.Role == entitlement.Slug { + if ruleSet.Role == role { found = true // check if it contains the principals and add principal to the rule set @@ -353,7 +369,7 @@ func (g *groupBuilder) Grant(ctx context.Context, principal *v2.Resource, entitl l.Info( "databricks-connector: group already has the entitlement", zap.String("principal_id", principalID), - zap.String("entitlement", entitlement.Slug), + zap.String("entitlement", role), ) return nil, nil @@ -366,13 +382,21 @@ func (g *groupBuilder) Grant(ctx context.Context, principal *v2.Resource, entitl if !found { ruleSets = append(ruleSets, databricks.RuleSet{ - Role: entitlement.Slug, + Role: role, Principals: []string{principalID}, }) } _, err = g.client.UpdateRuleSets(ctx, workspaceId, GroupsType, groupId.Resource, ruleSets) if err != nil { + var apiErr *databricks.APIError + if errors.As(err, &apiErr) { + if apiErr.StatusCode == http.StatusConflict { + if apiErr.Detail == databricks.AlreadyExists { + return nil, nil + } + } + } return nil, fmt.Errorf("databricks-connector: failed to update rule sets for group %s (%s): %w", principal.Id.Resource, groupId.Resource, err) } @@ -422,74 +446,101 @@ func (g *groupBuilder) Revoke(ctx context.Context, grant *v2.Grant) (annotations workspaceId = parentID } - if entitlement.Slug == groupMemberEntitlement { + membershipEntitlementID := ent.NewEntitlementID(entitlement.Resource, groupMemberEntitlement) + managerEntitlementID := ent.NewEntitlementID(entitlement.Resource, groupManagerEntitlement) + if entitlement.Id == membershipEntitlementID { group, _, err := g.client.GetGroup(ctx, workspaceId, groupId.Resource) if err != nil { return nil, fmt.Errorf("databricks-connector: failed to get group %s: %w", groupId.Resource, err) } + indexToDelete := -1 for i, member := range group.Members { if member.ID == principalId { - group.Members = slices.Delete(group.Members, i, i+1) + indexToDelete = i break } } + if indexToDelete != -1 { + group.Members = slices.Delete(group.Members, indexToDelete, indexToDelete+1) + } + _, err = g.client.UpdateGroup(ctx, workspaceId, group) if err != nil { return nil, fmt.Errorf("databricks-connector: failed to update group %s: %w", groupId.Resource, err) } - } else { - ruleSets, _, err := g.client.ListRuleSets(ctx, workspaceId, GroupsType, groupId.Resource) - if err != nil { - return nil, fmt.Errorf("databricks-connector: failed to list rule sets for group %s (%s): %w", principal.Id.Resource, groupId.Resource, err) - } + return nil, nil + } - if len(ruleSets) == 0 { - l.Info( - "databricks-connector: group already does not have the entitlement", - zap.String("principal_id", principal.Id.Resource), - zap.String("entitlement", entitlement.Slug), - ) + role := groupManagerEntitlement + if workspaceId == "" && entitlement.Id != managerEntitlementID { + return nil, fmt.Errorf("databricks-connector: only group manager entitlement is supported for role permissions for group %s", groupId.Resource) + } else if workspaceId != "" { + role = entitlement.Slug + } - return nil, nil - } + if role == "" { + return nil, fmt.Errorf("databricks-connector: role is empty") + } - principalId, err := preparePrincipalId(ctx, g.client, workspaceId, principal.Id.ResourceType, principal.Id.Resource) - if err != nil { - return nil, fmt.Errorf("databricks-connector: failed to prepare principal id: %w", err) - } + ruleSets, _, err := g.client.ListRuleSets(ctx, workspaceId, GroupsType, groupId.Resource) + if err != nil { + return nil, fmt.Errorf("databricks-connector: failed to list rule sets for group %s (%s): %w", principal.Id.Resource, groupId.Resource, err) + } - for i, ruleSet := range ruleSets { - if ruleSet.Role != entitlement.Slug { - continue - } + if len(ruleSets) == 0 { + l.Info( + "databricks-connector: group already does not have the entitlement", + zap.String("principal_id", principal.Id.Resource), + zap.String("entitlement", role), + ) - // check if it contains the principals and remove the principal to the rule set - if slices.Contains(ruleSet.Principals, principalId) { - // if there is only one principal, remove the whole rule set - if len(ruleSet.Principals) == 1 { - ruleSets = slices.Delete(ruleSets, i, i+1) - } else { - pI := slices.Index(ruleSet.Principals, principalId) - ruleSets[i].Principals = slices.Delete(ruleSet.Principals, pI, pI+1) - } - break - } + return nil, nil + } - l.Info( - "databricks-connector: group already does not have the entitlement", - zap.String("principal_id", principalId), - zap.String("entitlement", entitlement.Slug), - ) + principalId, prepareErr := preparePrincipalId(ctx, g.client, workspaceId, principal.Id.ResourceType, principal.Id.Resource) + if prepareErr != nil { + return nil, fmt.Errorf("databricks-connector: failed to prepare principal id: %w", prepareErr) + } - return nil, nil + for i, ruleSet := range ruleSets { + if ruleSet.Role != role { + continue } - _, err = g.client.UpdateRuleSets(ctx, workspaceId, GroupsType, groupId.Resource, ruleSets) - if err != nil { - return nil, fmt.Errorf("databricks-connector: failed to update rule sets for group %s (%s): %w", principal.Id.Resource, groupId.Resource, err) + // check if it contains the principals and remove the principal to the rule set + if slices.Contains(ruleSet.Principals, principalId) { + // if there is only one principal, remove the whole rule set + if len(ruleSet.Principals) == 1 { + ruleSets = slices.Delete(ruleSets, i, i+1) + } else { + pI := slices.Index(ruleSet.Principals, principalId) + ruleSets[i].Principals = slices.Delete(ruleSet.Principals, pI, pI+1) + } + break } + + l.Info( + "databricks-connector: group already does not have the entitlement", + zap.String("principal_id", principalId), + zap.String("entitlement", role), + ) + + return nil, nil + } + + _, err = g.client.UpdateRuleSets(ctx, workspaceId, GroupsType, groupId.Resource, ruleSets) + if err != nil { + var apiErr *databricks.APIError + if errors.As(err, &apiErr) { + if apiErr.StatusCode == http.StatusConflict { + if apiErr.Detail == databricks.AlreadyExists { + return nil, nil + } + } + } + return nil, fmt.Errorf("databricks-connector: failed to update rule sets for group %s (%s): %w", principal.Id.Resource, groupId.Resource, err) } return nil, nil diff --git a/pkg/databricks/request.go b/pkg/databricks/request.go index 927d07f6..8beb5d2c 100644 --- a/pkg/databricks/request.go +++ b/pkg/databricks/request.go @@ -10,8 +10,36 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/uhttp" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "go.uber.org/zap" ) +const ( + AlreadyExists = "AlreadyExists" +) + +// APIError represents an error response from the Databricks API. +type APIError struct { + StatusCode int + Detail string + Message string + Err error +} + +func (e *APIError) Error() string { + return fmt.Sprintf( + "unexpected status code %d: %s %s %v", + e.StatusCode, + e.Detail, + e.Message, + e.Err, + ) +} + +func (e *APIError) Unwrap() error { + return e.Err +} + func (c *Client) Get( ctx context.Context, urlAddress *url.URL, @@ -110,6 +138,8 @@ func (c *Client) doRequest( defer resp.Body.Close() if err == nil { + l := ctxzap.Extract(ctx) + l.Debug("do request response", zap.Any("response", response)) return ratelimitData, nil } @@ -121,11 +151,10 @@ func (c *Client) doRequest( return nil, err } - return ratelimitData, fmt.Errorf( - "unexpected status code %d: %s %s %w", - resp.StatusCode, - errorResponse.Detail, - errorResponse.Message, - err, - ) + return ratelimitData, &APIError{ + StatusCode: resp.StatusCode, + Detail: errorResponse.Detail, + Message: errorResponse.Message, + Err: err, + } }