Skip to content

Commit

Permalink
Merge pull request #7 from ConductorOne/feat/provisioning
Browse files Browse the repository at this point in the history
Feat/provisioning
  • Loading branch information
ggreer authored Feb 15, 2024
2 parents f967527 + 148f7cc commit 4d16ce8
Show file tree
Hide file tree
Showing 4 changed files with 201 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,5 @@
# Dependency directories (remove the comment below to include it)
# vendor/
dist/

.env
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,23 @@ Check out [Baton](https://github.com/conductorone/baton) to learn more the proje
## Setup
1. While connected to the Retool database, create a new user for the connector to connect to Postgres as. Be sure to create and save the secure password for this user:
```postgresql
CREATE USER conductorone WITH PASSWORD 'secure-password';
CREATE USER baton WITH PASSWORD 'secure-password';
```
2. Grant your new role the privileges required by the connector for inspecting retool privileges.
```postgresql
GRANT SELECT ("id", "name", "organizationId", "universalAccess", "universalResourceAccess", "universalQueryLibraryAccess",
"userListAccess", "auditLogAccess", "unpublishedReleaseAccess") ON groups TO baton;
GRANT SELECT ("id", "accessLevel") ON group_pages TO baton;
GRANT SELECT ("id", "accessLevel") ON group_folder_defaults TO baton;
GRANT SELECT ("id", "accessLevel") on group_resources TO baton;
GRANT SELECT ("id", "accessLevel") on group_resource_folder_defaults TO baton;
GRANT SELECT, INSERT, UPDATE ("id", "accessLevel") ON group_pages TO baton;
GRANT SELECT, INSERT, UPDATE ("id", "accessLevel") ON group_folder_defaults TO baton;
GRANT SELECT, INSERT, UPDATE ("id", "accessLevel") on group_resources TO baton;
GRANT SELECT, INSERT, UPDATE ("id", "accessLevel") on group_resource_folder_defaults TO baton;
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 ("id", "userId", "groupId", "isAdmin") ON user_groups TO baton;
GRANT SELECT, INSERT, UPDATE, DELETE ("id", "userId", "groupId", "isAdmin", "updatedAt") ON user_groups TO baton;
GRANT USAGE, SELECT ON SEQUENCE user_groups_id_seq TO baton;
GRANT DELETE ON user_groups 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:
Expand Down
88 changes: 88 additions & 0 deletions pkg/client/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,91 @@ 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)
}

func (c *Client) RemoveGroupMember(ctx context.Context, groupID, userID int64) error {
l := ctxzap.Extract(ctx)
l.Debug("remove user from group", zap.Int64("user_id", userID))

args := []interface{}{groupID, userID}
sb := &strings.Builder{}
_, err := sb.WriteString(`DELETE FROM user_groups WHERE "groupId"=$1 AND "userId"=$2`)
if err != nil {
return err
}

if _, err := c.db.Exec(ctx, sb.String(), args...); err != nil {
return err
}

return nil
}
107 changes: 103 additions & 4 deletions pkg/connector/groups.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand All @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -147,6 +162,90 @@ 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) {
l := ctxzap.Extract(ctx)

entitlement := grant.Entitlement
principial := grant.Principal

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),
)
}

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
}

err = o.client.RemoveGroupMember(ctx, groupID, userID)
if err != nil {
l.Error(
err.Error(),
zap.String("principal_id", principial.Id.Resource),
zap.String("principal_type", principial.Id.ResourceType),
)

return nil, err
}

return nil, nil
}

func newGroupSyncer(ctx context.Context, c *client.Client) *groupSyncer {
return &groupSyncer{
resourceType: resourceTypeGroup,
Expand Down

0 comments on commit 4d16ce8

Please sign in to comment.