Skip to content
Open
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
1 change: 1 addition & 0 deletions pkg/connector/connector.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func (d *Connector) ResourceSyncers(ctx context.Context) []connectorbuilder.Reso
newUserBuilder(d.cloudServiceClient, d.accountCreationSettings),
newNamespaceBuilder(d.cloudServiceClient),
newAccountBuilder(d.cloudServiceClient),
newGroupBuilder(d.cloudServiceClient),
}
}

Expand Down
236 changes: 236 additions & 0 deletions pkg/connector/groups.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
package connector

import (
"context"
"fmt"
"strings"
"time"

v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
"github.com/conductorone/baton-sdk/pkg/annotations"
"github.com/conductorone/baton-sdk/pkg/pagination"
"github.com/conductorone/baton-sdk/pkg/types/entitlement"
rs "github.com/conductorone/baton-sdk/pkg/types/resource"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
cloudservicev1 "go.temporal.io/cloud-sdk/api/cloudservice/v1"
identityv1 "go.temporal.io/cloud-sdk/api/identity/v1"
"go.uber.org/zap"
)

const (
GroupMembershipMaxDuration = 10 * time.Minute
)

type groupBuilder struct {
client cloudservicev1.CloudServiceClient
}

func (o *groupBuilder) ResourceType(ctx context.Context) *v2.ResourceType {
return groupResourceType
}

func (o *groupBuilder) List(ctx context.Context, parentResourceID *v2.ResourceId, opts rs.SyncOpAttrs) ([]*v2.Resource, *rs.SyncOpResults, error) {
bag := &pagination.Bag{}
err := bag.Unmarshal(opts.PageToken.Token)
if err != nil {
return nil, nil, err
}

if bag.Current() == nil {
bag.Push(pagination.PageState{
ResourceTypeID: groupResourceType.Id,
})
}

req := &cloudservicev1.GetUserGroupsRequest{}
if bag.PageToken() != "" {
req.PageToken = bag.PageToken()
}

resp, err := o.client.GetUserGroups(ctx, req)
if err != nil {
return nil, nil, fmt.Errorf("baton-temporalcloud: failed to list groups: %w", err)
}

rv := make([]*v2.Resource, 0, len(resp.GetGroups()))
for _, group := range resp.GetGroups() {
groupResource, err := protoUserGroupToResource(group)
if err != nil {
return nil, nil, err
}
rv = append(rv, groupResource)
}

return paginate(rv, bag, resp.GetNextPageToken())
}

func (o *groupBuilder) Entitlements(_ context.Context, resource *v2.Resource, _ rs.SyncOpAttrs) ([]*v2.Entitlement, *rs.SyncOpResults, error) {
annos := &v2.V1Identifier{
Id: membershipEntitlementID(resource.GetId().GetResource()),
}
member := entitlement.NewAssignmentEntitlement(resource, roleMemberEntitlement,
entitlement.WithGrantableTo(userResourceType),
entitlement.WithDescription(fmt.Sprintf("Member of %s group in Temporal Cloud", resource.GetDisplayName())),
entitlement.WithDisplayName(fmt.Sprintf("%s Group Member", resource.GetDisplayName())),
entitlement.WithAnnotation(annos),
)
return []*v2.Entitlement{member}, nil, nil
}

