From 302e11125880400b5de39154ba4000df946b43c0 Mon Sep 17 00:00:00 2001 From: Justin Gallardo Date: Wed, 30 Aug 2023 18:49:32 -0700 Subject: [PATCH] Make a new 'administration' app and give admin users a grant there instead of their own resource type --- pkg/connector/admin_users.go | 120 ------------------ pkg/connector/apps.go | 71 ++++++++++- pkg/connector/connector.go | 5 +- pkg/connector/helper.go | 38 ++++++ pkg/connector/role.go | 20 ++- .../pkg/types/entitlement/entitlement.go | 73 ----------- .../baton-sdk/pkg/types/grant/grant.go | 83 ------------ vendor/modules.txt | 2 - 8 files changed, 123 insertions(+), 289 deletions(-) delete mode 100644 pkg/connector/admin_users.go delete mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/types/entitlement/entitlement.go delete mode 100644 vendor/github.com/conductorone/baton-sdk/pkg/types/grant/grant.go diff --git a/pkg/connector/admin_users.go b/pkg/connector/admin_users.go deleted file mode 100644 index 459a4f45..00000000 --- a/pkg/connector/admin_users.go +++ /dev/null @@ -1,120 +0,0 @@ -package connector - -import ( - "context" - "fmt" - - "github.com/conductorone/baton-jumpcloud/pkg/jcapi1" - v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" - "github.com/conductorone/baton-sdk/pkg/annotations" - "github.com/conductorone/baton-sdk/pkg/pagination" - sdkEntitlements "github.com/conductorone/baton-sdk/pkg/types/entitlement" - sdkGrants "github.com/conductorone/baton-sdk/pkg/types/grant" - sdkResources "github.com/conductorone/baton-sdk/pkg/types/resource" -) - -const adminAppID = "jumpcloud-administration-app" - -type administrationAppBuilder struct { - resourceType *v2.ResourceType - client1 jc1Func - ext *ExtensionClient -} - -func (o *administrationAppBuilder) ResourceType(_ context.Context) *v2.ResourceType { - return o.resourceType -} - -func newAdministrationAppBuilder(jc1 jc1Func, ext *ExtensionClient) *administrationAppBuilder { - return &administrationAppBuilder{ - resourceType: resourceTypeAdministrationApp, - client1: jc1, - ext: ext, - } -} - -func (o *administrationAppBuilder) Entitlements(_ context.Context, resource *v2.Resource, _ *pagination.Token) ([]*v2.Entitlement, string, annotations.Annotations, error) { - var rv []*v2.Entitlement - - en := sdkEntitlements.NewAssignmentEntitlement(resource, "administrator", sdkEntitlements.WithGrantableTo(resourceTypeUser)) - rv = append(rv, en) - return rv, "", nil, nil -} - -func (o *administrationAppBuilder) lookForMatchingUser(ctx context.Context, email string) (*jcapi1.Systemuserreturn, error) { - ctx, client := o.client1(ctx) - list, resp, err := client.SystemusersApi.SystemusersList(ctx).Search(fmt.Sprintf("email:$eq:%s", email)).Execute() - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if len(list.Results) == 0 { - return nil, nil - } - - if len(list.Results) > 1 { - return nil, fmt.Errorf("found multiple users with email %s", email) - } - - return &list.Results[0], nil -} - -func (o *administrationAppBuilder) Grants(ctx context.Context, resource *v2.Resource, pt *pagination.Token) ([]*v2.Grant, string, annotations.Annotations, error) { - skip, b, err := unmarshalSkipToken(pt) - if err != nil { - return nil, "", nil, err - } - - users, resp, err := o.ext.UserList().Skip(skip).Execute(ctx) - if err != nil { - return nil, "", nil, err - } - defer resp.Body.Close() - - ctx, client := o.client1(ctx) - - var rv []*v2.Grant - for _, u := range users { - userEmail := u.GetEmail() - list, resp, err := client.SystemusersApi.SystemusersList(ctx).Filter(fmt.Sprintf("email:$eq:%s", userEmail)).Execute() - if err != nil { - return nil, "", nil, err - } - defer resp.Body.Close() - - if len(list.Results) != 1 { - return nil, "", nil, fmt.Errorf("found multiple users with email %s", userEmail) - } - - user := list.Results[0] - - rv = append(rv, sdkGrants.NewGrant( - resource, - "administrator", - &v2.ResourceId{ - ResourceType: resourceTypeUser.Id, - Resource: user.GetId(), - }, - )) - } - - pageToken, err := marshalSkipToken(len(users), skip, b) - if err != nil { - return nil, "", nil, err - } - - return rv, pageToken, nil, nil -} - -func (o *administrationAppBuilder) List(ctx context.Context, parentResourceID *v2.ResourceId, pt *pagination.Token) ([]*v2.Resource, string, annotations.Annotations, error) { - var rv []*v2.Resource - - adminApp, err := sdkResources.NewAppResource("JumpCloud Administration", resourceTypeAdministrationApp, adminAppID, nil) - if err != nil { - return nil, "", nil, err - } - rv = append(rv, adminApp) - - return rv, "", nil, nil -} diff --git a/pkg/connector/apps.go b/pkg/connector/apps.go index 2b415c52..04dad02f 100644 --- a/pkg/connector/apps.go +++ b/pkg/connector/apps.go @@ -12,17 +12,20 @@ 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" ) const ( apiUserType = "user" apiUserGroupType = "user_group" + adminAppID = "2UjI0eIRo77RGFwi2GKpAa0Til0" ) type appResourceType struct { resourceType *v2.ResourceType client1 jc1Func client2 jc2Func + ext *ExtensionClient } func (o *appResourceType) ResourceType(_ context.Context) *v2.ResourceType { @@ -34,6 +37,17 @@ func (o *appResourceType) List( resourceID *v2.ResourceId, token *pagination.Token, ) ([]*v2.Resource, string, annotations.Annotations, error) { + var rv []*v2.Resource + + // If this is the first call to List, we need to create the JumpCloud Administration app + if token.Token == "" { + adminApp, err := sdkResources.NewAppResource("JumpCloud Administration", resourceTypeApp, adminAppID, nil) + if err != nil { + return nil, "", nil, err + } + rv = append(rv, adminApp) + } + ctx, client := o.client1(ctx) skip, b, err := unmarshalSkipToken(token) @@ -47,7 +61,6 @@ func (o *appResourceType) List( } defer resp.Body.Close() - var rv []*v2.Resource for i := range apps.Results { ur, err := appResource(ctx, &apps.Results[i]) if err != nil { @@ -114,7 +127,7 @@ func appEntitlement(ctx context.Context, resource *v2.Resource) *v2.Entitlement Description: fmt.Sprintf("Assigned to %s app", resource.DisplayName), GrantableTo: []*v2.ResourceType{resourceTypeUser}, Purpose: v2.Entitlement_PURPOSE_VALUE_ASSIGNMENT, - Slug: resource.DisplayName, + Slug: "access", } } @@ -122,6 +135,53 @@ type graphRequest interface { Execute() ([]jcapi2.GraphConnection, *http.Response, error) } +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 { + return nil, "", nil, err + } + + appID := resource.Id.GetResource() + + users, resp, err := o.ext.UserList().Skip(skip).Execute(ctx) + if err != nil { + return nil, "", nil, err + } + defer resp.Body.Close() + + ctx, client := o.client1(ctx) + + var rv []*v2.Grant + for _, u := range users { + user, err := fetchUserByEmail(ctx, client, u.GetEmail()) + if err != nil { + return nil, "", nil, err + } + + ur := &v2.Resource{Id: &v2.ResourceId{ + ResourceType: resourceTypeUser.Id, + Resource: user.GetId(), + }, + } + + rv = append(rv, &v2.Grant{ + Id: fmtResourceGrant(resource.Id, ur.Id, appID), + Entitlement: &v2.Entitlement{ + Id: fmtResource(resource.Id, appID), + Resource: resource, + }, + Principal: ur, + }) + } + + pageToken, err := marshalSkipToken(len(users), skip, b) + if err != nil { + return nil, "", nil, err + } + + return rv, pageToken, nil, nil +} + func (o *appResourceType) Grants( ctx context.Context, resource *v2.Resource, @@ -129,6 +189,10 @@ func (o *appResourceType) Grants( ) ([]*v2.Grant, string, annotations.Annotations, error) { ctx, client := o.client2(ctx) + if resource.Id.Resource == adminAppID { + return o.adminGrants(ctx, resource, token) + } + b := pagination.Bag{} if token.Token == "" { b.Push(pagination.PageState{ @@ -231,10 +295,11 @@ func appGrant(resource *v2.Resource, resoureTypeId string, member *jcapi2.GraphC } } -func newAppBuilder(jc1 jc1Func, jc2 jc2Func) *appResourceType { +func newAppBuilder(jc1 jc1Func, jc2 jc2Func, ext *ExtensionClient) *appResourceType { return &appResourceType{ resourceType: resourceTypeApp, client1: jc1, client2: jc2, + ext: ext, } } diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 9e539d10..02251b32 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -135,8 +135,7 @@ func (s *Jumpcloud) ResourceSyncers(ctx context.Context) []connectorbuilder.Reso // 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), newGroupBuilder(s.client1, s.client2), - newAdministrationAppBuilder(s.client1, s.ext), - newRoleBuilder(s.ext), - newAppBuilder(s.client1, s.client2), + newRoleBuilder(s.client1, s.ext), + newAppBuilder(s.client1, s.client2, s.ext), } } diff --git a/pkg/connector/helper.go b/pkg/connector/helper.go index 3b7cae1e..e06cbf7a 100644 --- a/pkg/connector/helper.go +++ b/pkg/connector/helper.go @@ -1,11 +1,21 @@ package connector import ( + "context" + "errors" "fmt" + "sync" + "github.com/conductorone/baton-jumpcloud/pkg/jcapi1" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" ) +var ( + userCache sync.Map + errUserNotFoundForEmail = errors.New("user not found for email") + errMultipleUsersForEmail = errors.New("multiple users found for email") +) + func fmtResourceId(resourceTypeID string, id string) *v2.ResourceId { return &v2.ResourceId{ ResourceType: resourceTypeID, @@ -31,3 +41,31 @@ func fmtResourceGrant(resourceID *v2.ResourceId, principalId *v2.ResourceId, per permission, ) } + +func fetchUserByEmail(ctx context.Context, client *jcapi1.APIClient, email string) (*jcapi1.Systemuserreturn, error) { + if email == "" { + return nil, errors.New("email cannot be empty") + } + + if u, ok := userCache.Load(email); ok { + return u.(*jcapi1.Systemuserreturn), nil + } + + list, resp, err := client.SystemusersApi.SystemusersList(ctx).Filter(fmt.Sprintf("email:$eq:%s", email)).Execute() + if err != nil { + return nil, err + } + defer resp.Body.Close() + + if len(list.Results) == 0 { + return nil, errUserNotFoundForEmail + } + + if len(list.Results) != 1 { + return nil, errMultipleUsersForEmail + } + + userCache.Store(email, &list.Results[0]) + + return &list.Results[0], nil +} diff --git a/pkg/connector/role.go b/pkg/connector/role.go index c809cad7..17d69867 100644 --- a/pkg/connector/role.go +++ b/pkg/connector/role.go @@ -15,6 +15,7 @@ import ( type roleResourceType struct { resourceType *v2.ResourceType + client jc1Func ext *ExtensionClient allUsers []jcapi1.Userreturn @@ -128,21 +129,29 @@ func (o *roleResourceType) Grants( return nil, "", nil, err } + ctx, client := o.client(ctx) + var rv []*v2.Grant for i := range users { - user := &users[i] - roleID := fmtRoleNameAsID(user.GetRoleName()) + adminUser := &users[i] + roleID := fmtRoleNameAsID(adminUser.GetRoleName()) if resource.Id.Resource != roleID { continue } + + user, err := fetchUserByEmail(ctx, client, adminUser.GetEmail()) + if err != nil { + return nil, "", nil, err + } + rv = append(rv, roleGrant(resource, resourceTypeUser.Id, user)) } return rv, "", nil, nil } -func roleGrant(resource *v2.Resource, resoureTypeId string, user *jcapi1.Userreturn) *v2.Grant { +func roleGrant(resource *v2.Resource, resourceTypeID string, user *jcapi1.Systemuserreturn) *v2.Grant { roleID := resource.Id.GetResource() - ur := &v2.Resource{Id: &v2.ResourceId{ResourceType: resoureTypeId, Resource: user.GetId()}} + ur := &v2.Resource{Id: &v2.ResourceId{ResourceType: resourceTypeID, Resource: user.GetId()}} var annos annotations.Annotations @@ -157,9 +166,10 @@ func roleGrant(resource *v2.Resource, resoureTypeId string, user *jcapi1.Userret } } -func newRoleBuilder(ext *ExtensionClient) *roleResourceType { +func newRoleBuilder(client jc1Func, ext *ExtensionClient) *roleResourceType { return &roleResourceType{ resourceType: resourceTypeRole, + client: client, ext: ext, } } diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/types/entitlement/entitlement.go b/vendor/github.com/conductorone/baton-sdk/pkg/types/entitlement/entitlement.go deleted file mode 100644 index 69699b79..00000000 --- a/vendor/github.com/conductorone/baton-sdk/pkg/types/entitlement/entitlement.go +++ /dev/null @@ -1,73 +0,0 @@ -package entitlement - -import ( - "fmt" - - v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" - "github.com/conductorone/baton-sdk/pkg/annotations" - "google.golang.org/protobuf/proto" -) - -type EntitlementOption func(*v2.Entitlement) - -func WithAnnotation(msgs ...proto.Message) EntitlementOption { - return func(e *v2.Entitlement) { - annos := annotations.Annotations(e.Annotations) - for _, msg := range msgs { - annos.Append(msg) - } - e.Annotations = annos - } -} - -func WithGrantableTo(grantableTo ...*v2.ResourceType) EntitlementOption { - return func(g *v2.Entitlement) { - g.GrantableTo = grantableTo - } -} - -func WithDisplayName(displayName string) EntitlementOption { - return func(g *v2.Entitlement) { - g.DisplayName = displayName - } -} - -func WithDescription(description string) EntitlementOption { - return func(g *v2.Entitlement) { - g.Description = description - } -} - -func NewEntitlementID(resource *v2.Resource, permission string) string { - return fmt.Sprintf("%s:%s:%s", resource.Id.ResourceType, resource.Id.Resource, permission) -} - -func NewPermissionEntitlement(resource *v2.Resource, name string, entitlementOptions ...EntitlementOption) *v2.Entitlement { - entitlement := &v2.Entitlement{ - Id: NewEntitlementID(resource, name), - DisplayName: name, - Slug: name, - Purpose: v2.Entitlement_PURPOSE_VALUE_PERMISSION, - Resource: resource, - } - - for _, entitlementOption := range entitlementOptions { - entitlementOption(entitlement) - } - return entitlement -} - -func NewAssignmentEntitlement(resource *v2.Resource, name string, entitlementOptions ...EntitlementOption) *v2.Entitlement { - entitlement := &v2.Entitlement{ - Id: NewEntitlementID(resource, name), - DisplayName: name, - Slug: name, - Purpose: v2.Entitlement_PURPOSE_VALUE_ASSIGNMENT, - Resource: resource, - } - - for _, entitlementOption := range entitlementOptions { - entitlementOption(entitlement) - } - return entitlement -} diff --git a/vendor/github.com/conductorone/baton-sdk/pkg/types/grant/grant.go b/vendor/github.com/conductorone/baton-sdk/pkg/types/grant/grant.go deleted file mode 100644 index 6a55b482..00000000 --- a/vendor/github.com/conductorone/baton-sdk/pkg/types/grant/grant.go +++ /dev/null @@ -1,83 +0,0 @@ -package grant - -import ( - "fmt" - - v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" - "github.com/conductorone/baton-sdk/pkg/annotations" - eopt "github.com/conductorone/baton-sdk/pkg/types/entitlement" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/types/known/structpb" -) - -type GrantOption func(*v2.Grant) error - -type GrantPrincipal interface { - proto.Message - GetBatonResource() bool -} - -func WithGrantMetadata(metadata map[string]interface{}) GrantOption { - return func(g *v2.Grant) error { - md, err := structpb.NewStruct(metadata) - if err != nil { - return err - } - - annos := annotations.Annotations(g.Annotations) - annos.Update(md) - g.Annotations = annos - - return nil - } -} - -func WithAnnotation(msgs ...proto.Message) GrantOption { - return func(g *v2.Grant) error { - annos := annotations.Annotations(g.Annotations) - for _, msg := range msgs { - annos.Append(msg) - } - g.Annotations = annos - - return nil - } -} - -// NewGrant returns a new grant for the given entitlement on the resource for the provided principal resource ID. -func NewGrant(resource *v2.Resource, entitlementName string, principal GrantPrincipal, grantOptions ...GrantOption) *v2.Grant { - entitlement := &v2.Entitlement{ - Id: eopt.NewEntitlementID(resource, entitlementName), - Resource: resource, - } - - grant := &v2.Grant{ - Entitlement: entitlement, - } - - var resourceID *v2.ResourceId - switch p := principal.(type) { - case *v2.ResourceId: - resourceID = p - grant.Principal = &v2.Resource{Id: p} - case *v2.Resource: - grant.Principal = p - resourceID = p.Id - default: - panic("unexpected principal type") - } - - if resourceID == nil { - panic("principal resource must have a valid resource ID") - } - grant.Id = fmt.Sprintf("%s:%s:%s", entitlement.Id, resourceID.ResourceType, resourceID.Resource) - - for _, grantOption := range grantOptions { - err := grantOption(grant) - if err != nil { - panic(err) - } - } - - return grant -} diff --git a/vendor/modules.txt b/vendor/modules.txt index f56577c9..c6ba3fc7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -147,8 +147,6 @@ github.com/conductorone/baton-sdk/pkg/tasks github.com/conductorone/baton-sdk/pkg/tasks/c1api github.com/conductorone/baton-sdk/pkg/tasks/local github.com/conductorone/baton-sdk/pkg/types -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/conductorone/baton-sdk/pkg/ugrpc github.com/conductorone/baton-sdk/pkg/uhttp