From d367e56313c982ded506385624b5ddb087300a67 Mon Sep 17 00:00:00 2001 From: Vlada Nevyhosteny Date: Wed, 7 Feb 2024 13:35:00 +0100 Subject: [PATCH] feat: grant user group membership --- README.md | 3 +- pkg/client/group.go | 70 ++++++++++++++++++++++++++++++++++++++ pkg/connector/groups.go | 74 ++++++++++++++++++++++++++++++++++++++--- 3 files changed, 142 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 75385d14..366527d5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ GRANT SELECT ("id", "name") ON organizations TO baton; GRANT SELECT ("id", "name", "organizationId", "folderId", "photoUrl", "description", "deletedAt") ON pages TO baton; GRANT SELECT ("id", "name", "organizationId", "type", "displayName", "environmentId", "resourceFolderId") ON resources TO baton; GRANT SELECT ("id", "email", "firstName", "lastName", "profilePhotoUrl", "userName", "enabled", "lastLoggedIn", "organizationId") ON users TO baton; -GRANT SELECT, INSERT, UPDATE ("id", "userId", "groupId", "isAdmin") ON user_groups TO baton; +GRANT SELECT, INSERT, UPDATE ("id", "userId", "groupId", "isAdmin", "updatedAt") ON user_groups TO baton; +GRANT USAGE, SELECT ON SEQUENCE user_groups_id_seq TO baton; ``` 3. Run the connector with the proper connection string. For example if you created a new `baton` user with the password `baton`, it may look like this: diff --git a/pkg/client/group.go b/pkg/client/group.go index 98a5fc6d..2c104ef5 100644 --- a/pkg/client/group.go +++ b/pkg/client/group.go @@ -261,3 +261,73 @@ func (c *Client) ListGroupsForOrg(ctx context.Context, orgID int64, pager *Pager return ret, nextPageToken, nil } + +func (c *Client) GetGroupMember(ctx context.Context, groupID, userID int64) (*GroupMember, error) { + l := ctxzap.Extract(ctx) + l.Debug("getting group member", zap.Int64("group_id", groupID), zap.Int64("user_id", userID)) + + var args []interface{} + args = append(args, groupID) + args = append(args, userID) + + sb := &strings.Builder{} + _, err := sb.WriteString(`SELECT "id", "userId", "groupId", "isAdmin" FROM user_groups WHERE "groupId"=$1 AND "userId"=$2`) + if err != nil { + return nil, err + } + _, err = sb.WriteString("LIMIT 1") + if err != nil { + return nil, err + } + + var ret []*GroupMember + err = pgxscan.Select(ctx, c.db, &ret, sb.String(), args...) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + return nil, err + } + + if len(ret) == 0 { + return nil, nil + } + + return ret[0], nil +} + +func (c *Client) AddGroupMember(ctx context.Context, groupID, userID int64, isAdmin bool) error { + l := ctxzap.Extract(ctx) + l.Debug("add user to group", zap.Int64("user_id", userID)) + + args := []interface{}{groupID, userID, isAdmin} + sb := &strings.Builder{} + _, err := sb.WriteString(`INSERT INTO user_groups ("groupId", "userId", "isAdmin", "createdAt", "updatedAt") VALUES ($1, $2, $3, NOW(), NOW())`) + if err != nil { + return err + } + + if _, err := c.db.Exec(ctx, sb.String(), args...); err != nil { + return err + } + + return nil +} + +func (c *Client) UpdateGroupMember(ctx context.Context, groupID, userID int64, isAdmin bool) (*GroupMember, error) { + l := ctxzap.Extract(ctx) + l.Debug("update user in group", zap.Int64("user_id", userID)) + + args := []interface{}{groupID, userID, isAdmin} + sb := &strings.Builder{} + _, err := sb.WriteString(`UPDATE user_groups SET "isAdmin"=$3, "updatedAt" = NOW() WHERE "groupId"=$1 AND "userId"=$2`) + if err != nil { + return nil, err + } + + if _, err := c.db.Exec(ctx, sb.String(), args...); err != nil { + return nil, err + } + + return c.GetGroupMember(ctx, groupID, userID) +} diff --git a/pkg/connector/groups.go b/pkg/connector/groups.go index f704d98f..03dfc313 100644 --- a/pkg/connector/groups.go +++ b/pkg/connector/groups.go @@ -9,6 +9,8 @@ import ( "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/conductorone/baton-sdk/pkg/pagination" resources "github.com/conductorone/baton-sdk/pkg/types/resource" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "go.uber.org/zap" ) var resourceTypeGroup = &v2.ResourceType{ @@ -17,6 +19,19 @@ var resourceTypeGroup = &v2.ResourceType{ Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_GROUP}, } +const ( + adminEntitlementSlug = "admin" + memberEntitlementSlug = "member" +) + +func adminEntitlementId(groupID *v2.ResourceId) string { + return fmt.Sprintf("entitlement:%s:admin", groupID.Resource) +} + +func memberEntitlementId(groupID *v2.ResourceId) string { + return fmt.Sprintf("entitlement:%s:member", groupID.Resource) +} + type groupSyncer struct { resourceType *v2.ResourceType client *client.Client @@ -86,24 +101,24 @@ func (s *groupSyncer) Entitlements(ctx context.Context, resource *v2.Resource, p ret = append(ret, &v2.Entitlement{ Resource: resource, - Id: fmt.Sprintf("entitlement:%s:member", resource.Id.Resource), + Id: memberEntitlementId(resource.Id), DisplayName: fmt.Sprintf("%s Group Member", resource.DisplayName), Description: fmt.Sprintf("Is member of the %s organization", resource.DisplayName), GrantableTo: []*v2.ResourceType{resourceTypeUser}, Annotations: annos, Purpose: v2.Entitlement_PURPOSE_VALUE_ASSIGNMENT, - Slug: "member", + Slug: memberEntitlementSlug, }) ret = append(ret, &v2.Entitlement{ Resource: resource, - Id: fmt.Sprintf("entitlement:%s:admin", resource.Id.Resource), + Id: adminEntitlementId(resource.Id), DisplayName: fmt.Sprintf("%s Group Admin", resource.DisplayName), Description: fmt.Sprintf("Is admin of the %s group", resource.DisplayName), GrantableTo: []*v2.ResourceType{resourceTypeUser}, Annotations: annos, Purpose: v2.Entitlement_PURPOSE_VALUE_ASSIGNMENT, - Slug: "admin", + Slug: adminEntitlementSlug, }) return ret, "", nil, nil } @@ -147,6 +162,57 @@ func (s *groupSyncer) Grants(ctx context.Context, resource *v2.Resource, pToken return ret, nextPageToken, nil, nil } +func (o *groupSyncer) Grant(ctx context.Context, principial *v2.Resource, entitlement *v2.Entitlement) (annotations.Annotations, error) { + l := ctxzap.Extract(ctx) + + if principial.Id.ResourceType != resourceTypeUser.Id { + l.Warn( + "only users can be added to the group", + zap.String("principal_id", principial.Id.Resource), + zap.String("principal_type", principial.Id.ResourceType), + ) + } + + isAdminNewValue := entitlement.Slug == adminEntitlementSlug + groupID, err := parseObjectID(entitlement.Resource.Id.Resource) + if err != nil { + return nil, err + } + userID, err := parseObjectID(principial.Id.Resource) + if err != nil { + return nil, err + } + + member, err := o.client.GetGroupMember(ctx, groupID, userID) + if err != nil { + return nil, err + } + + if member == nil { + err = o.client.AddGroupMember(ctx, groupID, userID, isAdminNewValue) + if err != nil { + return nil, err + } + + return nil, nil + } + + if member.IsAdmin == isAdminNewValue { + return nil, nil + } + + _, err = o.client.UpdateGroupMember(ctx, groupID, userID, isAdminNewValue) + if err != nil { + return nil, err + } + + return nil, nil +} + +func (o *groupSyncer) Revoke(ctx context.Context, grant *v2.Grant) (annotations.Annotations, error) { + return nil, nil +} + func newGroupSyncer(ctx context.Context, c *client.Client) *groupSyncer { return &groupSyncer{ resourceType: resourceTypeGroup,