Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 102 additions & 51 deletions pkg/connector/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -19,6 +22,7 @@ import (
)

const groupMemberEntitlement = "member"
const groupManagerEntitlement = "roles/group.manager"

type groupBuilder struct {
client *databricks.Client
Expand Down Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -342,18 +347,29 @@ 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
if slices.Contains(ruleSet.Principals, principalID) {
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
Expand All @@ -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)
}

Expand Down Expand Up @@ -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)
Copy link

@johnallers johnallers Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@btipling Is it ok in go to delete from and change the value of a slice that is being iterated over?

Copy link
Contributor Author

@btipling btipling Mar 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johnallers it's not ok, fixed in ca9270d

} 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
Expand Down
43 changes: 36 additions & 7 deletions pkg/databricks/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

Expand All @@ -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,
}
}
Loading