func (o *groupBuilder) Grants(ctx context.Context, resource *v2.Resource, opts rs.SyncOpAttrs) ([]*v2.Grant, *rs.SyncOpResults, error) {
bag := &pagination.Bag{}
err := bag.Unmarshal(opts.PageToken.Token)
if err != nil {
return nil, nil, err
}
if bag.Current() == nil {
bag.Push(pagination.PageState{
ResourceTypeID: resource.GetId().GetResourceType(),
ResourceID: resource.GetId().GetResource(),
})
}

req := &cloudservicev1.GetUserGroupMembersRequest{
GroupId: resource.GetId().GetResource(),
}
if bag.PageToken() != "" {
req.PageToken = bag.PageToken()
}

resp, err := o.client.GetUserGroupMembers(ctx, req)
if err != nil {
return nil, nil, fmt.Errorf("baton-temporalcloud: failed to list group members: %w", err)
}

var rv []*v2.Grant
for _, member := range resp.GetMembers() {
userID := member.GetMemberId().GetUserId()
if userID == "" {
continue
}

userResp, err := o.client.GetUser(ctx, &cloudservicev1.GetUserRequest{UserId: userID})
if err != nil {
return nil, nil, fmt.Errorf("baton-temporalcloud: failed to get user %s for group grant: %w", userID, err)
}

g, err := createGroupMemberGrant(userResp.GetUser(), resource)
if err != nil {
return nil, nil, err
}
rv = append(rv, g)
}

return paginate(rv, bag, resp.GetNextPageToken())
}

func (o *groupBuilder) Grant(ctx context.Context, principal *v2.Resource, e *v2.Entitlement) ([]*v2.Grant, annotations.Annotations, error) {
userID := principal.GetId().GetResource()
userType := principal.GetId().GetResourceType()
groupResource := e.GetResource()
groupID := groupResource.GetId().GetResource()
groupType := groupResource.GetId().GetResourceType()
entitlementID := e.GetId()

req := &cloudservicev1.AddUserGroupMemberRequest{
GroupId: groupID,
MemberId: &identityv1.UserGroupMemberId{
MemberType: &identityv1.UserGroupMemberId_UserId{
UserId: userID,
},
},
}

resp, err := o.client.AddUserGroupMember(ctx, req)
if err != nil {
if strings.Contains(err.Error(), "already a member") || strings.Contains(err.Error(), "nothing to change") {
return nil, annotations.New(&v2.GrantAlreadyExists{}), nil
}
return nil, nil, fmt.Errorf("baton-temporalcloud: could not add user to group: %w", err)
}

retryDelay := resp.GetAsyncOperation().GetCheckDuration().AsDuration()
requestID := resp.GetAsyncOperation().GetId()
l := ctxzap.Extract(ctx).With(
zap.String("request_id", requestID),
zap.String("principal_id", userID),
zap.String("principal_type", userType),
zap.String("entitlement_id", entitlementID),
zap.String("entitlement_resource_id", groupID),
zap.String("entitlement_resource_type", groupType),
)
waitCtx, cancel := context.WithTimeout(ctx, GroupMembershipMaxDuration)
defer cancel()
err = awaitAsyncOperation(waitCtx, l, o.client, requestID, retryDelay)
if err != nil {
return nil, nil, fmt.Errorf("baton-temporalcloud: group membership addition failed: %w", err)
}

userResp, err := o.client.GetUser(ctx, &cloudservicev1.GetUserRequest{UserId: userID})
if err != nil {
return nil, nil, fmt.Errorf("baton-temporalcloud: failed to get user after adding to group: %w", err)
}

g, err := createGroupMemberGrant(userResp.GetUser(), groupResource)
if err != nil {
return nil, nil, err
}

annos := annotations.New()
annos.Append(&v2.RequestId{RequestId: requestID})

return []*v2.Grant{g}, annos, nil
}

