From 0606344457c152eb3c4b4125530609784e24b492 Mon Sep 17 00:00:00 2001 From: Justin Gallardo Date: Mon, 25 Sep 2023 23:37:41 -0700 Subject: [PATCH] Opportunistically use system users for admins, otherwise create a user for them. --- pkg/connector/apps.go | 26 ++++++--- pkg/connector/connector.go | 2 +- pkg/connector/pagination.go | 2 +- pkg/connector/role.go | 17 +++++- pkg/connector/users.go | 112 ++++++++++++++++++++++++++++++++---- 5 files changed, 135 insertions(+), 24 deletions(-) diff --git a/pkg/connector/apps.go b/pkg/connector/apps.go index 04dad02f..b6f1b0ee 100644 --- a/pkg/connector/apps.go +++ b/pkg/connector/apps.go @@ -135,6 +135,10 @@ type graphRequest interface { Execute() ([]jcapi2.GraphConnection, *http.Response, error) } +type appAdminPrincipal interface { + GetId() string +} + func (o *appResourceType) adminGrants(ctx context.Context, resource *v2.Resource, pt *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { skip, b, err := unmarshalSkipToken(pt) if err != nil { @@ -152,16 +156,24 @@ func (o *appResourceType) adminGrants(ctx context.Context, resource *v2.Resource ctx, client := o.client1(ctx) var rv []*v2.Grant - for _, u := range users { - user, err := fetchUserByEmail(ctx, client, u.GetEmail()) - if err != nil { + for i := range users { + adminUser := &users[i] + var adminPrincipal appAdminPrincipal = adminUser + + // If the user is a system user, we need to fetch the user by email to get the ID + systemUser, err := fetchUserByEmail(ctx, client, adminUser.GetEmail()) + if err != nil && !errors.Is(err, errUserNotFoundForEmail) { return nil, "", nil, err } + if systemUser != nil { + adminPrincipal = systemUser + } - ur := &v2.Resource{Id: &v2.ResourceId{ - ResourceType: resourceTypeUser.Id, - Resource: user.GetId(), - }, + ur := &v2.Resource{ + Id: &v2.ResourceId{ + ResourceType: resourceTypeUser.Id, + Resource: adminPrincipal.GetId(), + }, } rv = append(rv, &v2.Grant{ diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 51010bc1..3a1b297d 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -128,7 +128,7 @@ func (s *Jumpcloud) ResourceSyncers(ctx context.Context) []connectorbuilder.Reso // NOTE: Jumpcloud has 'two' types of Users, "admin users" and... uh, "normal users". // So, we put each in their own resource type. // https://support.jumpcloud.com/support/s/article/getting-started-jumpcloud-admin-accounts-vs-user-accounts-2019-08-21-10-36-47 - newUserBuilder(s.client1, s.client2), + newUserBuilder(s.client1, s.client2, s.ext), newGroupBuilder(s.client1, s.client2), newRoleBuilder(s.client1, s.ext), newAppBuilder(s.client1, s.client2, s.ext), diff --git a/pkg/connector/pagination.go b/pkg/connector/pagination.go index 43d8b622..bb2a72df 100644 --- a/pkg/connector/pagination.go +++ b/pkg/connector/pagination.go @@ -26,7 +26,7 @@ func unmarshalSkipToken(token *pagination.Token) (int32, *pagination.Bag, error) func marshalSkipToken(newObjects int, lastSkip int32, b *pagination.Bag) (string, error) { if newObjects == 0 { - return "", nil + return nextToken(b, "") } nextSkip := int64(newObjects) + int64(lastSkip) pageToken, err := nextToken(b, strconv.FormatInt(nextSkip, 10)) diff --git a/pkg/connector/role.go b/pkg/connector/role.go index 76770e19..f0e479eb 100644 --- a/pkg/connector/role.go +++ b/pkg/connector/role.go @@ -2,6 +2,7 @@ package connector import ( "context" + "errors" "fmt" "regexp" "strings" @@ -119,6 +120,10 @@ func (o *roleResourceType) cacheAllUsers(ctx context.Context) ([]jcapi1.Userretu return rv, nil } +type rolePrincipal interface { + GetId() string +} + func (o *roleResourceType) Grants( ctx context.Context, resource *v2.Resource, @@ -139,17 +144,23 @@ func (o *roleResourceType) Grants( continue } + var principal rolePrincipal = adminUser + user, err := fetchUserByEmail(ctx, client, adminUser.GetEmail()) - if err != nil { + if err != nil && !errors.Is(err, errUserNotFoundForEmail) { return nil, "", nil, err } - rv = append(rv, roleGrant(resource, resourceTypeUser.Id, user)) + if user != nil { + principal = user + } + + rv = append(rv, roleGrant(resource, resourceTypeUser.Id, principal)) } return rv, "", nil, nil } -func roleGrant(resource *v2.Resource, resourceTypeID string, user *jcapi1.Systemuserreturn) *v2.Grant { +func roleGrant(resource *v2.Resource, resourceTypeID string, user rolePrincipal) *v2.Grant { roleID := resource.Id.GetResource() ur := &v2.Resource{Id: &v2.ResourceId{ResourceType: resourceTypeID, Resource: user.GetId()}} diff --git a/pkg/connector/users.go b/pkg/connector/users.go index f0157643..fc81ae6a 100644 --- a/pkg/connector/users.go +++ b/pkg/connector/users.go @@ -2,6 +2,7 @@ package connector import ( "context" + "errors" "fmt" "strings" @@ -9,6 +10,7 @@ import ( v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/conductorone/baton-sdk/pkg/pagination" + sdkResources "github.com/conductorone/baton-sdk/pkg/types/resource" "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" "go.uber.org/zap" "google.golang.org/protobuf/types/known/structpb" @@ -19,18 +21,20 @@ type userResourceType struct { client1 jc1Func client2 jc2Func managers map[string]*jcapi1.Systemuserreturn + ext *ExtensionClient } func (o *userResourceType) ResourceType(_ context.Context) *v2.ResourceType { return o.resourceType } -func newUserBuilder(jc1 jc1Func, jc2 jc2Func) *userResourceType { +func newUserBuilder(jc1 jc1Func, jc2 jc2Func, ext *ExtensionClient) *userResourceType { return &userResourceType{ resourceType: resourceTypeUser, client1: jc1, client2: jc2, managers: make(map[string]*jcapi1.Systemuserreturn), + ext: ext, } } @@ -43,6 +47,8 @@ func (o *userResourceType) Grants(_ context.Context, _ *v2.Resource, _ *paginati } func (o *userResourceType) List(ctx context.Context, parentResourceID *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { + l := ctxzap.Extract(ctx) + ctx, client := o.client1(ctx) skip, b, err := unmarshalSkipToken(pt) @@ -50,27 +56,109 @@ func (o *userResourceType) List(ctx context.Context, parentResourceID *v2.Resour return nil, "", nil, err } - list, resp, err := client.SystemusersApi.SystemusersList(ctx).Skip(skip).Execute() - if err != nil { - return nil, "", nil, err + if b.Current() == nil { + // Push onto stack in reverse + b.Push(pagination.PageState{ + ResourceTypeID: "list-admin-users", + }) + b.Push(pagination.PageState{ + ResourceTypeID: "list-users", + }) } - defer resp.Body.Close() - var rv []*v2.Resource - for i := range list.Results { - ur, err := o.userResource(ctx, &list.Results[i]) + var pageToken string + switch b.Current().ResourceTypeID { + case "list-users": + list, resp, err := client.SystemusersApi.SystemusersList(ctx).Skip(skip).Execute() + if err != nil { + return nil, "", nil, err + } + defer resp.Body.Close() + + for i := range list.Results { + ur, err := o.userResource(ctx, &list.Results[i]) + if err != nil { + return nil, "", nil, err + } + rv = append(rv, ur) + } + pageToken, err = marshalSkipToken(len(list.Results), skip, b) + if err != nil { + return nil, "", nil, err + } + case "list-admin-users": + adminUsers, resp, err := o.ext.UserList().Skip(skip).Execute(ctx) if err != nil { return nil, "", nil, err } - rv = append(rv, ur) + defer resp.Body.Close() + + for i := range adminUsers { + adminEmail := adminUsers[i].GetEmail() + adminUser, err := o.adminUserResource(ctx, &adminUsers[i]) + if err != nil { + return nil, "", nil, err + } + + // Check if the admin user is also a system user, if so we'll use that user instead + systemUser, err := fetchUserByEmail(ctx, client, adminEmail) + if err != nil && !errors.Is(err, errUserNotFoundForEmail) { + return nil, "", nil, err + } + + if systemUser != nil { + continue + } + + l.Debug("admin user not found as system user, creating", zap.String("email", adminEmail)) + rv = append(rv, adminUser) + } + pageToken, err = marshalSkipToken(len(adminUsers), skip, b) + if err != nil { + return nil, "", nil, err + } + default: + return nil, "", nil, fmt.Errorf("baton-jumpcloud: unknown page state: %s", b.Current().ResourceTypeID) + } + + return rv, pageToken, nil, nil +} + +func (o *userResourceType) adminUserResource(ctx context.Context, user *jcapi1.Userreturn) (*v2.Resource, error) { + profile := map[string]interface{}{ + "id": user.GetId(), + } + + if user.HasOrganization() { + profile["organization"] = user.GetOrganization() + } + + userTraitOps := []sdkResources.UserTraitOption{ + sdkResources.WithUserProfile(profile), } - pageToken, err := marshalSkipToken(len(list.Results), skip, b) + status := v2.UserTrait_Status_STATUS_ENABLED + if user.GetSuspended() { + status = v2.UserTrait_Status_STATUS_DISABLED + } + userTraitOps = append(userTraitOps, sdkResources.WithStatus(status)) + + email := user.GetEmail() + if email != "" { + userTraitOps = append(userTraitOps, sdkResources.WithEmail(email, true)) + } + + r, err := sdkResources.NewUserResource( + fmt.Sprintf("%s %s", user.GetFirstname(), user.GetLastname()), + o.resourceType, + user.GetId(), + userTraitOps, + ) if err != nil { - return nil, "", nil, err + return nil, err } - return rv, pageToken, nil, nil + return r, nil } func (o *userResourceType) userResource(ctx context.Context, user *jcapi1.Systemuserreturn) (*v2.Resource, error) {