pkg/connector/roles.go
274 linesgo
package connector

import (
	"context"
	"fmt"

	"example/baton-junction/pkg/client"

	v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2"
	"github.com/conductorone/baton-sdk/pkg/annotations"
	"github.com/conductorone/baton-sdk/pkg/types/entitlement"
	"github.com/conductorone/baton-sdk/pkg/types/grant"
	"github.com/conductorone/baton-sdk/pkg/types/resource"
	"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
	"go.uber.org/zap"
)

type roleBuilder struct {
	client *client.Client
}

func newRoleBuilder(c *client.Client) *roleBuilder {
	return &roleBuilder{client: c}
}

func (o *roleBuilder) ResourceType(_ context.Context) *v2.ResourceType {
	return roleResourceType
}

func (o *roleBuilder) List(
	ctx context.Context,
	_ *v2.ResourceId,
	attrs resource.SyncOpAttrs,
) ([]*v2.Resource, *resource.SyncOpResults, error) {
	l := ctxzap.Extract(ctx)

	bag, cursor, err := parsePageToken(attrs.PageToken.Token, &v2.ResourceId{ResourceType: roleResourceType.Id})
	if err != nil {
		return nil, nil, err
	}

	roles, nextCursor, err := o.client.GetRoles(ctx, cursor)
	if err != nil {
		return nil, nil, fmt.Errorf("baton-junction: failed to list roles: %w", err)
	}

	l.Debug("listed roles", zap.Int("count", len(roles)), zap.String("next_cursor", nextCursor))

	var resources []*v2.Resource
	for _, r := range roles {
		res, err := roleResource(&r)
		if err != nil {
			return nil, nil, err
		}
		resources = append(resources, res)
	}

	if err := bag.Next(nextCursor); err != nil {
		return nil, nil, fmt.Errorf("baton-junction: failed to advance pagination: %w", err)
	}

	nextPageToken, err := bag.Marshal()
	if err != nil {
		return nil, nil, fmt.Errorf("baton-junction: failed to marshal pagination bag: %w", err)
	}

	return resources, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil
}

func (o *roleBuilder) Entitlements(
	_ context.Context,
	res *v2.Resource,
	_ resource.SyncOpAttrs,
) ([]*v2.Entitlement, *resource.SyncOpResults, error) {
	e := entitlement.NewAssignmentEntitlement(
		res,
		memberEntitlement,
		entitlement.WithGrantableTo(userResourceType, groupResourceType, roleResourceType),
		entitlement.WithDisplayName(fmt.Sprintf("%s Member", res.DisplayName)),
		entitlement.WithDescription(fmt.Sprintf("Assigned the %s role", res.DisplayName)),
	)
	return []*v2.Entitlement{e}, nil, nil
}