func (o *groupBuilder) Revoke(ctx context.Context, g *v2.Grant) (annotations.Annotations, error) {
userID := g.GetPrincipal().GetId().GetResource()
userType := g.GetPrincipal().GetId().GetResourceType()
entitlementID := g.GetEntitlement().GetId()
groupResource := g.GetEntitlement().GetResource()
groupID := groupResource.GetId().GetResource()
groupType := groupResource.GetId().GetResourceType()

req := &cloudservicev1.RemoveUserGroupMemberRequest{
GroupId: groupID,
MemberId: &identityv1.UserGroupMemberId{
MemberType: &identityv1.UserGroupMemberId_UserId{
UserId: userID,
},
},
}

resp, err := o.client.RemoveUserGroupMember(ctx, req)
if err != nil {
if strings.Contains(err.Error(), "not a member") || strings.Contains(err.Error(), "nothing to change") {
annos := annotations.New(&v2.GrantAlreadyRevoked{})
return annos, fmt.Errorf("baton-temporalcloud: user is not a member of this group")
}
return nil, fmt.Errorf("baton-temporalcloud: could not remove user from group: %w", err)
}

retryDelay := resp.GetAsyncOperation().GetCheckDuration().AsDuration()
requestID := resp.GetAsyncOperation().GetId()
l := ctxzap.Extract(ctx).With(
zap.String("request_id", requestID),
zap.String("principal_id", userID),
zap.String("principal_type", userType),
zap.String("entitlement_id", entitlementID),
zap.String("entitlement_resource_id", groupID),
zap.String("entitlement_resource_type", groupType),
)
waitCtx, cancel := context.WithTimeout(ctx, GroupMembershipMaxDuration)
defer cancel()
err = awaitAsyncOperation(waitCtx, l, o.client, requestID, retryDelay)
if err != nil {
return nil, fmt.Errorf("baton-temporalcloud: group membership removal failed: %w", err)
}

annos := annotations.New()
annos.Append(&v2.RequestId{RequestId: requestID})

return annos, nil
}

func newGroupBuilder(client cloudservicev1.CloudServiceClient) *groupBuilder {
return &groupBuilder{client: client}
}
62 changes: 62 additions & 0 deletions pkg/connector/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,68 @@ func createAccountRoleGrant(user *identityv1.User, ar *v2.Resource, accountID st
return g, nil
}

func protoUserGroupToResource(proto *identityv1.UserGroup) (*v2.Resource, error) {
annos := &v2.V1Identifier{
Id: fmt.Sprintf("group:%s", proto.GetId()),
}

var profile map[string]interface{}
switch {
case proto.GetSpec().GetGoogleGroup() != nil:
profile = map[string]interface{}{
"group_type": "google",
"email_address": proto.GetSpec().GetGoogleGroup().GetEmailAddress(),
}
case proto.GetSpec().GetScimGroup() != nil:
profile = map[string]interface{}{
"group_type": "scim",
"idp_id": proto.GetSpec().GetScimGroup().GetIdpId(),
}
case proto.GetSpec().GetCloudGroup() != nil:
profile = map[string]interface{}{
"group_type": "cloud",
}
}

var groupTraitOpts []rs.GroupTraitOption
if profile != nil {
groupTraitOpts = append(groupTraitOpts, rs.WithGroupProfile(profile))
}

group, err := rs.NewGroupResource(proto.GetSpec().GetDisplayName(), groupResourceType, proto.GetId(), groupTraitOpts, rs.WithAnnotation(annos))
if err != nil {
return nil, err
}
return group, nil
}

func createGroupMemberGrant(user *identityv1.User, group *v2.Resource) (*v2.Grant, error) {
ur, err := protoUserToResource(user)
if err != nil {
return nil, err
}
annos := &v2.V1Identifier{
Id: grantID(membershipEntitlementID(group.GetId().GetResource()), ur.GetId().GetResource()),
}
g := grant.NewGrant(group, roleMemberEntitlement, ur.GetId(), grant.WithAnnotation(annos))
g.Principal = ur
return g, nil
}

func createNamespaceGroupGrant(group *identityv1.UserGroup, namespace *v2.Resource, permission identityv1.NamespaceAccess_Permission) (*v2.Grant, error) {
perm := namespacePermissionName(permission)
gr, err := protoUserGroupToResource(group)
if err != nil {
return nil, err
}
annos := &v2.V1Identifier{
Id: grantID(namespaceEntitlementID(namespace.GetId().GetResource(), perm), gr.GetId().GetResource()),
}
g := grant.NewGrant(namespace, perm, gr.GetId(), grant.WithAnnotation(annos))
g.Principal = gr
return g, nil
}

func awaitAsyncOperation(ctx context.Context, l *zap.Logger, client cloudservicev1.CloudServiceClient, requestID string, retryDelay time.Duration) error {
complete, err := checkAsyncOperation(ctx, client, requestID)
if err != nil {
Expand Down
Loading
Loading