func (o *roleBuilder) Grants(
	ctx context.Context,
	res *v2.Resource,
	attrs resource.SyncOpAttrs,
) ([]*v2.Grant, *resource.SyncOpResults, error) {
	l := ctxzap.Extract(ctx)

	bag, cursor, err := parsePageToken(attrs.PageToken.Token, &v2.ResourceId{
		ResourceType: roleResourceType.Id,
		Resource:     res.Id.Resource,
	})
	if err != nil {
		return nil, nil, err
	}

	members, nextCursor, err := o.client.GetRoleMembers(ctx, res.Id.Resource, cursor)
	if err != nil {
		return nil, nil, fmt.Errorf("baton-junction: failed to list role members: %w", err)
	}

	l.Debug("listed role members",
		zap.String("role_id", res.Id.Resource),
		zap.Int("count", len(members)),
	)

	var grants []*v2.Grant
	for _, m := range members {
		g := grant.NewGrant(
			res,
			memberEntitlement,
			&v2.ResourceId{
				ResourceType: userResourceType.Id,
				Resource:     m.UserID,
			},
		)
		grants = append(grants, g)
	}

	roleGroups, err := o.client.GetRoleGroups(ctx, res.Id.Resource)
	if err != nil {
		return nil, nil, fmt.Errorf("baton-junction: failed to list role group assignments: %w", err)
	}

	for _, ga := range roleGroups {
		g := grant.NewGrant(
			res,
			memberEntitlement,
			&v2.ResourceId{
				ResourceType: groupResourceType.Id,
				Resource:     ga.GroupID,
			},
			grant.WithAnnotation(&v2.GrantExpandable{
				EntitlementIds: []string{fmt.Sprintf("group:%s:member", ga.GroupID)},
			}),
		)
		grants = append(grants, g)
	}

	l.Debug("listed role group assignments",
		zap.String("role_id", res.Id.Resource),
		zap.Int("group_count", len(roleGroups)),
	)

	roleRoles, err := o.client.GetRoleRoles(ctx, res.Id.Resource)
	if err != nil {
		return nil, nil, fmt.Errorf("baton-junction: failed to list role-to-role assignments: %w", err)
	}

	for _, ra := range roleRoles {
		g := grant.NewGrant(
			res,
			memberEntitlement,
			&v2.ResourceId{
				ResourceType: roleResourceType.Id,
				Resource:     ra.RoleID,
			},
			grant.WithAnnotation(&v2.GrantExpandable{
				EntitlementIds: []string{fmt.Sprintf("role:%s:member", ra.RoleID)},
			}),
		)
		grants = append(grants, g)
	}

	l.Debug("listed role-to-role assignments",
		zap.String("role_id", res.Id.Resource),
		zap.Int("role_count", len(roleRoles)),
	)

	if err := bag.Next(nextCursor); err != nil {
		return nil, nil, fmt.Errorf("baton-junction: failed to advance pagination: %w", err)
	}

	nextPageToken, err := bag.Marshal()
	if err != nil {
		return nil, nil, fmt.Errorf("baton-junction: failed to marshal pagination bag: %w", err)
	}

	return grants, &resource.SyncOpResults{NextPageToken: nextPageToken}, nil
}

func (o *roleBuilder) Grant(
	ctx context.Context,
	principal *v2.Resource,
	en *v2.Entitlement,
) (annotations.Annotations, error) {
	l := ctxzap.Extract(ctx)

	roleID := en.Resource.Id.Resource
	userID := principal.Id.Resource

	l.Info("granting role membership",
		zap.String("role_id", roleID),
		zap.String("user_id", userID),
	)

	err := o.client.AddRoleMember(ctx, roleID, userID)
	if err != nil {
		return nil, fmt.Errorf("baton-junction: failed to grant role membership: %w", err)
	}

	return nil, nil
}

func (o *roleBuilder) Revoke(
	ctx context.Context,
	g *v2.Grant,
) (annotations.Annotations, error) {
	l := ctxzap.Extract(ctx)

	roleID := g.Entitlement.Resource.Id.Resource
	userID := g.Principal.Id.Resource

	l.Info("revoking role membership",
		zap.String("role_id", roleID),
		zap.String("user_id", userID),
	)

	if err := o.client.RemoveRoleMember(ctx, roleID, userID); err != nil {
		if isNotFound(err) {
			l.Debug("role membership already removed", zap.String("role_id", roleID), zap.String("user_id", userID))
			return nil, nil
		}
		return nil, fmt.Errorf("baton-junction: failed to revoke role membership: %w", err)
	}

	return nil, nil
}

func (o *roleBuilder) Get(
	ctx context.Context,
	resourceId *v2.ResourceId,
	_ *v2.ResourceId,
) (*v2.Resource, annotations.Annotations, error) {
	l := ctxzap.Extract(ctx)

	role, err := o.client.GetRole(ctx, resourceId.Resource)
	if err != nil {
		return nil, nil, fmt.Errorf("baton-junction: failed to get role %s: %w", resourceId.Resource, err)
	}

	l.Debug("fetched role", zap.String("role_id", resourceId.Resource))

	r, err := roleResource(role)
	if err != nil {
		return nil, nil, err
	}

	return r, nil, nil
}

func roleResource(r *client.Role) (*v2.Resource, error) {
	res, err := resource.NewRoleResource(
		r.Name,
		roleResourceType,
		r.ID,
		[]resource.RoleTraitOption{
			resource.WithRoleProfile(map[string]interface{}{
				"description": r.Description,
			}),
		},
		resource.WithAnnotation(&v2.RawId{Id: r.ID}),
		resource.WithDescription(r.Description),
	)
	if err != nil {
		return nil, fmt.Errorf("baton-junction: failed to build role resource %s: %w", r.ID, err)
	}

	return res, nil
}