From 5a777d73606ddd02f88caac1316a81d359245457 Mon Sep 17 00:00:00 2001 From: Lauren Leach Date: Thu, 10 Oct 2024 12:50:50 -0700 Subject: [PATCH 1/7] wip okta+aws --- cmd/baton-okta/config.go | 14 +- cmd/baton-okta/main.go | 2 + go.mod | 2 +- pkg/connector/app.go | 13 + pkg/connector/aws_account.go | 443 +++++++++++++++++++++++++++++++++++ pkg/connector/connector.go | 201 ++++++++++++++-- pkg/connector/group.go | 293 ++++++++++++++++++++++- pkg/connector/helpers.go | 11 + 8 files changed, 947 insertions(+), 32 deletions(-) create mode 100644 pkg/connector/aws_account.go diff --git a/cmd/baton-okta/config.go b/cmd/baton-okta/config.go index 08e58a11..4ec8b4eb 100644 --- a/cmd/baton-okta/config.go +++ b/cmd/baton-okta/config.go @@ -15,15 +15,21 @@ var ( ciam = field.BoolField("ciam", field.WithDescription("Whether to run in CIAM mode or not. In CIAM mode, only roles and the users assigned to roles are synced")) ciamEmailDomains = field.StringSliceField("ciam-email-domains", field.WithDescription("The email domains to use for CIAM mode. Any users that don't have an email address with one of the provided domains will be ignored, unless explicitly granted a role")) - cache = field.BoolField("cache", field.WithDescription("Enable response cache"), field.WithDefaultValue(true)) - cacheTTI = field.IntField("cache-tti", field.WithDescription("Response cache cleanup interval in seconds"), field.WithDefaultValue(60)) - cacheTTL = field.IntField("cache-ttl", field.WithDescription("Response cache time to live in seconds"), field.WithDefaultValue(300)) + cache = field.BoolField("cache", field.WithDescription("Enable response cache"), field.WithDefaultValue(true)) + cacheTTI = field.IntField("cache-tti", field.WithDescription("Response cache cleanup interval in seconds"), field.WithDefaultValue(60)) + cacheTTL = field.IntField("cache-ttl", field.WithDescription("Response cache time to live in seconds"), field.WithDefaultValue(300)) + awsIdentityCenterMode = field.BoolField("aws-identity-center-mode", + field.WithDescription("Whether to run in AWS Identity center mode or not. In AWS mode, only samlRoles for groups and the users assigned to groups are synced")) + awsOktaAppId = field.StringField("aws-okta-app-id", field.WithDescription("The Okta app id for the AWS application")) ) var relationships = []field.SchemaFieldRelationship{ field.FieldsDependentOn([]field.SchemaField{oktaPrivateKeyId, oktaPrivateKey}, []field.SchemaField{oktaClientId}), + field.FieldsDependentOn([]field.SchemaField{awsOktaAppId}, []field.SchemaField{awsIdentityCenterMode}), field.FieldsMutuallyExclusive(apiToken, oktaClientId), field.FieldsAtLeastOneUsed(apiToken, oktaClientId), + field.FieldsMutuallyExclusive(ciam, awsIdentityCenterMode), + field.FieldsRequiredTogether(awsIdentityCenterMode, awsOktaAppId), } var configuration = field.NewConfiguration([]field.SchemaField{ @@ -39,4 +45,6 @@ var configuration = field.NewConfiguration([]field.SchemaField{ cache, cacheTTI, cacheTTL, + awsIdentityCenterMode, + awsOktaAppId, }, relationships...) diff --git a/cmd/baton-okta/main.go b/cmd/baton-okta/main.go index 17c3f537..2db438a4 100644 --- a/cmd/baton-okta/main.go +++ b/cmd/baton-okta/main.go @@ -51,6 +51,8 @@ func getConnector(ctx context.Context, v *viper.Viper) (types.ConnectorServer, e Cache: v.GetBool("cache"), CacheTTI: v.GetInt32("cache-tti"), CacheTTL: v.GetInt32("cache-ttl"), + AWSMode: v.GetBool("aws-identity-center-mode"), + AWSOktaAppId: v.GetString("aws-okta-app-id"), } cb, err := connector.New(ctx, ccfg) diff --git a/go.mod b/go.mod index 2632e60d..926dcad0 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.2 require ( github.com/conductorone/baton-sdk v0.2.25 + github.com/deckarep/golang-set/v2 v2.6.0 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 github.com/okta/okta-sdk-golang/v2 v2.20.0 github.com/spf13/viper v1.19.0 @@ -37,7 +38,6 @@ require ( github.com/aws/smithy-go v1.20.2 // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect - github.com/deckarep/golang-set/v2 v2.6.0 // indirect github.com/doug-martin/goqu/v9 v9.19.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/envoyproxy/protoc-gen-validate v1.0.4 // indirect diff --git a/pkg/connector/app.go b/pkg/connector/app.go index ef7ea936..cb03c1b2 100644 --- a/pkg/connector/app.go +++ b/pkg/connector/app.go @@ -304,6 +304,19 @@ func oktaAppsToOktaApplications(ctx context.Context, apps []okta.App) ([]*okta.A return applications, nil } +func oktaAppToOktaApplication(ctx context.Context, app okta.App) (*okta.Application, error) { + var oktaApp okta.Application + b, err := json.Marshal(app) + if err != nil { + return nil, fmt.Errorf("okta-connectorv2: error marshalling okta app: %w", err) + } + err = json.Unmarshal(b, &oktaApp) + if err != nil { + return nil, fmt.Errorf("okta-connectorv2: error unmarshalling okta app: %w", err) + } + return &oktaApp, nil +} + func appResource(ctx context.Context, app *okta.Application) (*v2.Resource, error) { appProfile := map[string]interface{}{ "status": app.Status, diff --git a/pkg/connector/aws_account.go b/pkg/connector/aws_account.go new file mode 100644 index 00000000..32951dd6 --- /dev/null +++ b/pkg/connector/aws_account.go @@ -0,0 +1,443 @@ +package connector + +import ( + "context" + "errors" + "fmt" + "log" + "net/http" + "regexp" + "strings" + + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" + "github.com/conductorone/baton-sdk/pkg/annotations" + "github.com/conductorone/baton-sdk/pkg/pagination" + sdkGrant "github.com/conductorone/baton-sdk/pkg/types/grant" + mapset "github.com/deckarep/golang-set/v2" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "go.uber.org/zap" + + sdkEntitlement "github.com/conductorone/baton-sdk/pkg/types/entitlement" + resource2 "github.com/conductorone/baton-sdk/pkg/types/resource" + "github.com/okta/okta-sdk-golang/v2/okta" +) + +type AWSRoles struct { + AWSEnvironmentEnum []string `json:"AWSEnvironmentEnum,omitempty"` + SamlIamRole []string `json:"SamlIamRole,omitempty"` + IamRole []string `json:"IamRole,omitempty"` +} + +type GroupMappingGrant struct { + OktaUserID string + Role string +} + +type accountResourceType struct { + resourceType *v2.ResourceType + domain string + apiToken string + client *okta.Client + awsConfig *awsConfig +} + +func (o *accountResourceType) ResourceType(_ context.Context) *v2.ResourceType { + return o.resourceType +} + +func accountBuilder(domain string, apiToken string, client *okta.Client, awsConfig *awsConfig) *accountResourceType { + return &accountResourceType{ + resourceType: resourceTypeAccount, + domain: domain, + apiToken: apiToken, + client: client, + awsConfig: awsConfig, + } +} + +func (o *accountResourceType) List( + ctx context.Context, + resourceID *v2.ResourceId, + token *pagination.Token, +) ([]*v2.Resource, string, annotations.Annotations, error) { + l := ctxzap.Extract(ctx) + l.Info("******* (o *accountResourceType) List", + zap.Any("resourceID", resourceID), + zap.Any("token", token), + zap.Any("o.awsConfig.UseGroupMapping", o.awsConfig.UseGroupMapping)) + + b := &pagination.Bag{} + err := b.Unmarshal(token.Token) + if err != nil { + return nil, "", nil, err + } + + if b.Current() == nil { + b.Push(pagination.PageState{ + ResourceTypeID: resourceTypeGroup.Id, + }) + page := b.PageToken() + mar, err := b.Marshal() + if err != nil { + return nil, "", nil, err + } + + l.Info("******* (o *accountResourceType) List pagination token nil:", + zap.Any("page", page), zap.Any("mar", mar)) + + return nil, mar, nil, nil + } else { + l.Info("******* (o *accountResourceType) List pagination token nil:", + zap.Any("token", token)) + } + + if !o.awsConfig.UseGroupMapping { + // TODO(lauren) move to new connector + l.Info("************ (o *accountResourceType) List", + zap.Any("identityProviderArnRegex", o.awsConfig.IdentityProviderArnRegex)) + + re, err := regexp.Compile(strings.ToLower(o.awsConfig.IdentityProviderArnRegex)) + if err != nil { + log.Fatal(err) + } + match := re.FindStringSubmatch(strings.ToLower(o.awsConfig.IdentityProviderArn)) + + l.Info("******* MATCH", zap.Any("match", match)) + // TODO(lauren) check if empty + // First element is full string + accountId := match[1] + + // TODO(lauren) what szhould name be? + resource, err := resource2.NewResource(accountId, o.resourceType, accountId) + if err != nil { + return nil, "", nil, err + } + return []*v2.Resource{resource}, "", nil, nil + + } else { + resources := make([]*v2.Resource, 0) + // TODO(lauren) check map is not nil + // TODO(lauren) how to paginate? + + o.awsConfig.accountRoleCache.Range(func(key, value interface{}) bool { + l.Info("************ (o *accountResourceType) List MAPPING", + zap.Any("makeaccountId", key)) + accountId, ok := key.(string) // Type assertion to convert interface{} to string + if ok { + resource, err := resource2.NewResource(accountId, o.resourceType, accountId) + //resource, err := resource2.NewResource(accountId, o.resourceType, accountId, resource2.WithParentResourceID(resourceID)) + // TODO(lauren) log + if err != nil { + // TODO(lauren) should we continu + return false + } + resources = append(resources, resource) + } + return true // continue iteration + }) + return resources, "", nil, nil + } +} + +func (o *accountResourceType) Entitlements( + ctx context.Context, + resource *v2.Resource, + token *pagination.Token, +) ([]*v2.Entitlement, string, annotations.Annotations, error) { + var rv []*v2.Entitlement + if !o.awsConfig.UseGroupMapping { + awsSamlRoles, _, err := o.listAWSSamlRoles(ctx) + if err != nil { + return nil, "", nil, err + } + for _, role := range awsSamlRoles.SamlIamRole { + rv = append(rv, samlRoleEntitlement(resource, role)) + } + } else { + accountRoleCachedSet, ok := o.awsConfig.accountRoleCache.Load(resource.Id.GetResource()) + if !ok { + // Should this error or just return empty? + return rv, "", nil, nil + } + accountRoleSet, ok := accountRoleCachedSet.(mapset.Set[string]) + for role := range accountRoleSet.Iterator().C { + rv = append(rv, samlRoleEntitlement(resource, role)) + } + } + + return rv, "", nil, nil +} + +func samlRoleEntitlement(resource *v2.Resource, role string) *v2.Entitlement { + return sdkEntitlement.NewAssignmentEntitlement(resource, role, + sdkEntitlement.WithDisplayName(fmt.Sprintf("%s Role Member", role)), + sdkEntitlement.WithDescription(fmt.Sprintf("Has the %s role in AWS Okta app", role)), + /*sdkEntitlement.WithAnnotation(&v2.V1Identifier{ + Id: V1MembershipEntitlementID(role.Type), + }),*/ + sdkEntitlement.WithGrantableTo(resourceTypeUser), + ) +} + +func (o *accountResourceType) Grants( + ctx context.Context, + resource *v2.Resource, + token *pagination.Token, +) ([]*v2.Grant, string, annotations.Annotations, error) { + + processedGroupGrants := o.awsConfig.processedGroupGrants.Load() + l := ctxzap.Extract(ctx) + l.Info("******************** (o *accountResourceType) Grants", + zap.Any("rersource", resource), + zap.Any("token", token), + zap.Any("proccessedGrants", processedGroupGrants), zap.Any("token", token)) + + b := &pagination.Bag{} + err := b.Unmarshal(token.Token) + if err != nil { + return nil, "", nil, err + } + + if !processedGroupGrants { + l.Info("********* DID NOT PROCESS GRANTS") + return nil, "", nil, nil + + /*if b.Current() == nil { + b.Push(pagination.PageState{ + ResourceTypeID: resourceTypeAccount.Id, + }) + }*/ + /*b.Push(pagination.PageState{ + ResourceTypeID: resourceTypeAccount.Id, + })*/ + /*b.Pop() + b.Push(pagination.PageState{ + ResourceTypeID: resourceTypeGroup.Id, + }) + + page := b.PageToken() + mar, err := b.Marshal() + if err != nil { + return nil, "", nil, err + } + + l.Info("********* DID NOT PROCESS GRANTS", zap.Any("page", page), zap.Any("mar", mar)) + + return nil, mar, nil, nil*/ + } else { + l.Info("********* PROCESS$ED GRANTS") + } + + /*if b.Current() == nil { + b.Push(pagination.PageState{ + ResourceTypeID: resourceTypeGroup.Id, + }) + + page := b.PageToken() + mar, err := b.Marshal() + if err != nil { + return nil, "", nil, err + } + + l.Info("********* GRANTS PAGE NU:L:", zap.Any("page", page), zap.Any("mar", mar)) + return nil, mar, nil, nil + } else { + fmt.Println("******* accountResourceType ******** NOT NULL") + }*/ + + var rv []*v2.Grant + + // TODO(lauren) ugh is this the right resource tpye?? + bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) + if err != nil { + l.Info("******************** errror parsing token") + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) + } + + l.Info("******************** (o *accountResourceType) Grants", zap.Any("token", token), + zap.Any("page", page)) + + useMapping := o.awsConfig.UseGroupMapping + //useMapping = false + if useMapping { + accountId := resource.Id.GetResource() + cachedAccountGrants, ok := o.awsConfig.accountGrantCache.Load(accountId) + if !ok { + l.Info("error getting account cached grants", zap.Any("accountId", accountId)) + return nil, "", nil, fmt.Errorf("error getting accounts grant cache '%s'", accountId) + } + l.Info("********* cachedAccountGrants", zap.Any("accountGrants", cachedAccountGrants)) + accountGrants, ok := cachedAccountGrants.(*[]*GroupMappingGrant) + + if !ok { + l.Info("error casting account grants", zap.Any("accountId", accountId)) + return nil, "", nil, fmt.Errorf("error casting account grants '%s'", accountId) + } + l.Info("********* o.awsConfig.UseGroupMapping", zap.Any("accountGrants", accountGrants)) + for _, groupGrant := range *accountGrants { + rv = append(rv, accountGrant(resource, groupGrant.Role, groupGrant.OktaUserID)) + } + // TODO(lauren) + } else { + + qp := queryParams(token.Size, page) + appUsers, respContext, err := listApplicationUsers(ctx, o.client, o.awsConfig.OktaAppId, token, qp) + // TODO(lauren) log error + for _, appUser := range appUsers { + + appUserSAMLRolesMap := mapset.NewSet[string]() + //var appUserSAMLRolesMap mapset.Set[string] + + if appUser.Scope == "USER" { + // TODO(lauren0 check nil + // TODO(lauren) check ok? + appUserProfile := appUser.Profile.(map[string]interface{}) + appUserSAMLRoles, err := getSAMLRoles(appUserProfile) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get saml roles for user '%s': %w", appUser.Id, err) + } + appUserSAMLRolesMap.Append(appUserSAMLRoles...) + l.Info("USER SCOPE", zap.Any("appUserSAMLRolesMap", appUserSAMLRolesMap)) + } + + if appUser.Scope == "GROUP" || o.awsConfig.JoinAllRoles { + appUserGroupCache, ok := o.awsConfig.appUserToGroup.Load(appUser.Id) + if !ok { + // TODO(lauren) load ap + l.Info("***** BREAKING") + break + // TODO(lauren) dont error in case not in groups + //return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get groups for user '%s'", appUser.Id) + } + appUserGroupsSet := appUserGroupCache.(mapset.Set[string]) + for group := range appUserGroupsSet.Iterator().C { + // TODO(lauren) this hould be empty roles + groupRoleCacheVal, ok := o.awsConfig.groupToSamlRoleCache.Load(group) + if !ok { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get roles for group '%s'", group) + } + groupRoleSet, ok := groupRoleCacheVal.(mapset.Set[string]) + if !ok { + return nil, "", nil, fmt.Errorf("error converting group '%s' role set", group) + } + // TODO(lauren) is this safe? + appUserSAMLRolesMap = appUserSAMLRolesMap.Union(groupRoleSet) + } + // TODO(lauren) have app user to group map + + } + + // TODO(lauren) use ToSlice instead? + for samlRole := range appUserSAMLRolesMap.Iterator().C { + rv = append(rv, accountGrant(resource, samlRole, appUser.Id)) + } + + // If the user scope is "GROUP", this means the user does not have a direct assignment + // We want to get the union of the group's samlRoles that the user is assigned to + // If joinAllRolesEnabled, we handle this in listApplicationGroups, so we only do this when joinAllRoles is not enabled + + } + /// TODO(lauren) need to cache + + nextPage, annos, err := parseResp(respContext.OktaResponse) + if err != nil { + return nil, "nil", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) + } + + err = bag.Next(nextPage) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) + } + + pageToken, err := bag.Marshal() + if err != nil { + return nil, "", nil, err + } + + return rv, pageToken, annos, nil + + } + return rv, "", nil, nil +} + +func accountGrant(resource *v2.Resource, samlRole string, oktaUserId string) *v2.Grant { + ur := &v2.Resource{Id: &v2.ResourceId{ResourceType: resourceTypeUser.Id, Resource: oktaUserId}} + + return sdkGrant.NewGrant(resource, samlRole, ur, sdkGrant.WithAnnotation(&v2.V1Identifier{ + Id: fmtGrantIdV1(V1MembershipEntitlementID(resource.Id.Resource), oktaUserId), + })) +} + +/* +Join all roles: This option enables merging all available roles assigned to a user as follows: + +For example, if a user is directly assigned Role1 and Role2 (user to app assignment), +and the user belongs to group GroupAWS with RoleA and RoleB assigned (group to app assignment), then: + +Join all roles OFF: Role1 and Role2 are available upon login to AWS +Join all roles ON: Role1, Role2, RoleA, and RoleB are available upon login to AWS +*/ + +func (o *accountResourceType) listAWSSamlRoles(ctx context.Context) (*AWSRoles, *responseContext, error) { + apiUrl := fmt.Sprintf("/api/v1/internal/apps/%s/types", o.awsConfig.OktaAppId) + + rq := o.client.CloneRequestExecutor() + + req, err := rq.WithAccept("application/json").WithContentType("application/json").NewRequest(http.MethodGet, apiUrl, nil) + if err != nil { + return nil, nil, err + } + + var awsRoles *AWSRoles + resp, err := rq.Do(ctx, req, &awsRoles) + if err != nil { + return nil, nil, err + } + respCtx, err := responseToContext(&pagination.Token{}, resp) + + return awsRoles, respCtx, nil +} + +func getSAMLRoles(profile map[string]interface{}) ([]string, error) { + samlRolesField := profile["samlRoles"] + if samlRolesField == nil { + return nil, nil + } + + samlRoles, ok := samlRolesField.([]interface{}) + if !ok { + return nil, errors.New("unexpected type in profile[\"samlRoles\"") + } + + ret := make([]string, len(samlRoles)) + for i, r := range samlRoles { + role, ok := r.(string) + if !ok { + return nil, errors.New("role was not string") + } + ret[i] = role + } + return ret, nil +} + +func getSAMLRolesMap(profile map[string]interface{}) (mapset.Set[string], error) { + ret := mapset.NewSet[string]() + samlRolesField := profile["samlRoles"] + if samlRolesField == nil { + return ret, nil + } + + samlRoles, ok := samlRolesField.([]interface{}) + if !ok { + return nil, errors.New("unexpected type in profile[\"samlRoles\"") + } + + for _, r := range samlRoles { + role, ok := r.(string) + if !ok { + return nil, errors.New("role was not string") + } + ret.Add(role) + } + return ret, nil +} diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 32a70124..37aca79c 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -5,21 +5,27 @@ import ( "fmt" "io" "net/http" + "strings" + "sync" + "sync/atomic" + v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" "github.com/conductorone/baton-sdk/pkg/connectorbuilder" + "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/conductorone/baton-sdk/pkg/uhttp" "github.com/okta/okta-sdk-golang/v2/okta" - - v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" ) +const awsApp = "amazon_aws" + type Okta struct { client *okta.Client domain string apiToken string syncInactiveApps bool ciamConfig *ciamConfig + awsConfig *awsConfig } type ciamConfig struct { @@ -27,6 +33,26 @@ type ciamConfig struct { EmailDomains []string } +// "groupFilter": "aws_(?{{accountid}}\\d+)_(?{{role}}[a-zA-Z0-9+=,.@\\-_]+)", +// arn:aws:iam::${accountid}:saml-provider/OKTA,arn:aws:iam::${accountid}:role/${role}" +type awsConfig struct { + Enabled bool + OktaAppId string + JoinAllRoles bool + IdentityProviderArn string + RoleRegex string + IdentityProviderArnRegex string + UseGroupMapping bool + GroupFilter string + appUserToGroup sync.Map // user id (key) to group set? + groupToSamlRoleCache sync.Map // group id to samlRoles set? + accountRoleCache sync.Map // key is account id, val is samlRole set + + accountGrantCache sync.Map // account -> slice of group grants + + processedGroupGrants atomic.Bool +} + type Config struct { Domain string ApiToken string @@ -40,6 +66,8 @@ type Config struct { Cache bool CacheTTI int32 CacheTTL int32 + AWSMode bool + AWSOktaAppId string } func v1AnnotationsForResourceType(resourceTypeID string, skipEntitlementsAndGrants bool) annotations.Annotations { @@ -80,6 +108,11 @@ var ( Traits: []v2.ResourceType_Trait{v2.ResourceType_TRAIT_APP}, Annotations: v1AnnotationsForResourceType("app", false), } + resourceTypeAccount = &v2.ResourceType{ + Id: "account", + DisplayName: "Account", + Annotations: v1AnnotationsForResourceType("account", false), + } defaultScopes = []string{ "okta.users.read", "okta.groups.read", @@ -92,6 +125,7 @@ var ( "okta.roles.manage", "okta.apps.manage", } + // TODO(lauren) use different scopes for aws mode? ) func (o *Okta) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceSyncer { @@ -101,22 +135,33 @@ func (o *Okta) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceS ciamBuilder(o.client), } } + if o.awsConfig.Enabled { + return []connectorbuilder.ResourceSyncer{ + userBuilder(o.domain, o.apiToken, o.client), + groupBuilder(o.domain, o.apiToken, o.client, o.awsConfig), + accountBuilder(o.domain, o.apiToken, o.client, o.awsConfig), + } + } return []connectorbuilder.ResourceSyncer{ roleBuilder(o.domain, o.apiToken, o.client), userBuilder(o.domain, o.apiToken, o.client), - groupBuilder(o.domain, o.apiToken, o.client), + groupBuilder(o.domain, o.apiToken, o.client, nil), appBuilder(o.domain, o.apiToken, o.syncInactiveApps, o.client), } } func (c *Okta) ListResourceTypes(ctx context.Context, request *v2.ResourceTypesServiceListResourceTypesRequest) (*v2.ResourceTypesServiceListResourceTypesResponse, error) { + resourceTypes := []*v2.ResourceType{ + resourceTypeUser, + resourceTypeGroup, + } + if c.awsConfig != nil && c.awsConfig.Enabled { + resourceTypes = append(resourceTypes, resourceTypeAccount) + } else { + resourceTypes = append(resourceTypes, resourceTypeRole, resourceTypeApp) + } return &v2.ResourceTypesServiceListResourceTypesResponse{ - List: []*v2.ResourceType{ - resourceTypeRole, - resourceTypeUser, - resourceTypeGroup, - resourceTypeApp, - }, + List: resourceTypes, }, nil } @@ -139,23 +184,47 @@ func (c *Okta) Metadata(ctx context.Context) (*v2.ConnectorMetadata, error) { } func (c *Okta) Validate(ctx context.Context) (annotations.Annotations, error) { - if c.apiToken != "" { - token := newPaginationToken(defaultLimit, "") + if c.apiToken == "" { + return nil, nil + } - _, respCtx, err := getOrgSettings(ctx, c.client, token) - if err != nil { - return nil, fmt.Errorf("okta-connector: verify failed to fetch org: %w", err) - } + token := newPaginationToken(defaultLimit, "") + + _, respCtx, err := getOrgSettings(ctx, c.client, token) + if err != nil { + return nil, fmt.Errorf("okta-connector: verify failed to fetch org: %w", err) + } - _, _, err = parseResp(respCtx.OktaResponse) + _, _, err = parseResp(respCtx.OktaResponse) + if err != nil { + return nil, fmt.Errorf("okta-connector: verify failed to parse response: %w", err) + } + + if respCtx.OktaResponse.StatusCode != http.StatusOK { + err := fmt.Errorf("okta-connector: verify returned non-200: '%d'", respCtx.OktaResponse.StatusCode) + return nil, err + } + + if c.awsConfig.Enabled { + if c.awsConfig.OktaAppId == "" { + return nil, fmt.Errorf("okta-connector: no app id set") + } + app, awsAppResp, err := c.client.Application.GetApplication(ctx, c.awsConfig.OktaAppId, okta.NewApplication(), nil) if err != nil { - return nil, fmt.Errorf("okta-connector: verify failed to parse response: %w", err) + return nil, fmt.Errorf("okta-connector: verify failed to fetch aws app: %w", err) } - - if respCtx.OktaResponse.StatusCode != http.StatusOK { - err := fmt.Errorf("okta-connector: verify returned non-200: '%d'", respCtx.OktaResponse.StatusCode) + awsAppRespCtx, err := responseToContext(&pagination.Token{}, awsAppResp) + if awsAppRespCtx.OktaResponse.StatusCode != http.StatusOK { + err := fmt.Errorf("okta-connector: verify returned non-200 for aws app: '%d'", awsAppRespCtx.OktaResponse.StatusCode) return nil, err } + oktaApp, err := oktaAppToOktaApplication(ctx, app) + if err != nil { + return nil, fmt.Errorf("okta-connector: verify failed to convert aws app: %w", err) + } + if oktaApp.Name != awsApp { + return nil, fmt.Errorf("okta-connector: okta app is not aws: %w", err) + } } return nil, nil @@ -209,6 +278,97 @@ func New(ctx context.Context, cfg *Config) (*Okta, error) { } } + awsConfig := &awsConfig{ + Enabled: cfg.AWSMode, + OktaAppId: cfg.AWSOktaAppId, + } + if cfg.AWSMode { + if cfg.AWSOktaAppId == "" { + return nil, fmt.Errorf("okta-connector: no app id set") + } + app, awsAppResp, err := oktaClient.Application.GetApplication(ctx, awsConfig.OktaAppId, okta.NewApplication(), nil) + if err != nil { + return nil, fmt.Errorf("okta-connector: verify failed to fetch aws app: %w", err) + } + // TODO(lauren) do we need to parseResp? + awsAppRespCtx, err := responseToContext(&pagination.Token{}, awsAppResp) + if awsAppRespCtx.OktaResponse.StatusCode != http.StatusOK { + err := fmt.Errorf("okta-connector: verify returned non-200 for aws app: '%d'", awsAppRespCtx.OktaResponse.StatusCode) + return nil, err + } + oktaApp, err := oktaAppToOktaApplication(ctx, app) + if err != nil { + return nil, fmt.Errorf("okta-connector: verify failed to convert aws app: %w", err) + } + if oktaApp.Name != awsApp { + return nil, fmt.Errorf("okta-connector: okta app is not aws: %w", err) + } + if oktaApp.Settings == nil { + return nil, fmt.Errorf("okta-connector: settings are not present on okta app") + } + if oktaApp.Settings.App == nil { + return nil, fmt.Errorf("okta-connector: app settings are not present on okta app") + } + appSettings := *oktaApp.Settings.App + useGroupMapping, ok := appSettings["useGroupMapping"] + if !ok { + return nil, fmt.Errorf("okta-connector: 'useGroupMapping' app setting is not present on okta app settings") + } + useGroupMappingBool, ok := useGroupMapping.(bool) + if !ok { + return nil, fmt.Errorf("okta-connector: 'useGroupMapping' app setting is not boolean") + } + groupFilter, ok := appSettings["groupFilter"] + if !ok { + return nil, fmt.Errorf("okta-connector: 'groupFilter' app setting is not present on okta app settings") + } + groupFilterString, ok := groupFilter.(string) + if !ok { + return nil, fmt.Errorf("okta-connector: 'groupFilter' app setting is not string") + } + joinAllRoles, ok := appSettings["joinAllRoles"] + if !ok { + return nil, fmt.Errorf("okta-connector: 'joinAllRoles' app setting is not present on okta app settings") + } + joinAllRolesBool, ok := joinAllRoles.(bool) + if !ok { + return nil, fmt.Errorf("okta-connector: 'joinAllRoles' app setting is not boolean") + } + identityProviderArn, ok := appSettings["identityProviderArn"] + if !ok { + return nil, fmt.Errorf("okta-connector: 'identityProviderArn' app setting is not present on okta app settings") + } + identityProviderArnString, ok := identityProviderArn.(string) + if !ok { + return nil, fmt.Errorf("okta-connector: 'identityProviderArn' app setting is not string") + } + roleValuePattern, ok := appSettings["roleValuePattern"] + if !ok { + return nil, fmt.Errorf("okta-connector: 'roleValuePattern' app setting is not present on okta app settings") + } + roleValuePatternString, ok := roleValuePattern.(string) + if !ok { + return nil, fmt.Errorf("okta-connector: 'roleValuePattern' app setting is not string") + } + + splitPattern := strings.Split(roleValuePatternString, ",") + accountPattern := splitPattern[0] + + identityProviderRegex := strings.Replace(accountPattern, "${accountid}", `(\d{12})`, 1) + groupFilterRegex := strings.Replace(groupFilterString, `(?{{accountid}}`, `(\d+`, 1) + groupFilterRegex = strings.Replace(groupFilterRegex, `(?{{role}}`, `([a-zA-Z0-9+=,.@\\-_]+`, 1) + + // Unescape the groupFilterRegex regex string + roleRegex := strings.Replace(groupFilterRegex, `\\`, `\`, -1) + + awsConfig.UseGroupMapping = useGroupMappingBool + awsConfig.GroupFilter = groupFilterString + awsConfig.JoinAllRoles = joinAllRolesBool + awsConfig.IdentityProviderArn = identityProviderArnString + awsConfig.IdentityProviderArnRegex = identityProviderRegex + awsConfig.RoleRegex = roleRegex + } + return &Okta{ client: oktaClient, domain: cfg.Domain, @@ -218,5 +378,6 @@ func New(ctx context.Context, cfg *Config) (*Okta, error) { Enabled: cfg.Ciam, EmailDomains: cfg.CiamEmailDomains, }, + awsConfig: awsConfig, }, nil } diff --git a/pkg/connector/group.go b/pkg/connector/group.go index 76b6ee08..252257c0 100644 --- a/pkg/connector/group.go +++ b/pkg/connector/group.go @@ -2,9 +2,12 @@ package connector import ( "context" + "encoding/json" "fmt" + "regexp" "time" + mapset "github.com/deckarep/golang-set/v2" "go.uber.org/zap" "google.golang.org/protobuf/types/known/structpb" @@ -25,6 +28,7 @@ type groupResourceType struct { domain string apiToken string client *okta.Client + awsConfig *awsConfig } func (o *groupResourceType) ResourceType(_ context.Context) *v2.ResourceType { @@ -36,19 +40,32 @@ func (o *groupResourceType) List( resourceID *v2.ResourceId, token *pagination.Token, ) ([]*v2.Resource, string, annotations.Annotations, error) { + l := ctxzap.Extract(ctx) + l.Info("************** (o *groupResourceType) List") + // TODO(lauren) why is this user? bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) if err != nil { return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) } - var rv []*v2.Resource - qp := queryParams(token.Size, page) - - groups, respCtx, err := o.listGroups(ctx, token, qp) - if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list users: %w", err) + var groups []*okta.Group + var respCtx *responseContext + if o.awsConfig != nil && o.awsConfig.Enabled { + qp := queryParamsExpand(token.Size, page, "group") + groups, respCtx, err = o.listApplicationGroups(ctx, token, qp) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list app groups: %w", err) + } + } else { + qp := queryParams(token.Size, page) + groups, respCtx, err = o.listGroups(ctx, token, qp) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list groups: %w", err) + } } + var rv []*v2.Resource + nextPage, annos, err := parseResp(respCtx.OktaResponse) if err != nil { return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) @@ -73,6 +90,30 @@ func (o *groupResourceType) List( return nil, "", nil, err } + if pageToken == "" && o.awsConfig != nil && o.awsConfig.Enabled { + l.Info("************ HERE") + o.awsConfig.accountRoleCache.Range(func(key, value interface{}) bool { + l.Info("************ (o *accountResourceType) List MAPPING", + zap.Any("makeaccountId", key)) + accountId, ok := key.(string) // Type assertion to convert interface{} to string + if ok { + bag.Push(pagination.PageState{ + ResourceTypeID: resourceTypeAccount.Id, + ResourceID: accountId, + }) + + } + return true // continue iteration + }) + + bag.Pop() + + bagStr, err := bag.Marshal() + l.Info("****************** page token", zap.Error(err), zap.Any("bag", bag), zap.Any("bagStr", bagStr)) + pageToken = bagStr + // TODO(lauren) pop? + } + return rv, pageToken, annos, nil } @@ -108,15 +149,26 @@ func (o *groupResourceType) Grants( resource *v2.Resource, token *pagination.Token, ) ([]*v2.Grant, string, annotations.Annotations, error) { + l := ctxzap.Extract(ctx) + l.Info("**************** (o *groupResourceType) Grants", + zap.Any("resource", resource), + zap.Any("token", token)) + + // TODO(lauren) why is this user bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) if err != nil { return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) } + l.Info("**************** parsed token", + zap.Any("bag", bag), + zap.Any("page", page)) var rv []*v2.Grant qp := queryParams(token.Size, page) - users, respCtx, err := o.listGroupUsers(ctx, resource.Id.GetResource(), token, qp) + groupID := resource.Id.GetResource() + + users, respCtx, err := o.listGroupUsers(ctx, groupID, token, qp) if err != nil { return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list group users: %w", err) } @@ -132,7 +184,78 @@ func (o *groupResourceType) Grants( } for _, user := range users { + l.Info("************* processing user", zap.Any("user", user)) rv = append(rv, groupGrant(resource, user)) + + var userGroupSet mapset.Set[string] + userGroupCachedSet, ok := o.awsConfig.appUserToGroup.Load(user.Id) + if !ok { + userGroupSet = mapset.NewSet[string](groupID) + } else { + userGroupSet, ok = userGroupCachedSet.(mapset.Set[string]) + if !ok { + return nil, "", nil, fmt.Errorf("error converting set") + } + userGroupSet.Add(groupID) + } + o.awsConfig.appUserToGroup.Store(user.Id, userGroupSet) + + if o.awsConfig.UseGroupMapping { + // Cache group grants + re, err := regexp.Compile(o.awsConfig.RoleRegex) + if err != nil { + return nil, "", nil, fmt.Errorf("error compiling regex '%s': %w", o.awsConfig.RoleRegex, err) + } + match := re.FindStringSubmatch(resource.DisplayName) + + l.Info("******* GRANT MATCH", zap.Any("esource.DisplayName", resource.DisplayName), + zap.Any("match", match)) + // TODO(lauren) check exact length + if len(match) != 3 { + l.Info("******* continuing ", zap.Any("match", match)) + // TODO(lauren) error or continue? + continue + } + + // First element is full string + accountId := match[1] + role := match[2] + cachedAccountGrants, ok := o.awsConfig.accountGrantCache.Load(accountId) + if !ok { + return nil, "", nil, fmt.Errorf("error getting accounts grant cache '%s'", accountId) + } + accountGrantsPtr, ok := cachedAccountGrants.(*[]*GroupMappingGrant) + if !ok { + return nil, "", nil, fmt.Errorf("error casting account grants '%s'", accountId) + } + accountGrants := *accountGrantsPtr + gmg := &GroupMappingGrant{ + OktaUserID: user.Id, + Role: role, + } + accountGrants = append(accountGrants, gmg) + l.Info("******* account grnats ", zap.Any("gmg", gmg), + zap.Any("accountGrants", accountGrants)) + o.awsConfig.accountGrantCache.Store(accountId, &accountGrants) + } + + /*if o.awsConfig.UseGroupMapping { + groupRoleCacheVal, ok := o.awsConfig.groupToSamlRoleCache.Load(groupID) + if !ok { + // TODO(lauren) error or continue? + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get roles for group '%s'", groupID) + } + groupRoleSet, ok := groupRoleCacheVal.(mapset.Set[string]) + if !ok { + return nil, "", nil, fmt.Errorf("error converting group '%s' role set", group) + } + // This should only be 1 value + //for role := range groupRoleSet.Iterator().C { + // + //} + + }*/ + } pageToken, err := bag.Marshal() @@ -140,17 +263,64 @@ func (o *groupResourceType) Grants( return nil, "", nil, err } + l.Info("******** GROUP TOKEN", zap.Any("pageToken", pageToken)) if pageToken == "" { etag := &v2.ETag{ Value: time.Now().UTC().Format(time.RFC3339Nano), } annos.Update(etag) + processedGrants := o.awsConfig.processedGroupGrants.Load() + + if o.awsConfig != nil && o.awsConfig.Enabled { + l.Info("******** GROUP TOKEN AWS ENABLED", zap.Any("processedGrants", processedGrants)) + if !processedGrants { + beforeMarshal, err := bag.Marshal() + l.Info("******** before", zap.Any("beforeMarshal", beforeMarshal)) + bag.Push(pagination.PageState{ + ResourceTypeID: resourceTypeAccount.Id, + }) + /*beforePopMarshal, err := bag.Marshal() + l.Info("******** before pop", zap.Any("beforePopMarshal", beforePopMarshal)) + bag.Pop() + l.Info("******** after pop", zap.Any("bag", bag))*/ + newTokenString, err := bag.Marshal() + if err != nil { + return nil, "", nil, err + } + l.Info("******** newTokenString", zap.Any("newTokenString", newTokenString)) + pageToken = newTokenString + } + o.awsConfig.processedGroupGrants.Store(true) + } + // TODO(check empty token) + /* + f pageToken == "" && o.awsConfig != nil && o.awsConfig.Enabled { + + o.awsConfig.accountRoleCache.Range(func(key, value interface{}) bool { + l.Info("************ (o *accountResourceType) List MAPPING", + zap.Any("makeaccountId", key)) + accountId, ok := key.(string) // Type assertion to convert interface{} to string + if ok { + bag.Push(pagination.PageState{ + ResourceTypeID: resourceTypeAccount.Id, + ResourceID: accountId, + }) + } + return true // continue iteration + }) + } + */ } return rv, pageToken, annos, nil } func (o *groupResourceType) listGroups(ctx context.Context, token *pagination.Token, qp *query.Params) ([]*okta.Group, *responseContext, error) { + //if o.awsConfig != nil && o.awsConfig.Enabled { + // return listApplicationGroups(ctx) + //} + // TODO(lauren) here + //ListApplicationGroupAssignments groups, resp, err := o.client.Group.ListGroups(ctx, qp) if err != nil { return nil, nil, fmt.Errorf("okta-connectorv2: failed to fetch groups from okta: %w", err) @@ -164,6 +334,107 @@ func (o *groupResourceType) listGroups(ctx context.Context, token *pagination.To return groups, reqCtx, nil } +func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pagination.Token, qp *query.Params) ([]*okta.Group, *responseContext, error) { + l := ctxzap.Extract(ctx) + appGroups, reqCtx, err := listApplicationGroupAssignments(ctx, o.client, o.awsConfig.OktaAppId, token, qp) + if err != nil { + return nil, nil, err + } + + groups := make([]*okta.Group, 0, len(appGroups)) + + for _, appGroup := range appGroups { + embedded := appGroup.Embedded + if embedded == nil { + return nil, nil, fmt.Errorf("app group '%s' embedded data was nil", appGroup.Id) + } + embeddedMap, ok := embedded.(map[string]interface{}) + if !ok { + return nil, nil, fmt.Errorf("app group embedded data was not a map for group with id '%s'", appGroup.Id) + } + embeddedGroup, ok := embeddedMap["group"] + if !ok { + return nil, nil, fmt.Errorf("embedded group data was nil for app group '%s'", appGroup.Id) + } + groupJSON, err := json.Marshal(embeddedGroup) + if err != nil { + return nil, nil, fmt.Errorf("error marshalling embedded group data for app group '%s': %w", appGroup.Id, err) + } + oktaGroup := &okta.Group{} + err = json.Unmarshal(groupJSON, &oktaGroup) + if err != nil { + return nil, nil, fmt.Errorf("error unmarshalling embedded group data for app group '%s': %w", appGroup.Id, err) + } + groups = append(groups, oktaGroup) + + appGroupProfile, ok := appGroup.Profile.(map[string]interface{}) + if !ok { + l.Info("*********** error converting app group profile", zap.Any("appGroup", appGroup.Id)) + return nil, nil, fmt.Errorf("error converting app group profile '%s'", appGroup.Id) + } + groupSAMLRoles, err := getSAMLRolesMap(appGroupProfile) + if err != nil { + l.Info("*********** error gettings saml rples") + return nil, nil, fmt.Errorf("error getting samlRoles from group profile '%s': %w", appGroup.Id, err) + } + + l.Info("*********** group", zap.Any("oktaGroup", oktaGroup), zap.Any("groupSAMLRoles", groupSAMLRoles)) + o.awsConfig.groupToSamlRoleCache.Store(oktaGroup.Id, groupSAMLRoles) + + if o.awsConfig.UseGroupMapping { + + // TODO(lauren) move gthis to new connector + re, err := regexp.Compile(o.awsConfig.RoleRegex) + if err != nil { + return nil, nil, fmt.Errorf("error compiling regex '%s': %w", o.awsConfig.RoleRegex, err) + } + match := re.FindStringSubmatch(oktaGroup.Profile.Name) + + l.Info("******* USER GROUP MATCH", zap.Any("match", match)) + // TODO(lauren) check exact length + if len(match) < 3 { + l.Info("******* continuing ", zap.Any("match", match)) + // TODO(lauren) error or continue? + continue + } + + // First element is full string + accountId := match[1] + role := match[2] + + var accountRoleSet mapset.Set[string] + accountRoleCachedSet, ok := o.awsConfig.accountRoleCache.Load(accountId) + if !ok { + //accountRoleSet = mapset.NewSet(role) + accountRoleSet = mapset.NewSet[string](role) + //accountRoleSet = mapset.NewSet[string](role) + l.Info("************ not ok", + zap.Any("accountRoleSet", accountRoleSet)) + } else { + accountRoleSet, ok = accountRoleCachedSet.(mapset.Set[string]) + if !ok { + l.Info("******** error converting set") + return nil, nil, fmt.Errorf("error converting set") + } + l.Info("************ OK", + zap.Any("accountRoleSet", accountRoleSet)) + accountRoleSet.Add(role) + } + o.awsConfig.accountRoleCache.Store(accountId, accountRoleSet) + + l.Info("************ INIT FOR ACCOUNT", + zap.Any("accountId", accountId)) + + accountGrants := make([]*GroupMappingGrant, 0) + // Initialize slice to cache account grants + o.awsConfig.accountGrantCache.LoadOrStore(accountId, &accountGrants) + + } + } + + return groups, reqCtx, nil +} + func (o *groupResourceType) listGroupUsers(ctx context.Context, groupID string, token *pagination.Token, qp *query.Params) ([]*okta.User, *responseContext, error) { users, resp, err := o.client.Group.ListGroupUsers(ctx, groupID, qp) if err != nil { @@ -189,6 +460,11 @@ func (o *groupResourceType) groupResource(ctx context.Context, group *okta.Group annos.Update(&v2.V1Identifier{ Id: fmtResourceIdV1(group.Id), }) + if o.awsConfig != nil && o.awsConfig.Enabled { + annos.Update(&v2.ChildResourceType{ + ResourceTypeId: resourceTypeAccount.Id, + }) + } etagMd, err := o.etagMd(group) if err != nil { @@ -292,11 +568,12 @@ func (g *groupResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annota return nil, nil } -func groupBuilder(domain string, apiToken string, client *okta.Client) *groupResourceType { +func groupBuilder(domain string, apiToken string, client *okta.Client, awsConfig *awsConfig) *groupResourceType { return &groupResourceType{ resourceType: resourceTypeGroup, domain: domain, apiToken: apiToken, client: client, + awsConfig: awsConfig, } } diff --git a/pkg/connector/helpers.go b/pkg/connector/helpers.go index 219106ae..1f170ea9 100644 --- a/pkg/connector/helpers.go +++ b/pkg/connector/helpers.go @@ -64,6 +64,17 @@ func queryParams(size int, after string) *query.Params { return query.NewQueryParams(query.WithLimit(int64(size)), query.WithAfter(after)) } +func queryParamsExpand(size int, after string, expand string) *query.Params { + if size == 0 || size > defaultLimit { + size = defaultLimit + } + if after == "" { + return query.NewQueryParams(query.WithLimit(int64(size)), query.WithExpand(expand)) + } + + return query.NewQueryParams(query.WithLimit(int64(size)), query.WithAfter(after), query.WithExpand(expand)) +} + func responseToContext(token *pagination.Token, resp *okta.Response) (*responseContext, error) { u, err := url.Parse(resp.NextPage) if err != nil { From 81fa502edc20c3eb112d4074b650ee957cc313d1 Mon Sep 17 00:00:00 2001 From: Lauren Leach Date: Thu, 10 Oct 2024 17:04:21 -0700 Subject: [PATCH 2/7] fetch user groups if group cache empty --- pkg/connector/aws_account.go | 288 +++++++++++------------------------ pkg/connector/connector.go | 14 +- pkg/connector/group.go | 147 +++--------------- 3 files changed, 114 insertions(+), 335 deletions(-) diff --git a/pkg/connector/aws_account.go b/pkg/connector/aws_account.go index 32951dd6..c8964e24 100644 --- a/pkg/connector/aws_account.go +++ b/pkg/connector/aws_account.go @@ -12,13 +12,10 @@ 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" - sdkGrant "github.com/conductorone/baton-sdk/pkg/types/grant" - mapset "github.com/deckarep/golang-set/v2" - "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" - "go.uber.org/zap" - sdkEntitlement "github.com/conductorone/baton-sdk/pkg/types/entitlement" + sdkGrant "github.com/conductorone/baton-sdk/pkg/types/grant" resource2 "github.com/conductorone/baton-sdk/pkg/types/resource" + mapset "github.com/deckarep/golang-set/v2" "github.com/okta/okta-sdk-golang/v2/okta" ) @@ -60,75 +57,39 @@ func (o *accountResourceType) List( resourceID *v2.ResourceId, token *pagination.Token, ) ([]*v2.Resource, string, annotations.Annotations, error) { - l := ctxzap.Extract(ctx) - l.Info("******* (o *accountResourceType) List", - zap.Any("resourceID", resourceID), - zap.Any("token", token), - zap.Any("o.awsConfig.UseGroupMapping", o.awsConfig.UseGroupMapping)) - - b := &pagination.Bag{} - err := b.Unmarshal(token.Token) - if err != nil { - return nil, "", nil, err - } - - if b.Current() == nil { - b.Push(pagination.PageState{ - ResourceTypeID: resourceTypeGroup.Id, - }) - page := b.PageToken() - mar, err := b.Marshal() - if err != nil { - return nil, "", nil, err - } - - l.Info("******* (o *accountResourceType) List pagination token nil:", - zap.Any("page", page), zap.Any("mar", mar)) - - return nil, mar, nil, nil - } else { - l.Info("******* (o *accountResourceType) List pagination token nil:", - zap.Any("token", token)) - } if !o.awsConfig.UseGroupMapping { // TODO(lauren) move to new connector - l.Info("************ (o *accountResourceType) List", - zap.Any("identityProviderArnRegex", o.awsConfig.IdentityProviderArnRegex)) - re, err := regexp.Compile(strings.ToLower(o.awsConfig.IdentityProviderArnRegex)) if err != nil { log.Fatal(err) } match := re.FindStringSubmatch(strings.ToLower(o.awsConfig.IdentityProviderArn)) - l.Info("******* MATCH", zap.Any("match", match)) - // TODO(lauren) check if empty // First element is full string + if len(match) != 2 { + if err != nil { + return nil, "", nil, fmt.Errorf("error getting aws account id") + } + } accountId := match[1] - // TODO(lauren) what szhould name be? + // TODO(lauren) what should name be? resource, err := resource2.NewResource(accountId, o.resourceType, accountId) if err != nil { return nil, "", nil, err } return []*v2.Resource{resource}, "", nil, nil - } else { resources := make([]*v2.Resource, 0) - // TODO(lauren) check map is not nil - // TODO(lauren) how to paginate? - + // TODO(lauren) check map is not empty/nil + // If it is, list groups and cache o.awsConfig.accountRoleCache.Range(func(key, value interface{}) bool { - l.Info("************ (o *accountResourceType) List MAPPING", - zap.Any("makeaccountId", key)) - accountId, ok := key.(string) // Type assertion to convert interface{} to string + accountId, ok := key.(string) if ok { resource, err := resource2.NewResource(accountId, o.resourceType, accountId) - //resource, err := resource2.NewResource(accountId, o.resourceType, accountId, resource2.WithParentResourceID(resourceID)) - // TODO(lauren) log if err != nil { - // TODO(lauren) should we continu + // TODO(lauren) should we continue return false } resources = append(resources, resource) @@ -184,180 +145,115 @@ func (o *accountResourceType) Grants( resource *v2.Resource, token *pagination.Token, ) ([]*v2.Grant, string, annotations.Annotations, error) { - - processedGroupGrants := o.awsConfig.processedGroupGrants.Load() - l := ctxzap.Extract(ctx) - l.Info("******************** (o *accountResourceType) Grants", - zap.Any("rersource", resource), - zap.Any("token", token), - zap.Any("proccessedGrants", processedGroupGrants), zap.Any("token", token)) - - b := &pagination.Bag{} - err := b.Unmarshal(token.Token) - if err != nil { - return nil, "", nil, err - } - - if !processedGroupGrants { - l.Info("********* DID NOT PROCESS GRANTS") - return nil, "", nil, nil - - /*if b.Current() == nil { - b.Push(pagination.PageState{ - ResourceTypeID: resourceTypeAccount.Id, - }) - }*/ - /*b.Push(pagination.PageState{ - ResourceTypeID: resourceTypeAccount.Id, - })*/ - /*b.Pop() - b.Push(pagination.PageState{ - ResourceTypeID: resourceTypeGroup.Id, - }) - - page := b.PageToken() - mar, err := b.Marshal() - if err != nil { - return nil, "", nil, err - } - - l.Info("********* DID NOT PROCESS GRANTS", zap.Any("page", page), zap.Any("mar", mar)) - - return nil, mar, nil, nil*/ - } else { - l.Info("********* PROCESS$ED GRANTS") - } - - /*if b.Current() == nil { - b.Push(pagination.PageState{ - ResourceTypeID: resourceTypeGroup.Id, - }) - - page := b.PageToken() - mar, err := b.Marshal() - if err != nil { - return nil, "", nil, err - } - - l.Info("********* GRANTS PAGE NU:L:", zap.Any("page", page), zap.Any("mar", mar)) - return nil, mar, nil, nil - } else { - fmt.Println("******* accountResourceType ******** NOT NULL") - }*/ - var rv []*v2.Grant - // TODO(lauren) ugh is this the right resource tpye?? + // TODO(lauren) what resource type should this be bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) if err != nil { - l.Info("******************** errror parsing token") return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) } - l.Info("******************** (o *accountResourceType) Grants", zap.Any("token", token), - zap.Any("page", page)) - useMapping := o.awsConfig.UseGroupMapping - //useMapping = false - if useMapping { - accountId := resource.Id.GetResource() - cachedAccountGrants, ok := o.awsConfig.accountGrantCache.Load(accountId) - if !ok { - l.Info("error getting account cached grants", zap.Any("accountId", accountId)) - return nil, "", nil, fmt.Errorf("error getting accounts grant cache '%s'", accountId) - } - l.Info("********* cachedAccountGrants", zap.Any("accountGrants", cachedAccountGrants)) - accountGrants, ok := cachedAccountGrants.(*[]*GroupMappingGrant) - if !ok { - l.Info("error casting account grants", zap.Any("accountId", accountId)) - return nil, "", nil, fmt.Errorf("error casting account grants '%s'", accountId) - } - l.Info("********* o.awsConfig.UseGroupMapping", zap.Any("accountGrants", accountGrants)) - for _, groupGrant := range *accountGrants { - rv = append(rv, accountGrant(resource, groupGrant.Role, groupGrant.OktaUserID)) - } - // TODO(lauren) - } else { + qp := queryParams(token.Size, page) + appUsers, respContext, err := listApplicationUsers(ctx, o.client, o.awsConfig.OktaAppId, token, qp) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: list application users %w", err) + } - qp := queryParams(token.Size, page) - appUsers, respContext, err := listApplicationUsers(ctx, o.client, o.awsConfig.OktaAppId, token, qp) - // TODO(lauren) log error - for _, appUser := range appUsers { + for _, appUser := range appUsers { + appUserSAMLRolesMap := mapset.NewSet[string]() - appUserSAMLRolesMap := mapset.NewSet[string]() - //var appUserSAMLRolesMap mapset.Set[string] + if appUser.Scope == "USER" { + if appUser.Profile == nil { + // TODO(lauren) continue or error? + continue + } + appUserProfile, ok := appUser.Profile.(map[string]interface{}) + if !ok { + // TODO(lauren) continue or error? + continue + } + appUserSAMLRoles, err := getSAMLRoles(appUserProfile) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get saml roles for user '%s': %w", appUser.Id, err) + } + appUserSAMLRolesMap.Append(appUserSAMLRoles...) + } - if appUser.Scope == "USER" { - // TODO(lauren0 check nil - // TODO(lauren) check ok? - appUserProfile := appUser.Profile.(map[string]interface{}) - appUserSAMLRoles, err := getSAMLRoles(appUserProfile) + // If the user scope is "GROUP", this means the user does not have a direct assignment + // We want to get the union of the group's samlRoles that the user is assigned to + // We also want a union of the group's samlRoles if useGroupMapping is enabled + if appUser.Scope == "GROUP" || o.awsConfig.JoinAllRoles || useMapping { + appUserGroupCache, ok := o.awsConfig.appUserToGroup.Load(appUser.Id) + var appUserGroupsSet mapset.Set[string] + if !ok { + // TODO(lauren) This endpoint doesn't paginate but + // I think we should check the resp code rate limit? + userGroups, _, err := listUsersGroupsClient(ctx, o.client, appUser.Id) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get saml roles for user '%s': %w", appUser.Id, err) + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to groups for user '%s': %w", appUser.Id, err) } - appUserSAMLRolesMap.Append(appUserSAMLRoles...) - l.Info("USER SCOPE", zap.Any("appUserSAMLRolesMap", appUserSAMLRolesMap)) - } - if appUser.Scope == "GROUP" || o.awsConfig.JoinAllRoles { - appUserGroupCache, ok := o.awsConfig.appUserToGroup.Load(appUser.Id) - if !ok { - // TODO(lauren) load ap - l.Info("***** BREAKING") - break - // TODO(lauren) dont error in case not in groups - //return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get groups for user '%s'", appUser.Id) - } - appUserGroupsSet := appUserGroupCache.(mapset.Set[string]) - for group := range appUserGroupsSet.Iterator().C { - // TODO(lauren) this hould be empty roles - groupRoleCacheVal, ok := o.awsConfig.groupToSamlRoleCache.Load(group) - if !ok { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get roles for group '%s'", group) + groupIDSFilter := mapset.NewSet[string]() + o.awsConfig.groupToSamlRoleCache.Range(func(key, value interface{}) bool { + groupID, ok := key.(string) + if ok { + // TODO(lauren) return false or just continue? + groupIDSFilter.Add(groupID) } - groupRoleSet, ok := groupRoleCacheVal.(mapset.Set[string]) - if !ok { - return nil, "", nil, fmt.Errorf("error converting group '%s' role set", group) + return true + }) + + filteredUserGroups := mapset.NewSet[string]() + for _, userGroup := range userGroups { + if groupIDSFilter.ContainsOne(userGroup.Id) { + filteredUserGroups.Add(userGroup.Id) } - // TODO(lauren) is this safe? - appUserSAMLRolesMap = appUserSAMLRolesMap.Union(groupRoleSet) } - // TODO(lauren) have app user to group map + o.awsConfig.appUserToGroup.Store(appUser.Id, filteredUserGroups) + appUserGroupsSet = filteredUserGroups + } else { + appUserGroupsSet = appUserGroupCache.(mapset.Set[string]) } - // TODO(lauren) use ToSlice instead? - for samlRole := range appUserSAMLRolesMap.Iterator().C { - rv = append(rv, accountGrant(resource, samlRole, appUser.Id)) + for group := range appUserGroupsSet.Iterator().C { + groupRoleCacheVal, ok := o.awsConfig.groupToSamlRoleCache.Load(group) + if !ok { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get roles for group '%s'", group) + } + groupRoleSet, ok := groupRoleCacheVal.(mapset.Set[string]) + if !ok { + return nil, "", nil, fmt.Errorf("error converting group '%s' role set", group) + } + // TODO(lauren) is this safe? + appUserSAMLRolesMap = appUserSAMLRolesMap.Union(groupRoleSet) } - - // If the user scope is "GROUP", this means the user does not have a direct assignment - // We want to get the union of the group's samlRoles that the user is assigned to - // If joinAllRolesEnabled, we handle this in listApplicationGroups, so we only do this when joinAllRoles is not enabled - } - /// TODO(lauren) need to cache - nextPage, annos, err := parseResp(respContext.OktaResponse) - if err != nil { - return nil, "nil", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) - } - - err = bag.Next(nextPage) - if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) + // TODO(lauren) use ToSlice instead? + for samlRole := range appUserSAMLRolesMap.Iterator().C { + rv = append(rv, accountGrant(resource, samlRole, appUser.Id)) } + } - pageToken, err := bag.Marshal() - if err != nil { - return nil, "", nil, err - } + nextPage, annos, err := parseResp(respContext.OktaResponse) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) + } - return rv, pageToken, annos, nil + err = bag.Next(nextPage) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) + } + pageToken, err := bag.Marshal() + if err != nil { + return nil, "", nil, err } - return rv, "", nil, nil + + return rv, pageToken, annos, nil } func accountGrant(resource *v2.Resource, samlRole string, oktaUserId string) *v2.Grant { diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 37aca79c..d031c58d 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -7,7 +7,6 @@ import ( "net/http" "strings" "sync" - "sync/atomic" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" @@ -43,14 +42,10 @@ type awsConfig struct { RoleRegex string IdentityProviderArnRegex string UseGroupMapping bool - GroupFilter string - appUserToGroup sync.Map // user id (key) to group set? - groupToSamlRoleCache sync.Map // group id to samlRoles set? - accountRoleCache sync.Map // key is account id, val is samlRole set - - accountGrantCache sync.Map // account -> slice of group grants - - processedGroupGrants atomic.Bool + appUserToGroup sync.Map // user id (key) to group mapset + groupToSamlRoleCache sync.Map // group id to samlRoles mapset + accountRoleCache sync.Map // key is account id, val is samlRole mapset + accountGrantCache sync.Map // account -> slice of group grants } type Config struct { @@ -362,7 +357,6 @@ func New(ctx context.Context, cfg *Config) (*Okta, error) { roleRegex := strings.Replace(groupFilterRegex, `\\`, `\`, -1) awsConfig.UseGroupMapping = useGroupMappingBool - awsConfig.GroupFilter = groupFilterString awsConfig.JoinAllRoles = joinAllRolesBool awsConfig.IdentityProviderArn = identityProviderArnString awsConfig.IdentityProviderArnRegex = identityProviderRegex diff --git a/pkg/connector/group.go b/pkg/connector/group.go index 252257c0..bfb5093e 100644 --- a/pkg/connector/group.go +++ b/pkg/connector/group.go @@ -40,8 +40,6 @@ func (o *groupResourceType) List( resourceID *v2.ResourceId, token *pagination.Token, ) ([]*v2.Resource, string, annotations.Annotations, error) { - l := ctxzap.Extract(ctx) - l.Info("************** (o *groupResourceType) List") // TODO(lauren) why is this user? bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) if err != nil { @@ -90,30 +88,6 @@ func (o *groupResourceType) List( return nil, "", nil, err } - if pageToken == "" && o.awsConfig != nil && o.awsConfig.Enabled { - l.Info("************ HERE") - o.awsConfig.accountRoleCache.Range(func(key, value interface{}) bool { - l.Info("************ (o *accountResourceType) List MAPPING", - zap.Any("makeaccountId", key)) - accountId, ok := key.(string) // Type assertion to convert interface{} to string - if ok { - bag.Push(pagination.PageState{ - ResourceTypeID: resourceTypeAccount.Id, - ResourceID: accountId, - }) - - } - return true // continue iteration - }) - - bag.Pop() - - bagStr, err := bag.Marshal() - l.Info("****************** page token", zap.Error(err), zap.Any("bag", bag), zap.Any("bagStr", bagStr)) - pageToken = bagStr - // TODO(lauren) pop? - } - return rv, pageToken, annos, nil } @@ -149,19 +123,11 @@ func (o *groupResourceType) Grants( resource *v2.Resource, token *pagination.Token, ) ([]*v2.Grant, string, annotations.Annotations, error) { - l := ctxzap.Extract(ctx) - l.Info("**************** (o *groupResourceType) Grants", - zap.Any("resource", resource), - zap.Any("token", token)) - // TODO(lauren) why is this user bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) if err != nil { return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) } - l.Info("**************** parsed token", - zap.Any("bag", bag), - zap.Any("page", page)) var rv []*v2.Grant qp := queryParams(token.Size, page) @@ -184,7 +150,6 @@ func (o *groupResourceType) Grants( } for _, user := range users { - l.Info("************* processing user", zap.Any("user", user)) rv = append(rv, groupGrant(resource, user)) var userGroupSet mapset.Set[string] @@ -208,11 +173,7 @@ func (o *groupResourceType) Grants( } match := re.FindStringSubmatch(resource.DisplayName) - l.Info("******* GRANT MATCH", zap.Any("esource.DisplayName", resource.DisplayName), - zap.Any("match", match)) - // TODO(lauren) check exact length if len(match) != 3 { - l.Info("******* continuing ", zap.Any("match", match)) // TODO(lauren) error or continue? continue } @@ -234,28 +195,8 @@ func (o *groupResourceType) Grants( Role: role, } accountGrants = append(accountGrants, gmg) - l.Info("******* account grnats ", zap.Any("gmg", gmg), - zap.Any("accountGrants", accountGrants)) o.awsConfig.accountGrantCache.Store(accountId, &accountGrants) } - - /*if o.awsConfig.UseGroupMapping { - groupRoleCacheVal, ok := o.awsConfig.groupToSamlRoleCache.Load(groupID) - if !ok { - // TODO(lauren) error or continue? - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get roles for group '%s'", groupID) - } - groupRoleSet, ok := groupRoleCacheVal.(mapset.Set[string]) - if !ok { - return nil, "", nil, fmt.Errorf("error converting group '%s' role set", group) - } - // This should only be 1 value - //for role := range groupRoleSet.Iterator().C { - // - //} - - }*/ - } pageToken, err := bag.Marshal() @@ -263,64 +204,17 @@ func (o *groupResourceType) Grants( return nil, "", nil, err } - l.Info("******** GROUP TOKEN", zap.Any("pageToken", pageToken)) if pageToken == "" { etag := &v2.ETag{ Value: time.Now().UTC().Format(time.RFC3339Nano), } annos.Update(etag) - processedGrants := o.awsConfig.processedGroupGrants.Load() - - if o.awsConfig != nil && o.awsConfig.Enabled { - l.Info("******** GROUP TOKEN AWS ENABLED", zap.Any("processedGrants", processedGrants)) - if !processedGrants { - beforeMarshal, err := bag.Marshal() - l.Info("******** before", zap.Any("beforeMarshal", beforeMarshal)) - bag.Push(pagination.PageState{ - ResourceTypeID: resourceTypeAccount.Id, - }) - /*beforePopMarshal, err := bag.Marshal() - l.Info("******** before pop", zap.Any("beforePopMarshal", beforePopMarshal)) - bag.Pop() - l.Info("******** after pop", zap.Any("bag", bag))*/ - newTokenString, err := bag.Marshal() - if err != nil { - return nil, "", nil, err - } - l.Info("******** newTokenString", zap.Any("newTokenString", newTokenString)) - pageToken = newTokenString - } - o.awsConfig.processedGroupGrants.Store(true) - } - // TODO(check empty token) - /* - f pageToken == "" && o.awsConfig != nil && o.awsConfig.Enabled { - - o.awsConfig.accountRoleCache.Range(func(key, value interface{}) bool { - l.Info("************ (o *accountResourceType) List MAPPING", - zap.Any("makeaccountId", key)) - accountId, ok := key.(string) // Type assertion to convert interface{} to string - if ok { - bag.Push(pagination.PageState{ - ResourceTypeID: resourceTypeAccount.Id, - ResourceID: accountId, - }) - } - return true // continue iteration - }) - } - */ } return rv, pageToken, annos, nil } func (o *groupResourceType) listGroups(ctx context.Context, token *pagination.Token, qp *query.Params) ([]*okta.Group, *responseContext, error) { - //if o.awsConfig != nil && o.awsConfig.Enabled { - // return listApplicationGroups(ctx) - //} - // TODO(lauren) here - //ListApplicationGroupAssignments groups, resp, err := o.client.Group.ListGroups(ctx, qp) if err != nil { return nil, nil, fmt.Errorf("okta-connectorv2: failed to fetch groups from okta: %w", err) @@ -369,32 +263,25 @@ func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pa appGroupProfile, ok := appGroup.Profile.(map[string]interface{}) if !ok { - l.Info("*********** error converting app group profile", zap.Any("appGroup", appGroup.Id)) + l.Info("error converting app group profile", zap.String("groupId", appGroup.Id), zap.Error(err)) return nil, nil, fmt.Errorf("error converting app group profile '%s'", appGroup.Id) } groupSAMLRoles, err := getSAMLRolesMap(appGroupProfile) if err != nil { - l.Info("*********** error gettings saml rples") + l.Info("error getting samlRoles from group profile", zap.String("groupId", appGroup.Id), zap.Error(err)) return nil, nil, fmt.Errorf("error getting samlRoles from group profile '%s': %w", appGroup.Id, err) } - l.Info("*********** group", zap.Any("oktaGroup", oktaGroup), zap.Any("groupSAMLRoles", groupSAMLRoles)) o.awsConfig.groupToSamlRoleCache.Store(oktaGroup.Id, groupSAMLRoles) if o.awsConfig.UseGroupMapping { - - // TODO(lauren) move gthis to new connector + // TODO(lauren) move this to new connector? re, err := regexp.Compile(o.awsConfig.RoleRegex) if err != nil { return nil, nil, fmt.Errorf("error compiling regex '%s': %w", o.awsConfig.RoleRegex, err) } match := re.FindStringSubmatch(oktaGroup.Profile.Name) - - l.Info("******* USER GROUP MATCH", zap.Any("match", match)) - // TODO(lauren) check exact length - if len(match) < 3 { - l.Info("******* continuing ", zap.Any("match", match)) - // TODO(lauren) error or continue? + if len(match) != 3 { continue } @@ -405,30 +292,18 @@ func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pa var accountRoleSet mapset.Set[string] accountRoleCachedSet, ok := o.awsConfig.accountRoleCache.Load(accountId) if !ok { - //accountRoleSet = mapset.NewSet(role) accountRoleSet = mapset.NewSet[string](role) - //accountRoleSet = mapset.NewSet[string](role) - l.Info("************ not ok", - zap.Any("accountRoleSet", accountRoleSet)) } else { accountRoleSet, ok = accountRoleCachedSet.(mapset.Set[string]) if !ok { - l.Info("******** error converting set") return nil, nil, fmt.Errorf("error converting set") } - l.Info("************ OK", - zap.Any("accountRoleSet", accountRoleSet)) accountRoleSet.Add(role) } o.awsConfig.accountRoleCache.Store(accountId, accountRoleSet) - - l.Info("************ INIT FOR ACCOUNT", - zap.Any("accountId", accountId)) - accountGrants := make([]*GroupMappingGrant, 0) // Initialize slice to cache account grants o.awsConfig.accountGrantCache.LoadOrStore(accountId, &accountGrants) - } } @@ -449,6 +324,20 @@ func (o *groupResourceType) listGroupUsers(ctx context.Context, groupID string, return users, reqCtx, nil } +func listUsersGroupsClient(ctx context.Context, client *okta.Client, userId string) ([]*okta.Group, *responseContext, error) { + users, resp, err := client.User.ListUserGroups(ctx, userId) + if err != nil { + return nil, nil, fmt.Errorf("okta-connectorv2: failed to fetch group users from okta: %w", err) + } + + reqCtx, err := responseToContext(&pagination.Token{}, resp) + if err != nil { + return nil, nil, err + } + + return users, reqCtx, nil +} + func (o *groupResourceType) groupResource(ctx context.Context, group *okta.Group) (*v2.Resource, error) { trait, err := o.groupTrait(ctx, group) if err != nil { From 3e9497b4d6b95011f0d2f1bf2e88eaf6fe00dd8d Mon Sep 17 00:00:00 2001 From: Lauren Leach Date: Thu, 10 Oct 2024 20:43:39 -0700 Subject: [PATCH 3/7] mutex/cache saml roles --- pkg/connector/aws_account.go | 70 +++++----- pkg/connector/connector.go | 258 ++++++++++++++++++++--------------- pkg/connector/group.go | 128 ++++++++--------- 3 files changed, 252 insertions(+), 204 deletions(-) diff --git a/pkg/connector/aws_account.go b/pkg/connector/aws_account.go index c8964e24..cdbf6d8c 100644 --- a/pkg/connector/aws_account.go +++ b/pkg/connector/aws_account.go @@ -16,7 +16,6 @@ import ( sdkGrant "github.com/conductorone/baton-sdk/pkg/types/grant" resource2 "github.com/conductorone/baton-sdk/pkg/types/resource" mapset "github.com/deckarep/golang-set/v2" - "github.com/okta/okta-sdk-golang/v2/okta" ) type AWSRoles struct { @@ -32,23 +31,17 @@ type GroupMappingGrant struct { type accountResourceType struct { resourceType *v2.ResourceType - domain string - apiToken string - client *okta.Client - awsConfig *awsConfig + connector *Okta } func (o *accountResourceType) ResourceType(_ context.Context) *v2.ResourceType { return o.resourceType } -func accountBuilder(domain string, apiToken string, client *okta.Client, awsConfig *awsConfig) *accountResourceType { +func accountBuilder(connector *Okta) *accountResourceType { return &accountResourceType{ resourceType: resourceTypeAccount, - domain: domain, - apiToken: apiToken, - client: client, - awsConfig: awsConfig, + connector: connector, } } @@ -57,14 +50,17 @@ func (o *accountResourceType) List( resourceID *v2.ResourceId, token *pagination.Token, ) ([]*v2.Resource, string, annotations.Annotations, error) { - - if !o.awsConfig.UseGroupMapping { + awsConfig, err := o.connector.getAWSApplicationConfig(ctx) + if err != nil { + return nil, "", nil, fmt.Errorf("error getting aws app settings config") + } + if !awsConfig.UseGroupMapping { // TODO(lauren) move to new connector - re, err := regexp.Compile(strings.ToLower(o.awsConfig.IdentityProviderArnRegex)) + re, err := regexp.Compile(strings.ToLower(awsConfig.IdentityProviderArnRegex)) if err != nil { log.Fatal(err) } - match := re.FindStringSubmatch(strings.ToLower(o.awsConfig.IdentityProviderArn)) + match := re.FindStringSubmatch(strings.ToLower(awsConfig.IdentityProviderArn)) // First element is full string if len(match) != 2 { @@ -84,7 +80,7 @@ func (o *accountResourceType) List( resources := make([]*v2.Resource, 0) // TODO(lauren) check map is not empty/nil // If it is, list groups and cache - o.awsConfig.accountRoleCache.Range(func(key, value interface{}) bool { + awsConfig.accountRoleCache.Range(func(key, value interface{}) bool { accountId, ok := key.(string) if ok { resource, err := resource2.NewResource(accountId, o.resourceType, accountId) @@ -105,23 +101,27 @@ func (o *accountResourceType) Entitlements( resource *v2.Resource, token *pagination.Token, ) ([]*v2.Entitlement, string, annotations.Annotations, error) { + awsConfig, err := o.connector.getAWSApplicationConfig(ctx) + if err != nil { + return nil, "", nil, fmt.Errorf("error getting aws app settings config") + } var rv []*v2.Entitlement - if !o.awsConfig.UseGroupMapping { - awsSamlRoles, _, err := o.listAWSSamlRoles(ctx) - if err != nil { - return nil, "", nil, err - } - for _, role := range awsSamlRoles.SamlIamRole { + if !awsConfig.UseGroupMapping { + for role := range awsConfig.appSamlRoles.Iterator().C { rv = append(rv, samlRoleEntitlement(resource, role)) } } else { - accountRoleCachedSet, ok := o.awsConfig.accountRoleCache.Load(resource.Id.GetResource()) + accountRoleCachedSet, ok := awsConfig.accountRoleCache.Load(resource.Id.GetResource()) if !ok { // Should this error or just return empty? return rv, "", nil, nil } accountRoleSet, ok := accountRoleCachedSet.(mapset.Set[string]) for role := range accountRoleSet.Iterator().C { + if !awsConfig.appSamlRoles.ContainsOne(role) { + // TODO(lauren) error or just ignore invalid role? + continue + } rv = append(rv, samlRoleEntitlement(resource, role)) } } @@ -145,6 +145,11 @@ func (o *accountResourceType) Grants( resource *v2.Resource, token *pagination.Token, ) ([]*v2.Grant, string, annotations.Annotations, error) { + awsConfig, err := o.connector.getAWSApplicationConfig(ctx) + if err != nil { + return nil, "", nil, fmt.Errorf("error getting aws app settings config") + } + var rv []*v2.Grant // TODO(lauren) what resource type should this be @@ -153,10 +158,10 @@ func (o *accountResourceType) Grants( return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) } - useMapping := o.awsConfig.UseGroupMapping + useMapping := awsConfig.UseGroupMapping qp := queryParams(token.Size, page) - appUsers, respContext, err := listApplicationUsers(ctx, o.client, o.awsConfig.OktaAppId, token, qp) + appUsers, respContext, err := listApplicationUsers(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) if err != nil { return nil, "", nil, fmt.Errorf("okta-connectorv2: list application users %w", err) } @@ -184,19 +189,19 @@ func (o *accountResourceType) Grants( // If the user scope is "GROUP", this means the user does not have a direct assignment // We want to get the union of the group's samlRoles that the user is assigned to // We also want a union of the group's samlRoles if useGroupMapping is enabled - if appUser.Scope == "GROUP" || o.awsConfig.JoinAllRoles || useMapping { - appUserGroupCache, ok := o.awsConfig.appUserToGroup.Load(appUser.Id) + if appUser.Scope == "GROUP" || awsConfig.JoinAllRoles || useMapping { + appUserGroupCache, ok := awsConfig.appUserToGroup.Load(appUser.Id) var appUserGroupsSet mapset.Set[string] if !ok { // TODO(lauren) This endpoint doesn't paginate but // I think we should check the resp code rate limit? - userGroups, _, err := listUsersGroupsClient(ctx, o.client, appUser.Id) + userGroups, _, err := listUsersGroupsClient(ctx, o.connector.client, appUser.Id) if err != nil { return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to groups for user '%s': %w", appUser.Id, err) } groupIDSFilter := mapset.NewSet[string]() - o.awsConfig.groupToSamlRoleCache.Range(func(key, value interface{}) bool { + awsConfig.groupToSamlRoleCache.Range(func(key, value interface{}) bool { groupID, ok := key.(string) if ok { // TODO(lauren) return false or just continue? @@ -212,14 +217,14 @@ func (o *accountResourceType) Grants( } } - o.awsConfig.appUserToGroup.Store(appUser.Id, filteredUserGroups) + awsConfig.appUserToGroup.Store(appUser.Id, filteredUserGroups) appUserGroupsSet = filteredUserGroups } else { appUserGroupsSet = appUserGroupCache.(mapset.Set[string]) } for group := range appUserGroupsSet.Iterator().C { - groupRoleCacheVal, ok := o.awsConfig.groupToSamlRoleCache.Load(group) + groupRoleCacheVal, ok := awsConfig.groupToSamlRoleCache.Load(group) if !ok { return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get roles for group '%s'", group) } @@ -275,9 +280,10 @@ Join all roles ON: Role1, Role2, RoleA, and RoleB are available upon login to AW */ func (o *accountResourceType) listAWSSamlRoles(ctx context.Context) (*AWSRoles, *responseContext, error) { - apiUrl := fmt.Sprintf("/api/v1/internal/apps/%s/types", o.awsConfig.OktaAppId) - rq := o.client.CloneRequestExecutor() + apiUrl := fmt.Sprintf("/api/v1/internal/apps/%s/types", o.connector.awsConfig.OktaAppId) + + rq := o.connector.client.CloneRequestExecutor() req, err := rq.WithAccept("application/json").WithContentType("application/json").NewRequest(http.MethodGet, apiUrl, nil) if err != nil { diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index d031c58d..6b4aa2b1 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -13,6 +13,7 @@ import ( "github.com/conductorone/baton-sdk/pkg/connectorbuilder" "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/conductorone/baton-sdk/pkg/uhttp" + mapset "github.com/deckarep/golang-set/v2" "github.com/okta/okta-sdk-golang/v2/okta" ) @@ -35,17 +36,23 @@ type ciamConfig struct { // "groupFilter": "aws_(?{{accountid}}\\d+)_(?{{role}}[a-zA-Z0-9+=,.@\\-_]+)", // arn:aws:iam::${accountid}:saml-provider/OKTA,arn:aws:iam::${accountid}:role/${role}" type awsConfig struct { - Enabled bool - OktaAppId string + Enabled bool + OktaAppId string + awsAppConfigCacheMutex sync.Mutex + oktaAWSAppSettings *oktaAWSAppSettings +} + +type oktaAWSAppSettings struct { JoinAllRoles bool IdentityProviderArn string RoleRegex string IdentityProviderArnRegex string UseGroupMapping bool - appUserToGroup sync.Map // user id (key) to group mapset - groupToSamlRoleCache sync.Map // group id to samlRoles mapset - accountRoleCache sync.Map // key is account id, val is samlRole mapset - accountGrantCache sync.Map // account -> slice of group grants + appUserToGroup sync.Map // user id (key) to group mapset + groupToSamlRoleCache sync.Map // group id to samlRoles mapset + accountRoleCache sync.Map // key is account id, val is samlRole mapset + accountGrantCache sync.Map // account -> slice of group grants + appSamlRoles mapset.Set[string] // TODO(lauren) Is this safe? } type Config struct { @@ -133,14 +140,14 @@ func (o *Okta) ResourceSyncers(ctx context.Context) []connectorbuilder.ResourceS if o.awsConfig.Enabled { return []connectorbuilder.ResourceSyncer{ userBuilder(o.domain, o.apiToken, o.client), - groupBuilder(o.domain, o.apiToken, o.client, o.awsConfig), - accountBuilder(o.domain, o.apiToken, o.client, o.awsConfig), + groupBuilder(o), + accountBuilder(o), } } return []connectorbuilder.ResourceSyncer{ roleBuilder(o.domain, o.apiToken, o.client), userBuilder(o.domain, o.apiToken, o.client), - groupBuilder(o.domain, o.apiToken, o.client, nil), + groupBuilder(o), appBuilder(o.domain, o.apiToken, o.syncInactiveApps, o.client), } } @@ -200,26 +207,11 @@ func (c *Okta) Validate(ctx context.Context) (annotations.Annotations, error) { return nil, err } - if c.awsConfig.Enabled { - if c.awsConfig.OktaAppId == "" { - return nil, fmt.Errorf("okta-connector: no app id set") - } - app, awsAppResp, err := c.client.Application.GetApplication(ctx, c.awsConfig.OktaAppId, okta.NewApplication(), nil) + if c.awsConfig != nil && c.awsConfig.Enabled { + _, err = c.getAWSApplicationConfig(ctx) if err != nil { - return nil, fmt.Errorf("okta-connector: verify failed to fetch aws app: %w", err) - } - awsAppRespCtx, err := responseToContext(&pagination.Token{}, awsAppResp) - if awsAppRespCtx.OktaResponse.StatusCode != http.StatusOK { - err := fmt.Errorf("okta-connector: verify returned non-200 for aws app: '%d'", awsAppRespCtx.OktaResponse.StatusCode) return nil, err } - oktaApp, err := oktaAppToOktaApplication(ctx, app) - if err != nil { - return nil, fmt.Errorf("okta-connector: verify failed to convert aws app: %w", err) - } - if oktaApp.Name != awsApp { - return nil, fmt.Errorf("okta-connector: okta app is not aws: %w", err) - } } return nil, nil @@ -277,91 +269,6 @@ func New(ctx context.Context, cfg *Config) (*Okta, error) { Enabled: cfg.AWSMode, OktaAppId: cfg.AWSOktaAppId, } - if cfg.AWSMode { - if cfg.AWSOktaAppId == "" { - return nil, fmt.Errorf("okta-connector: no app id set") - } - app, awsAppResp, err := oktaClient.Application.GetApplication(ctx, awsConfig.OktaAppId, okta.NewApplication(), nil) - if err != nil { - return nil, fmt.Errorf("okta-connector: verify failed to fetch aws app: %w", err) - } - // TODO(lauren) do we need to parseResp? - awsAppRespCtx, err := responseToContext(&pagination.Token{}, awsAppResp) - if awsAppRespCtx.OktaResponse.StatusCode != http.StatusOK { - err := fmt.Errorf("okta-connector: verify returned non-200 for aws app: '%d'", awsAppRespCtx.OktaResponse.StatusCode) - return nil, err - } - oktaApp, err := oktaAppToOktaApplication(ctx, app) - if err != nil { - return nil, fmt.Errorf("okta-connector: verify failed to convert aws app: %w", err) - } - if oktaApp.Name != awsApp { - return nil, fmt.Errorf("okta-connector: okta app is not aws: %w", err) - } - if oktaApp.Settings == nil { - return nil, fmt.Errorf("okta-connector: settings are not present on okta app") - } - if oktaApp.Settings.App == nil { - return nil, fmt.Errorf("okta-connector: app settings are not present on okta app") - } - appSettings := *oktaApp.Settings.App - useGroupMapping, ok := appSettings["useGroupMapping"] - if !ok { - return nil, fmt.Errorf("okta-connector: 'useGroupMapping' app setting is not present on okta app settings") - } - useGroupMappingBool, ok := useGroupMapping.(bool) - if !ok { - return nil, fmt.Errorf("okta-connector: 'useGroupMapping' app setting is not boolean") - } - groupFilter, ok := appSettings["groupFilter"] - if !ok { - return nil, fmt.Errorf("okta-connector: 'groupFilter' app setting is not present on okta app settings") - } - groupFilterString, ok := groupFilter.(string) - if !ok { - return nil, fmt.Errorf("okta-connector: 'groupFilter' app setting is not string") - } - joinAllRoles, ok := appSettings["joinAllRoles"] - if !ok { - return nil, fmt.Errorf("okta-connector: 'joinAllRoles' app setting is not present on okta app settings") - } - joinAllRolesBool, ok := joinAllRoles.(bool) - if !ok { - return nil, fmt.Errorf("okta-connector: 'joinAllRoles' app setting is not boolean") - } - identityProviderArn, ok := appSettings["identityProviderArn"] - if !ok { - return nil, fmt.Errorf("okta-connector: 'identityProviderArn' app setting is not present on okta app settings") - } - identityProviderArnString, ok := identityProviderArn.(string) - if !ok { - return nil, fmt.Errorf("okta-connector: 'identityProviderArn' app setting is not string") - } - roleValuePattern, ok := appSettings["roleValuePattern"] - if !ok { - return nil, fmt.Errorf("okta-connector: 'roleValuePattern' app setting is not present on okta app settings") - } - roleValuePatternString, ok := roleValuePattern.(string) - if !ok { - return nil, fmt.Errorf("okta-connector: 'roleValuePattern' app setting is not string") - } - - splitPattern := strings.Split(roleValuePatternString, ",") - accountPattern := splitPattern[0] - - identityProviderRegex := strings.Replace(accountPattern, "${accountid}", `(\d{12})`, 1) - groupFilterRegex := strings.Replace(groupFilterString, `(?{{accountid}}`, `(\d+`, 1) - groupFilterRegex = strings.Replace(groupFilterRegex, `(?{{role}}`, `([a-zA-Z0-9+=,.@\\-_]+`, 1) - - // Unescape the groupFilterRegex regex string - roleRegex := strings.Replace(groupFilterRegex, `\\`, `\`, -1) - - awsConfig.UseGroupMapping = useGroupMappingBool - awsConfig.JoinAllRoles = joinAllRolesBool - awsConfig.IdentityProviderArn = identityProviderArnString - awsConfig.IdentityProviderArnRegex = identityProviderRegex - awsConfig.RoleRegex = roleRegex - } return &Okta{ client: oktaClient, @@ -375,3 +282,132 @@ func New(ctx context.Context, cfg *Config) (*Okta, error) { awsConfig: awsConfig, }, nil } + +func (c *Okta) getAWSApplicationConfig(ctx context.Context) (*oktaAWSAppSettings, error) { + if c.awsConfig == nil { + return nil, nil + } + c.awsConfig.awsAppConfigCacheMutex.Lock() + defer c.awsConfig.awsAppConfigCacheMutex.Unlock() + if c.awsConfig.oktaAWSAppSettings != nil { + return c.awsConfig.oktaAWSAppSettings, nil + } + + if c.awsConfig.OktaAppId == "" { + return nil, fmt.Errorf("okta-connector: no app id set") + } + + app, awsAppResp, err := c.client.Application.GetApplication(ctx, c.awsConfig.OktaAppId, okta.NewApplication(), nil) + if err != nil { + return nil, fmt.Errorf("okta-connector: verify failed to fetch aws app: %w", err) + } + // TODO(lauren) do we need to parseResp? + awsAppRespCtx, err := responseToContext(&pagination.Token{}, awsAppResp) + if awsAppRespCtx.OktaResponse.StatusCode != http.StatusOK { + err := fmt.Errorf("okta-connector: verify returned non-200 for aws app: '%d'", awsAppRespCtx.OktaResponse.StatusCode) + return nil, err + } + oktaApp, err := oktaAppToOktaApplication(ctx, app) + if err != nil { + return nil, fmt.Errorf("okta-connector: verify failed to convert aws app: %w", err) + } + if oktaApp.Name != awsApp { + return nil, fmt.Errorf("okta-connector: okta app is not aws: %w", err) + } + if oktaApp.Settings == nil { + return nil, fmt.Errorf("okta-connector: settings are not present on okta app") + } + if oktaApp.Settings.App == nil { + return nil, fmt.Errorf("okta-connector: app settings are not present on okta app") + } + appSettings := *oktaApp.Settings.App + useGroupMapping, ok := appSettings["useGroupMapping"] + if !ok { + return nil, fmt.Errorf("okta-connector: 'useGroupMapping' app setting is not present on okta app settings") + } + useGroupMappingBool, ok := useGroupMapping.(bool) + if !ok { + return nil, fmt.Errorf("okta-connector: 'useGroupMapping' app setting is not boolean") + } + groupFilter, ok := appSettings["groupFilter"] + if !ok { + return nil, fmt.Errorf("okta-connector: 'groupFilter' app setting is not present on okta app settings") + } + groupFilterString, ok := groupFilter.(string) + if !ok { + return nil, fmt.Errorf("okta-connector: 'groupFilter' app setting is not string") + } + joinAllRoles, ok := appSettings["joinAllRoles"] + if !ok { + return nil, fmt.Errorf("okta-connector: 'joinAllRoles' app setting is not present on okta app settings") + } + joinAllRolesBool, ok := joinAllRoles.(bool) + if !ok { + return nil, fmt.Errorf("okta-connector: 'joinAllRoles' app setting is not boolean") + } + identityProviderArn, ok := appSettings["identityProviderArn"] + if !ok { + return nil, fmt.Errorf("okta-connector: 'identityProviderArn' app setting is not present on okta app settings") + } + identityProviderArnString, ok := identityProviderArn.(string) + if !ok { + return nil, fmt.Errorf("okta-connector: 'identityProviderArn' app setting is not string") + } + roleValuePattern, ok := appSettings["roleValuePattern"] + if !ok { + return nil, fmt.Errorf("okta-connector: 'roleValuePattern' app setting is not present on okta app settings") + } + roleValuePatternString, ok := roleValuePattern.(string) + if !ok { + return nil, fmt.Errorf("okta-connector: 'roleValuePattern' app setting is not string") + } + + splitPattern := strings.Split(roleValuePatternString, ",") + accountPattern := splitPattern[0] + + identityProviderRegex := strings.Replace(accountPattern, "${accountid}", `(\d{12})`, 1) + groupFilterRegex := strings.Replace(groupFilterString, `(?{{accountid}}`, `(\d+`, 1) + groupFilterRegex = strings.Replace(groupFilterRegex, `(?{{role}}`, `([a-zA-Z0-9+=,.@\\-_]+`, 1) + + // Unescape the groupFilterRegex regex string + roleRegex := strings.Replace(groupFilterRegex, `\\`, `\`, -1) + + // TODO(lauren) need to check resp? + appSamlRoles, _, err := c.listAWSSamlRoles(ctx, c.awsConfig.OktaAppId) + if err != nil { + return nil, err + } + // TODO(lauren) track iam roles also? + appSamlRolesMap := mapset.NewSet(appSamlRoles.SamlIamRole...) + + oktaAWSAppSettings := &oktaAWSAppSettings{ + JoinAllRoles: joinAllRolesBool, + IdentityProviderArn: identityProviderArnString, + RoleRegex: roleRegex, + IdentityProviderArnRegex: identityProviderRegex, + UseGroupMapping: useGroupMappingBool, + appSamlRoles: appSamlRolesMap, + } + c.awsConfig.oktaAWSAppSettings = oktaAWSAppSettings + return oktaAWSAppSettings, nil +} + +func (c *Okta) listAWSSamlRoles(ctx context.Context, appID string) (*AWSRoles, *responseContext, error) { + apiUrl := fmt.Sprintf("/api/v1/internal/apps/%s/types", appID) + + rq := c.client.CloneRequestExecutor() + + req, err := rq.WithAccept("application/json").WithContentType("application/json").NewRequest(http.MethodGet, apiUrl, nil) + if err != nil { + return nil, nil, err + } + + var awsRoles *AWSRoles + resp, err := rq.Do(ctx, req, &awsRoles) + if err != nil { + return nil, nil, err + } + respCtx, err := responseToContext(&pagination.Token{}, resp) + + return awsRoles, respCtx, nil +} diff --git a/pkg/connector/group.go b/pkg/connector/group.go index bfb5093e..f275a175 100644 --- a/pkg/connector/group.go +++ b/pkg/connector/group.go @@ -25,10 +25,7 @@ const membershipUpdatedField = "lastMembershipUpdated" type groupResourceType struct { resourceType *v2.ResourceType - domain string - apiToken string - client *okta.Client - awsConfig *awsConfig + connector *Okta } func (o *groupResourceType) ResourceType(_ context.Context) *v2.ResourceType { @@ -48,7 +45,7 @@ func (o *groupResourceType) List( var groups []*okta.Group var respCtx *responseContext - if o.awsConfig != nil && o.awsConfig.Enabled { + if o.connector.awsConfig != nil && o.connector.awsConfig.Enabled { qp := queryParamsExpand(token.Size, page, "group") groups, respCtx, err = o.listApplicationGroups(ctx, token, qp) if err != nil { @@ -150,53 +147,61 @@ func (o *groupResourceType) Grants( } for _, user := range users { + rv = append(rv, groupGrant(resource, user)) - var userGroupSet mapset.Set[string] - userGroupCachedSet, ok := o.awsConfig.appUserToGroup.Load(user.Id) - if !ok { - userGroupSet = mapset.NewSet[string](groupID) - } else { - userGroupSet, ok = userGroupCachedSet.(mapset.Set[string]) + if o.connector.awsConfig != nil && o.connector.awsConfig.Enabled { + awsAppSettings, err := o.connector.getAWSApplicationConfig(ctx) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get AWS app settings: %w", err) + } + var userGroupSet mapset.Set[string] + userGroupCachedSet, ok := awsAppSettings.appUserToGroup.Load(user.Id) if !ok { - return nil, "", nil, fmt.Errorf("error converting set") + userGroupSet = mapset.NewSet[string](groupID) + } else { + userGroupSet, ok = userGroupCachedSet.(mapset.Set[string]) + if !ok { + return nil, "", nil, fmt.Errorf("error converting set") + } + userGroupSet.Add(groupID) } - userGroupSet.Add(groupID) - } - o.awsConfig.appUserToGroup.Store(user.Id, userGroupSet) + awsAppSettings.appUserToGroup.Store(user.Id, userGroupSet) - if o.awsConfig.UseGroupMapping { - // Cache group grants - re, err := regexp.Compile(o.awsConfig.RoleRegex) - if err != nil { - return nil, "", nil, fmt.Errorf("error compiling regex '%s': %w", o.awsConfig.RoleRegex, err) - } - match := re.FindStringSubmatch(resource.DisplayName) + if awsAppSettings.UseGroupMapping { + // Cache group grants + re, err := regexp.Compile(awsAppSettings.RoleRegex) + if err != nil { + return nil, "", nil, fmt.Errorf("error compiling regex '%s': %w", awsAppSettings.RoleRegex, err) + } + match := re.FindStringSubmatch(resource.DisplayName) - if len(match) != 3 { - // TODO(lauren) error or continue? - continue - } + if len(match) != 3 { + // TODO(lauren) error or continue? + continue + } - // First element is full string - accountId := match[1] - role := match[2] - cachedAccountGrants, ok := o.awsConfig.accountGrantCache.Load(accountId) - if !ok { - return nil, "", nil, fmt.Errorf("error getting accounts grant cache '%s'", accountId) - } - accountGrantsPtr, ok := cachedAccountGrants.(*[]*GroupMappingGrant) - if !ok { - return nil, "", nil, fmt.Errorf("error casting account grants '%s'", accountId) - } - accountGrants := *accountGrantsPtr - gmg := &GroupMappingGrant{ - OktaUserID: user.Id, - Role: role, + // First element is full string + accountId := match[1] + role := match[2] + cachedAccountGrants, ok := awsAppSettings.accountGrantCache.Load(accountId) + if !ok { + return nil, "", nil, fmt.Errorf("error getting accounts grant cache '%s'", accountId) + } + accountGrantsPtr, ok := cachedAccountGrants.(*[]*GroupMappingGrant) + if !ok { + return nil, "", nil, fmt.Errorf("error casting account grants '%s'", accountId) + } + accountGrants := *accountGrantsPtr + gmg := &GroupMappingGrant{ + OktaUserID: user.Id, + Role: role, + } + accountGrants = append(accountGrants, gmg) + awsAppSettings.accountGrantCache.Store(accountId, &accountGrants) } - accountGrants = append(accountGrants, gmg) - o.awsConfig.accountGrantCache.Store(accountId, &accountGrants) } + } pageToken, err := bag.Marshal() @@ -215,7 +220,7 @@ func (o *groupResourceType) Grants( } func (o *groupResourceType) listGroups(ctx context.Context, token *pagination.Token, qp *query.Params) ([]*okta.Group, *responseContext, error) { - groups, resp, err := o.client.Group.ListGroups(ctx, qp) + groups, resp, err := o.connector.client.Group.ListGroups(ctx, qp) if err != nil { return nil, nil, fmt.Errorf("okta-connectorv2: failed to fetch groups from okta: %w", err) } @@ -229,8 +234,12 @@ func (o *groupResourceType) listGroups(ctx context.Context, token *pagination.To } func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pagination.Token, qp *query.Params) ([]*okta.Group, *responseContext, error) { + awsAppSettings, err := o.connector.getAWSApplicationConfig(ctx) + if err != nil { + return nil, nil, err + } l := ctxzap.Extract(ctx) - appGroups, reqCtx, err := listApplicationGroupAssignments(ctx, o.client, o.awsConfig.OktaAppId, token, qp) + appGroups, reqCtx, err := listApplicationGroupAssignments(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) if err != nil { return nil, nil, err } @@ -272,13 +281,13 @@ func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pa return nil, nil, fmt.Errorf("error getting samlRoles from group profile '%s': %w", appGroup.Id, err) } - o.awsConfig.groupToSamlRoleCache.Store(oktaGroup.Id, groupSAMLRoles) + awsAppSettings.groupToSamlRoleCache.Store(oktaGroup.Id, groupSAMLRoles) - if o.awsConfig.UseGroupMapping { + if awsAppSettings.UseGroupMapping { // TODO(lauren) move this to new connector? - re, err := regexp.Compile(o.awsConfig.RoleRegex) + re, err := regexp.Compile(awsAppSettings.RoleRegex) if err != nil { - return nil, nil, fmt.Errorf("error compiling regex '%s': %w", o.awsConfig.RoleRegex, err) + return nil, nil, fmt.Errorf("error compiling regex '%s': %w", awsAppSettings.RoleRegex, err) } match := re.FindStringSubmatch(oktaGroup.Profile.Name) if len(match) != 3 { @@ -290,7 +299,7 @@ func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pa role := match[2] var accountRoleSet mapset.Set[string] - accountRoleCachedSet, ok := o.awsConfig.accountRoleCache.Load(accountId) + accountRoleCachedSet, ok := awsAppSettings.accountRoleCache.Load(accountId) if !ok { accountRoleSet = mapset.NewSet[string](role) } else { @@ -300,10 +309,10 @@ func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pa } accountRoleSet.Add(role) } - o.awsConfig.accountRoleCache.Store(accountId, accountRoleSet) + awsAppSettings.accountRoleCache.Store(accountId, accountRoleSet) accountGrants := make([]*GroupMappingGrant, 0) // Initialize slice to cache account grants - o.awsConfig.accountGrantCache.LoadOrStore(accountId, &accountGrants) + awsAppSettings.accountGrantCache.LoadOrStore(accountId, &accountGrants) } } @@ -311,7 +320,7 @@ func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pa } func (o *groupResourceType) listGroupUsers(ctx context.Context, groupID string, token *pagination.Token, qp *query.Params) ([]*okta.User, *responseContext, error) { - users, resp, err := o.client.Group.ListGroupUsers(ctx, groupID, qp) + users, resp, err := o.connector.client.Group.ListGroupUsers(ctx, groupID, qp) if err != nil { return nil, nil, fmt.Errorf("okta-connectorv2: failed to fetch group users from okta: %w", err) } @@ -349,7 +358,7 @@ func (o *groupResourceType) groupResource(ctx context.Context, group *okta.Group annos.Update(&v2.V1Identifier{ Id: fmtResourceIdV1(group.Id), }) - if o.awsConfig != nil && o.awsConfig.Enabled { + if o.connector.awsConfig != nil && o.connector.awsConfig.Enabled { annos.Update(&v2.ChildResourceType{ ResourceTypeId: resourceTypeAccount.Id, }) @@ -417,7 +426,7 @@ func (g *groupResourceType) Grant(ctx context.Context, principal *v2.Resource, e groupId := entitlement.Resource.Id.Resource userId := principal.Id.Resource - response, err := g.client.Group.AddUserToGroup(ctx, groupId, userId) + response, err := g.connector.client.Group.AddUserToGroup(ctx, groupId, userId) if err != nil { return nil, err } @@ -445,7 +454,7 @@ func (g *groupResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annota groupId := entitlement.Resource.Id.Resource userId := principal.Id.Resource - response, err := g.client.Group.RemoveUserFromGroup(ctx, groupId, userId) + response, err := g.connector.client.Group.RemoveUserFromGroup(ctx, groupId, userId) if err != nil { return nil, err } @@ -457,12 +466,9 @@ func (g *groupResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annota return nil, nil } -func groupBuilder(domain string, apiToken string, client *okta.Client, awsConfig *awsConfig) *groupResourceType { +func groupBuilder(connector *Okta) *groupResourceType { return &groupResourceType{ resourceType: resourceTypeGroup, - domain: domain, - apiToken: apiToken, - client: client, - awsConfig: awsConfig, + connector: connector, } } From f7c1158b22b5474941f56b7570523b03ad7c8f10 Mon Sep 17 00:00:00 2001 From: Lauren Leach Date: Sat, 12 Oct 2024 17:44:43 -0700 Subject: [PATCH 4/7] use grant expandable/remove caching/add group cache --- pkg/connector/aws_account.go | 445 +++++++++++++++++++++++++---------- pkg/connector/connector.go | 75 ++++-- pkg/connector/group.go | 206 ++++++++-------- 3 files changed, 473 insertions(+), 253 deletions(-) diff --git a/pkg/connector/aws_account.go b/pkg/connector/aws_account.go index cdbf6d8c..5ecbc367 100644 --- a/pkg/connector/aws_account.go +++ b/pkg/connector/aws_account.go @@ -2,6 +2,7 @@ package connector import ( "context" + "encoding/json" "errors" "fmt" "log" @@ -16,8 +17,18 @@ import ( sdkGrant "github.com/conductorone/baton-sdk/pkg/types/grant" resource2 "github.com/conductorone/baton-sdk/pkg/types/resource" mapset "github.com/deckarep/golang-set/v2" + "github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap" + "github.com/okta/okta-sdk-golang/v2/okta" + "github.com/okta/okta-sdk-golang/v2/okta/query" + "go.uber.org/zap" ) +type OktaAppGroupWrapper struct { + oktaGroup *okta.Group + samlRoles []string + accountID string +} + type AWSRoles struct { AWSEnvironmentEnum []string `json:"AWSEnvironmentEnum,omitempty"` SamlIamRole []string `json:"SamlIamRole,omitempty"` @@ -77,22 +88,56 @@ func (o *accountResourceType) List( } return []*v2.Resource{resource}, "", nil, nil } else { - resources := make([]*v2.Resource, 0) - // TODO(lauren) check map is not empty/nil - // If it is, list groups and cache - awsConfig.accountRoleCache.Range(func(key, value interface{}) bool { - accountId, ok := key.(string) - if ok { - resource, err := resource2.NewResource(accountId, o.resourceType, accountId) - if err != nil { - // TODO(lauren) should we continue - return false - } - resources = append(resources, resource) + // TODO(lauren) what resource type should this be + bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) + } + + qp := queryParamsExpand(token.Size, page, "group") + accountSet := mapset.NewSet[string]() + + appGroups, respCtx, err := listApplicationGroupsHelper(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list application groups: %w", err) + } + + var rv []*v2.Resource + + nextPage, annos, err := parseResp(respCtx.OktaResponse) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) + } + err = bag.Next(nextPage) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) + } + + for _, group := range appGroups { + accountId, _, matchesRolePattern, err := parseAccountIDAndRoleFromGroupName(ctx, awsConfig.RoleRegex, group.Profile.Name) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse account id and role from group name: %w", err) } - return true // continue iteration - }) - return resources, "", nil, nil + if !matchesRolePattern { + continue + } + accountSet.Add(accountId) + } + + for accountId := range accountSet.Iterator().C { + resource, err := resource2.NewResource(accountId, o.resourceType, accountId) + if err != nil { + return nil, "", nil, err + } + rv = append(rv, resource) + } + + pageToken, err := bag.Marshal() + if err != nil { + return nil, "", nil, err + } + + return rv, pageToken, annos, nil } } @@ -107,39 +152,70 @@ func (o *accountResourceType) Entitlements( } var rv []*v2.Entitlement if !awsConfig.UseGroupMapping { - for role := range awsConfig.appSamlRoles.Iterator().C { + awsRoles, _, err := o.listAWSSamlRoles(ctx) + if err != nil { + return nil, "", nil, err + } + for _, role := range awsRoles.SamlIamRole { rv = append(rv, samlRoleEntitlement(resource, role)) } + return rv, "", nil, nil } else { - accountRoleCachedSet, ok := awsConfig.accountRoleCache.Load(resource.Id.GetResource()) - if !ok { - // Should this error or just return empty? - return rv, "", nil, nil + // TODO(lauren) what resource type should this be + bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) + } + + qp := queryParamsExpand(token.Size, page, "group") + + appGroups, respCtx, err := listApplicationGroupsHelper(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list application groups: %w", err) } - accountRoleSet, ok := accountRoleCachedSet.(mapset.Set[string]) - for role := range accountRoleSet.Iterator().C { - if !awsConfig.appSamlRoles.ContainsOne(role) { - // TODO(lauren) error or just ignore invalid role? + + nextPage, annos, err := parseResp(respCtx.OktaResponse) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) + } + err = bag.Next(nextPage) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) + } + + for _, group := range appGroups { + accountId, roleName, matchesRolePattern, err := parseAccountIDAndRoleFromGroupName(ctx, awsConfig.RoleRegex, group.Profile.Name) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse account id and role from group name: %w", err) + } + if !matchesRolePattern || accountId != resource.GetId().Resource { continue } - rv = append(rv, samlRoleEntitlement(resource, role)) + rv = append(rv, samlRoleEntitlement(resource, roleName)) + } + + pageToken, err := bag.Marshal() + if err != nil { + return nil, "", nil, err } - } - return rv, "", nil, nil + return rv, pageToken, annos, nil + } } func samlRoleEntitlement(resource *v2.Resource, role string) *v2.Entitlement { return sdkEntitlement.NewAssignmentEntitlement(resource, role, sdkEntitlement.WithDisplayName(fmt.Sprintf("%s Role Member", role)), sdkEntitlement.WithDescription(fmt.Sprintf("Has the %s role in AWS Okta app", role)), - /*sdkEntitlement.WithAnnotation(&v2.V1Identifier{ - Id: V1MembershipEntitlementID(role.Type), - }),*/ - sdkEntitlement.WithGrantableTo(resourceTypeUser), + sdkEntitlement.WithGrantableTo(resourceTypeUser, resourceTypeGroup), ) } +// Add group principal grant if assigned with a saml role +// Use expand grant if join all roles/use group mapping enabled to get user grants +// Otherwise: +// list application users, if direct assignment, give those role, if group scope, look at all the users groups +// if join all roles also do the above JUST for direct assignments func (o *accountResourceType) Grants( ctx context.Context, resource *v2.Resource, @@ -149,123 +225,153 @@ func (o *accountResourceType) Grants( if err != nil { return nil, "", nil, fmt.Errorf("error getting aws app settings config") } - var rv []*v2.Grant // TODO(lauren) what resource type should this be - bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) + bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resource.Id.ResourceType}) + //bag, page, err := parsePageToken(token.Token, resource.Id) + //bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) if err != nil { return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) } - useMapping := awsConfig.UseGroupMapping - - qp := queryParams(token.Size, page) - appUsers, respContext, err := listApplicationUsers(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) - if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: list application users %w", err) - } + switch bag.ResourceTypeID() { + case resourceTypeUser.Id: + qp := queryParams(token.Size, page) + appUsers, respContext, err := listApplicationUsers(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: list application users %w", err) + } - for _, appUser := range appUsers { - appUserSAMLRolesMap := mapset.NewSet[string]() + for _, appUser := range appUsers { + appUserSAMLRolesMap := mapset.NewSet[string]() - if appUser.Scope == "USER" { - if appUser.Profile == nil { - // TODO(lauren) continue or error? - continue - } - appUserProfile, ok := appUser.Profile.(map[string]interface{}) - if !ok { - // TODO(lauren) continue or error? - continue - } - appUserSAMLRoles, err := getSAMLRoles(appUserProfile) - if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get saml roles for user '%s': %w", appUser.Id, err) + if appUser.Scope == "USER" { + if appUser.Profile == nil { + // TODO(lauren) continue or error? + continue + } + appUserProfile, ok := appUser.Profile.(map[string]interface{}) + if !ok { + // TODO(lauren) continue or error? + continue + } + appUserSAMLRoles, err := getSAMLRoles(appUserProfile) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get saml roles for user '%s': %w", appUser.Id, err) + } + appUserSAMLRolesMap.Append(appUserSAMLRoles...) } - appUserSAMLRolesMap.Append(appUserSAMLRoles...) - } - - // If the user scope is "GROUP", this means the user does not have a direct assignment - // We want to get the union of the group's samlRoles that the user is assigned to - // We also want a union of the group's samlRoles if useGroupMapping is enabled - if appUser.Scope == "GROUP" || awsConfig.JoinAllRoles || useMapping { - appUserGroupCache, ok := awsConfig.appUserToGroup.Load(appUser.Id) - var appUserGroupsSet mapset.Set[string] - if !ok { - // TODO(lauren) This endpoint doesn't paginate but - // I think we should check the resp code rate limit? + + // If the user scope is "GROUP", this means the user does not have a direct assignment + // We want to get the union of the group's samlRoles that the user is assigned to + // We also want a union of the group's samlRoles if useGroupMapping is enabled + if appUser.Scope == "GROUP" && !awsConfig.JoinAllRoles { userGroups, _, err := listUsersGroupsClient(ctx, o.connector.client, appUser.Id) if err != nil { return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to groups for user '%s': %w", appUser.Id, err) } - groupIDSFilter := mapset.NewSet[string]() - awsConfig.groupToSamlRoleCache.Range(func(key, value interface{}) bool { - groupID, ok := key.(string) - if ok { - // TODO(lauren) return false or just continue? - groupIDSFilter.Add(groupID) - } - return true - }) - - filteredUserGroups := mapset.NewSet[string]() for _, userGroup := range userGroups { - if groupIDSFilter.ContainsOne(userGroup.Id) { - filteredUserGroups.Add(userGroup.Id) + // TODO(lauren) additional request, need to update ratelimit annotation? + oktaAppGroup, err := o.getOktaAppGroupFromCacheOrFetch(ctx, userGroup.Id) + if err != nil { + return nil, "", nil, err + } + if oktaAppGroup == nil { + continue } + appUserSAMLRolesMap.Append(oktaAppGroup.samlRoles...) } + } - awsConfig.appUserToGroup.Store(appUser.Id, filteredUserGroups) - appUserGroupsSet = filteredUserGroups - } else { - appUserGroupsSet = appUserGroupCache.(mapset.Set[string]) + // TODO(lauren) use ToSlice instead? + for samlRole := range appUserSAMLRolesMap.Iterator().C { + rv = append(rv, accountGrant(resource, samlRole, appUser.Id)) } + } + nextPage, annos, err := parseResp(respContext.OktaResponse) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) + } - for group := range appUserGroupsSet.Iterator().C { - groupRoleCacheVal, ok := awsConfig.groupToSamlRoleCache.Load(group) - if !ok { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get roles for group '%s'", group) - } - groupRoleSet, ok := groupRoleCacheVal.(mapset.Set[string]) - if !ok { - return nil, "", nil, fmt.Errorf("error converting group '%s' role set", group) + err = bag.Next(nextPage) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) + } + + pageToken, err := bag.Marshal() + if err != nil { + return nil, "", nil, err + } + return rv, pageToken, annos, nil + default: + qp := queryParamsExpand(token.Size, page, "group") + appGroups, respCtx, err := listApplicationGroupAssignments(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list application groups: %w", err) + } + + nextPage, annos, err := parseResp(respCtx.OktaResponse) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) + } + + for _, group := range appGroups { + oktaAppGroup, err := o.oktaAppGroup(ctx, group) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list application groups: %w", err) + } + if oktaAppGroup.accountID != resource.GetId().GetResource() { + continue + } + + // TODO(lauren) we only need this cached when !awsConfig.UseGroupMapping & !awsConfig.JoinAllRoles + awsConfig.appGroupCache.Store(group.Id, oktaAppGroup) + for _, role := range oktaAppGroup.samlRoles { + if !awsConfig.UseGroupMapping && !awsConfig.JoinAllRoles { + rv = append(rv, accountGrantGroup(resource, role, oktaAppGroup.oktaGroup.Id)) + } else { + rv = append(rv, accountGrantGroupExpandable(resource, role, oktaAppGroup.oktaGroup.Id)) } - // TODO(lauren) is this safe? - appUserSAMLRolesMap = appUserSAMLRolesMap.Union(groupRoleSet) } } - // TODO(lauren) use ToSlice instead? - for samlRole := range appUserSAMLRolesMap.Iterator().C { - rv = append(rv, accountGrant(resource, samlRole, appUser.Id)) + err = bag.Next(nextPage) + if err != nil { + return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) } - } - nextPage, annos, err := parseResp(respContext.OktaResponse) - if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) - } + if !awsConfig.UseGroupMapping { + bag.Push(pagination.PageState{ + ResourceTypeID: resourceTypeUser.Id, + }) + } - err = bag.Next(nextPage) - if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) - } + pageToken, err := bag.Marshal() + if err != nil { + return nil, "", nil, err + } - pageToken, err := bag.Marshal() - if err != nil { - return nil, "", nil, err + return rv, pageToken, annos, nil } - - return rv, pageToken, annos, nil } func accountGrant(resource *v2.Resource, samlRole string, oktaUserId string) *v2.Grant { ur := &v2.Resource{Id: &v2.ResourceId{ResourceType: resourceTypeUser.Id, Resource: oktaUserId}} + return sdkGrant.NewGrant(resource, samlRole, ur) +} + +func accountGrantGroup(resource *v2.Resource, samlRole string, groupId string) *v2.Grant { + gr := &v2.Resource{Id: &v2.ResourceId{ResourceType: resourceTypeGroup.Id, Resource: groupId}} + return sdkGrant.NewGrant(resource, samlRole, gr) +} - return sdkGrant.NewGrant(resource, samlRole, ur, sdkGrant.WithAnnotation(&v2.V1Identifier{ - Id: fmtGrantIdV1(V1MembershipEntitlementID(resource.Id.Resource), oktaUserId), +func accountGrantGroupExpandable(resource *v2.Resource, samlRole string, groupId string) *v2.Grant { + gr := &v2.Resource{Id: &v2.ResourceId{ResourceType: resourceTypeGroup.Id, Resource: groupId}} + return sdkGrant.NewGrant(resource, samlRole, gr, sdkGrant.WithAnnotation(&v2.GrantExpandable{ + EntitlementIds: []string{fmt.Sprintf("group:%s:member", groupId)}, + Shallow: true, })) } @@ -280,7 +386,6 @@ Join all roles ON: Role1, Role2, RoleA, and RoleB are available upon login to AW */ func (o *accountResourceType) listAWSSamlRoles(ctx context.Context) (*AWSRoles, *responseContext, error) { - apiUrl := fmt.Sprintf("/api/v1/internal/apps/%s/types", o.connector.awsConfig.OktaAppId) rq := o.connector.client.CloneRequestExecutor() @@ -322,24 +427,110 @@ func getSAMLRoles(profile map[string]interface{}) ([]string, error) { return ret, nil } -func getSAMLRolesMap(profile map[string]interface{}) (mapset.Set[string], error) { - ret := mapset.NewSet[string]() - samlRolesField := profile["samlRoles"] - if samlRolesField == nil { - return ret, nil +func (o *accountResourceType) oktaAppGroup(ctx context.Context, appGroup *okta.ApplicationGroupAssignment) (*OktaAppGroupWrapper, error) { + embedded := appGroup.Embedded + if embedded == nil { + return nil, fmt.Errorf("app group '%s' embedded data was nil", appGroup.Id) + } + embeddedMap, ok := embedded.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("app group embedded data was not a map for group with id '%s'", appGroup.Id) + } + embeddedGroup, ok := embeddedMap["group"] + if !ok { + return nil, fmt.Errorf("embedded group data was nil for app group '%s'", appGroup.Id) + } + groupJSON, err := json.Marshal(embeddedGroup) + if err != nil { + return nil, fmt.Errorf("error marshalling embedded group data for app group '%s': %w", appGroup.Id, err) + } + oktaGroup := &okta.Group{} + err = json.Unmarshal(groupJSON, &oktaGroup) + if err != nil { + return nil, fmt.Errorf("error unmarshalling embedded group data for app group '%s': %w", appGroup.Id, err) } - samlRoles, ok := samlRolesField.([]interface{}) + appGroupProfile, ok := appGroup.Profile.(map[string]interface{}) if !ok { - return nil, errors.New("unexpected type in profile[\"samlRoles\"") + return nil, fmt.Errorf("error converting app group profile '%s'", appGroup.Id) } - for _, r := range samlRoles { - role, ok := r.(string) - if !ok { - return nil, errors.New("role was not string") + awsAppSettings, err := o.connector.getAWSApplicationConfig(ctx) + if err != nil { + return nil, err + } + samlRoles := make([]string, 0) + accountId := awsAppSettings.IdentityProviderArnAccountID + var roleName string + matchesRolePattern := false + + if awsAppSettings.UseGroupMapping { + accountId, roleName, matchesRolePattern, err = parseAccountIDAndRoleFromGroupName(ctx, awsAppSettings.RoleRegex, oktaGroup.Profile.Name) + if err != nil { + return nil, err + } + if matchesRolePattern { + samlRoles = append(samlRoles, roleName) + } + } else { + samlRoles, err = getSAMLRoles(appGroupProfile) + if err != nil { + return nil, err } - ret.Add(role) } - return ret, nil + + return &OktaAppGroupWrapper{ + oktaGroup: oktaGroup, + samlRoles: samlRoles, + accountID: accountId, + }, nil +} + +func (o *accountResourceType) getOktaAppGroupFromCacheOrFetch(ctx context.Context, groupId string) (*OktaAppGroupWrapper, error) { + l := ctxzap.Extract(ctx) + awsConfig, err := o.connector.getAWSApplicationConfig(ctx) + if err != nil { + return nil, err + } + appGroup, err := awsConfig.getAppGroupFromCache(ctx, groupId) + if err != nil { + return nil, err + } + if appGroup != nil { + l.Debug("okta-aws-connector: found group in cache", zap.String("groupId", groupId)) + return appGroup, nil + } + notAnAppGroup, err := awsConfig.checkIfNotAppGroupFromCache(ctx, groupId) + if err != nil { + return nil, err + } + if notAnAppGroup { + return nil, nil + } + oktaAppGroup, resp, err := o.connector.client.Application.GetApplicationGroupAssignment( + ctx, o.connector.awsConfig.OktaAppId, + groupId, + query.NewQueryParams(query.WithExpand("group"))) + + if err != nil { + defer resp.Body.Close() + errOkta, err := getError(resp) + if err != nil { + return nil, err + } + if errOkta.ErrorCode != ResourceNotFoundExceptionErrorCode { + l.Warn("okta-aws-connector: ", zap.String("ErrorCode", errOkta.ErrorCode), zap.String("ErrorSummary", errOkta.ErrorSummary)) + return nil, fmt.Errorf("okta-connector: %v", errOkta) + } + awsConfig.notAppGroupCache.Store(groupId, true) + return nil, nil + } + + appGroup, err = o.oktaAppGroup(ctx, oktaAppGroup) + if err != nil { + return nil, err + } + awsConfig.appGroupCache.Store(ctx, appGroup) + + return appGroup, nil } diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 6b4aa2b1..58d10d26 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -5,6 +5,7 @@ import ( "fmt" "io" "net/http" + "regexp" "strings" "sync" @@ -13,11 +14,11 @@ import ( "github.com/conductorone/baton-sdk/pkg/connectorbuilder" "github.com/conductorone/baton-sdk/pkg/pagination" "github.com/conductorone/baton-sdk/pkg/uhttp" - mapset "github.com/deckarep/golang-set/v2" "github.com/okta/okta-sdk-golang/v2/okta" ) const awsApp = "amazon_aws" +const ResourceNotFoundExceptionErrorCode = "E0000007" type Okta struct { client *okta.Client @@ -43,16 +44,14 @@ type awsConfig struct { } type oktaAWSAppSettings struct { - JoinAllRoles bool - IdentityProviderArn string - RoleRegex string - IdentityProviderArnRegex string - UseGroupMapping bool - appUserToGroup sync.Map // user id (key) to group mapset - groupToSamlRoleCache sync.Map // group id to samlRoles mapset - accountRoleCache sync.Map // key is account id, val is samlRole mapset - accountGrantCache sync.Map // account -> slice of group grants - appSamlRoles mapset.Set[string] // TODO(lauren) Is this safe? + JoinAllRoles bool + IdentityProviderArn string + RoleRegex string + IdentityProviderArnRegex string + UseGroupMapping bool + IdentityProviderArnAccountID string + appGroupCache sync.Map // group ID to app group cache + notAppGroupCache sync.Map // group IDs that are not app groups } type Config struct { @@ -301,7 +300,6 @@ func (c *Okta) getAWSApplicationConfig(ctx context.Context) (*oktaAWSAppSettings if err != nil { return nil, fmt.Errorf("okta-connector: verify failed to fetch aws app: %w", err) } - // TODO(lauren) do we need to parseResp? awsAppRespCtx, err := responseToContext(&pagination.Token{}, awsAppResp) if awsAppRespCtx.OktaResponse.StatusCode != http.StatusOK { err := fmt.Errorf("okta-connector: verify returned non-200 for aws app: '%d'", awsAppRespCtx.OktaResponse.StatusCode) @@ -372,21 +370,28 @@ func (c *Okta) getAWSApplicationConfig(ctx context.Context) (*oktaAWSAppSettings // Unescape the groupFilterRegex regex string roleRegex := strings.Replace(groupFilterRegex, `\\`, `\`, -1) - // TODO(lauren) need to check resp? - appSamlRoles, _, err := c.listAWSSamlRoles(ctx, c.awsConfig.OktaAppId) + // TODO(lauren) only do this if use group mapping not enabled? + re, err := regexp.Compile(strings.ToLower(identityProviderRegex)) if err != nil { - return nil, err + return nil, fmt.Errorf("okta-connector: error compiling 'identityProviderRegex' regex") + } + match := re.FindStringSubmatch(strings.ToLower(identityProviderArnString)) + + // First element is full string + if len(match) != 2 { + if err != nil { + return nil, fmt.Errorf("okta-aws-connector: error getting account id from identityProviderArn") + } } - // TODO(lauren) track iam roles also? - appSamlRolesMap := mapset.NewSet(appSamlRoles.SamlIamRole...) + accountId := match[1] oktaAWSAppSettings := &oktaAWSAppSettings{ - JoinAllRoles: joinAllRolesBool, - IdentityProviderArn: identityProviderArnString, - RoleRegex: roleRegex, - IdentityProviderArnRegex: identityProviderRegex, - UseGroupMapping: useGroupMappingBool, - appSamlRoles: appSamlRolesMap, + JoinAllRoles: joinAllRolesBool, + IdentityProviderArn: identityProviderArnString, + RoleRegex: roleRegex, + IdentityProviderArnRegex: identityProviderRegex, + UseGroupMapping: useGroupMappingBool, + IdentityProviderArnAccountID: accountId, } c.awsConfig.oktaAWSAppSettings = oktaAWSAppSettings return oktaAWSAppSettings, nil @@ -411,3 +416,27 @@ func (c *Okta) listAWSSamlRoles(ctx context.Context, appID string) (*AWSRoles, * return awsRoles, respCtx, nil } + +func (a *oktaAWSAppSettings) getAppGroupFromCache(ctx context.Context, groupId string) (*OktaAppGroupWrapper, error) { + appGroupCacheVal, ok := a.appGroupCache.Load(groupId) + if !ok { + return nil, nil + } + oktaAppGroup, ok := appGroupCacheVal.(*OktaAppGroupWrapper) + if !ok { + return nil, fmt.Errorf("error converting app group '%s' from cache", oktaAppGroup) + } + return oktaAppGroup, nil +} + +func (a *oktaAWSAppSettings) checkIfNotAppGroupFromCache(ctx context.Context, groupId string) (bool, error) { + notAppGroupCacheVal, ok := a.notAppGroupCache.Load(groupId) + if !ok { + return false, nil + } + notAppGroup, ok := notAppGroupCacheVal.(bool) + if !ok { + return false, fmt.Errorf("error converting not a app group bool '%s' ", notAppGroup) + } + return notAppGroup, nil +} diff --git a/pkg/connector/group.go b/pkg/connector/group.go index f275a175..78f224d9 100644 --- a/pkg/connector/group.go +++ b/pkg/connector/group.go @@ -7,7 +7,6 @@ import ( "regexp" "time" - mapset "github.com/deckarep/golang-set/v2" "go.uber.org/zap" "google.golang.org/protobuf/types/known/structpb" @@ -43,6 +42,7 @@ func (o *groupResourceType) List( return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) } + var rv []*v2.Resource var groups []*okta.Group var respCtx *responseContext if o.connector.awsConfig != nil && o.connector.awsConfig.Enabled { @@ -59,8 +59,6 @@ func (o *groupResourceType) List( } } - var rv []*v2.Resource - nextPage, annos, err := parseResp(respCtx.OktaResponse) if err != nil { return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) @@ -147,61 +145,7 @@ func (o *groupResourceType) Grants( } for _, user := range users { - rv = append(rv, groupGrant(resource, user)) - - if o.connector.awsConfig != nil && o.connector.awsConfig.Enabled { - awsAppSettings, err := o.connector.getAWSApplicationConfig(ctx) - if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get AWS app settings: %w", err) - } - var userGroupSet mapset.Set[string] - userGroupCachedSet, ok := awsAppSettings.appUserToGroup.Load(user.Id) - if !ok { - userGroupSet = mapset.NewSet[string](groupID) - } else { - userGroupSet, ok = userGroupCachedSet.(mapset.Set[string]) - if !ok { - return nil, "", nil, fmt.Errorf("error converting set") - } - userGroupSet.Add(groupID) - } - awsAppSettings.appUserToGroup.Store(user.Id, userGroupSet) - - if awsAppSettings.UseGroupMapping { - // Cache group grants - re, err := regexp.Compile(awsAppSettings.RoleRegex) - if err != nil { - return nil, "", nil, fmt.Errorf("error compiling regex '%s': %w", awsAppSettings.RoleRegex, err) - } - match := re.FindStringSubmatch(resource.DisplayName) - - if len(match) != 3 { - // TODO(lauren) error or continue? - continue - } - - // First element is full string - accountId := match[1] - role := match[2] - cachedAccountGrants, ok := awsAppSettings.accountGrantCache.Load(accountId) - if !ok { - return nil, "", nil, fmt.Errorf("error getting accounts grant cache '%s'", accountId) - } - accountGrantsPtr, ok := cachedAccountGrants.(*[]*GroupMappingGrant) - if !ok { - return nil, "", nil, fmt.Errorf("error casting account grants '%s'", accountId) - } - accountGrants := *accountGrantsPtr - gmg := &GroupMappingGrant{ - OktaUserID: user.Id, - Role: role, - } - accountGrants = append(accountGrants, gmg) - awsAppSettings.accountGrantCache.Store(accountId, &accountGrants) - } - } - } pageToken, err := bag.Marshal() @@ -234,18 +178,102 @@ func (o *groupResourceType) listGroups(ctx context.Context, token *pagination.To } func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pagination.Token, qp *query.Params) ([]*okta.Group, *responseContext, error) { - awsAppSettings, err := o.connector.getAWSApplicationConfig(ctx) + awsConfig, err := o.connector.getAWSApplicationConfig(ctx) if err != nil { return nil, nil, err } - l := ctxzap.Extract(ctx) + appGroups, reqCtx, err := listApplicationGroupAssignments(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) if err != nil { return nil, nil, err } groups := make([]*okta.Group, 0, len(appGroups)) + for _, appGroup := range appGroups { + oktaAppGroup, err := o.oktaAppGroup(ctx, appGroup) + if err != nil { + return nil, nil, err + } + groups = append(groups, oktaAppGroup.oktaGroup) + awsConfig.appGroupCache.Store(appGroup.Id, oktaAppGroup) + } + + return groups, reqCtx, nil +} + +func (o *groupResourceType) oktaAppGroup(ctx context.Context, appGroup *okta.ApplicationGroupAssignment) (*OktaAppGroupWrapper, error) { + embedded := appGroup.Embedded + if embedded == nil { + return nil, fmt.Errorf("app group '%s' embedded data was nil", appGroup.Id) + } + embeddedMap, ok := embedded.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("app group embedded data was not a map for group with id '%s'", appGroup.Id) + } + embeddedGroup, ok := embeddedMap["group"] + if !ok { + return nil, fmt.Errorf("embedded group data was nil for app group '%s'", appGroup.Id) + } + groupJSON, err := json.Marshal(embeddedGroup) + if err != nil { + return nil, fmt.Errorf("error marshalling embedded group data for app group '%s': %w", appGroup.Id, err) + } + oktaGroup := &okta.Group{} + err = json.Unmarshal(groupJSON, &oktaGroup) + if err != nil { + return nil, fmt.Errorf("error unmarshalling embedded group data for app group '%s': %w", appGroup.Id, err) + } + + appGroupProfile, ok := appGroup.Profile.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("error converting app group profile '%s'", appGroup.Id) + } + + awsAppSettings, err := o.connector.getAWSApplicationConfig(ctx) + if err != nil { + return nil, err + } + samlRoles := make([]string, 0) + accountId := awsAppSettings.IdentityProviderArnAccountID + var roleName string + matchesRolePattern := false + if awsAppSettings.UseGroupMapping { + accountId, roleName, matchesRolePattern, err = parseAccountIDAndRoleFromGroupName(ctx, awsAppSettings.RoleRegex, oktaGroup.Profile.Name) + if err != nil { + return nil, err + } + if matchesRolePattern { + samlRoles = append(samlRoles, roleName) + } + } else { + samlRoles, err = getSAMLRoles(appGroupProfile) + if err != nil { + return nil, err + } + } + + return &OktaAppGroupWrapper{ + oktaGroup: oktaGroup, + samlRoles: samlRoles, + accountID: accountId, + }, nil +} + +// TODO(lauren) move shared code into helper +func listApplicationGroupsHelper( + ctx context.Context, + client *okta.Client, + appID string, + token *pagination.Token, + qp *query.Params, +) ([]*okta.Group, *responseContext, error) { + appGroups, reqCtx, err := listApplicationGroupAssignments(ctx, client, appID, token, qp) + if err != nil { + return nil, nil, err + } + + groups := make([]*okta.Group, 0, len(appGroups)) for _, appGroup := range appGroups { embedded := appGroup.Embedded if embedded == nil { @@ -269,54 +297,26 @@ func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pa return nil, nil, fmt.Errorf("error unmarshalling embedded group data for app group '%s': %w", appGroup.Id, err) } groups = append(groups, oktaGroup) + } - appGroupProfile, ok := appGroup.Profile.(map[string]interface{}) - if !ok { - l.Info("error converting app group profile", zap.String("groupId", appGroup.Id), zap.Error(err)) - return nil, nil, fmt.Errorf("error converting app group profile '%s'", appGroup.Id) - } - groupSAMLRoles, err := getSAMLRolesMap(appGroupProfile) - if err != nil { - l.Info("error getting samlRoles from group profile", zap.String("groupId", appGroup.Id), zap.Error(err)) - return nil, nil, fmt.Errorf("error getting samlRoles from group profile '%s': %w", appGroup.Id, err) - } + return groups, reqCtx, nil +} - awsAppSettings.groupToSamlRoleCache.Store(oktaGroup.Id, groupSAMLRoles) - - if awsAppSettings.UseGroupMapping { - // TODO(lauren) move this to new connector? - re, err := regexp.Compile(awsAppSettings.RoleRegex) - if err != nil { - return nil, nil, fmt.Errorf("error compiling regex '%s': %w", awsAppSettings.RoleRegex, err) - } - match := re.FindStringSubmatch(oktaGroup.Profile.Name) - if len(match) != 3 { - continue - } - - // First element is full string - accountId := match[1] - role := match[2] - - var accountRoleSet mapset.Set[string] - accountRoleCachedSet, ok := awsAppSettings.accountRoleCache.Load(accountId) - if !ok { - accountRoleSet = mapset.NewSet[string](role) - } else { - accountRoleSet, ok = accountRoleCachedSet.(mapset.Set[string]) - if !ok { - return nil, nil, fmt.Errorf("error converting set") - } - accountRoleSet.Add(role) - } - awsAppSettings.accountRoleCache.Store(accountId, accountRoleSet) - accountGrants := make([]*GroupMappingGrant, 0) - // Initialize slice to cache account grants - awsAppSettings.accountGrantCache.LoadOrStore(accountId, &accountGrants) - } +func parseAccountIDAndRoleFromGroupName(ctx context.Context, roleRegex string, groupName string) (string, string, bool, error) { + // TODO(lauren) move to get app config + re, err := regexp.Compile(roleRegex) + if err != nil { + return "", "", false, fmt.Errorf("error compiling regex '%s': %w", roleRegex, err) + } + match := re.FindStringSubmatch(groupName) + if len(match) != 3 { + return "", "", false, nil } + // First element is full string + accountId := match[1] + role := match[2] - return groups, reqCtx, nil + return accountId, role, true, nil } func (o *groupResourceType) listGroupUsers(ctx context.Context, groupID string, token *pagination.Token, qp *query.Params) ([]*okta.User, *responseContext, error) { From c6a011d817fd823c5092498f1a05e02ebc419204 Mon Sep 17 00:00:00 2001 From: Lauren Leach Date: Sat, 12 Oct 2024 20:10:35 -0700 Subject: [PATCH 5/7] cleanup --- pkg/connector/aws_account.go | 138 ++++++++--------------------------- pkg/connector/connector.go | 46 +++++++++++- pkg/connector/group.go | 73 ++++++++---------- pkg/connector/pagination.go | 10 +++ 4 files changed, 112 insertions(+), 155 deletions(-) diff --git a/pkg/connector/aws_account.go b/pkg/connector/aws_account.go index 5ecbc367..21af94d6 100644 --- a/pkg/connector/aws_account.go +++ b/pkg/connector/aws_account.go @@ -2,13 +2,9 @@ package connector import ( "context" - "encoding/json" "errors" "fmt" - "log" "net/http" - "regexp" - "strings" v2 "github.com/conductorone/baton-sdk/pb/c1/connector/v2" "github.com/conductorone/baton-sdk/pkg/annotations" @@ -35,11 +31,6 @@ type AWSRoles struct { IamRole []string `json:"IamRole,omitempty"` } -type GroupMappingGrant struct { - OktaUserID string - Role string -} - type accountResourceType struct { resourceType *v2.ResourceType connector *Okta @@ -66,21 +57,7 @@ func (o *accountResourceType) List( return nil, "", nil, fmt.Errorf("error getting aws app settings config") } if !awsConfig.UseGroupMapping { - // TODO(lauren) move to new connector - re, err := regexp.Compile(strings.ToLower(awsConfig.IdentityProviderArnRegex)) - if err != nil { - log.Fatal(err) - } - match := re.FindStringSubmatch(strings.ToLower(awsConfig.IdentityProviderArn)) - - // First element is full string - if len(match) != 2 { - if err != nil { - return nil, "", nil, fmt.Errorf("error getting aws account id") - } - } - accountId := match[1] - + accountId := awsConfig.IdentityProviderArnAccountID // TODO(lauren) what should name be? resource, err := resource2.NewResource(accountId, o.resourceType, accountId) if err != nil { @@ -91,7 +68,7 @@ func (o *accountResourceType) List( // TODO(lauren) what resource type should this be bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to parse page token: %w", err) } qp := queryParamsExpand(token.Size, page, "group") @@ -99,24 +76,24 @@ func (o *accountResourceType) List( appGroups, respCtx, err := listApplicationGroupsHelper(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list application groups: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to list application groups: %w", err) } var rv []*v2.Resource nextPage, annos, err := parseResp(respCtx.OktaResponse) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to parse response: %w", err) } err = bag.Next(nextPage) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to fetch bag.Next: %w", err) } for _, group := range appGroups { accountId, _, matchesRolePattern, err := parseAccountIDAndRoleFromGroupName(ctx, awsConfig.RoleRegex, group.Profile.Name) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse account id and role from group name: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to parse account id and role from group name: %w", err) } if !matchesRolePattern { continue @@ -152,41 +129,45 @@ func (o *accountResourceType) Entitlements( } var rv []*v2.Entitlement if !awsConfig.UseGroupMapping { - awsRoles, _, err := o.listAWSSamlRoles(ctx) + awsRoles, respCtx, err := o.listAWSSamlRoles(ctx) if err != nil { return nil, "", nil, err } for _, role := range awsRoles.SamlIamRole { rv = append(rv, samlRoleEntitlement(resource, role)) } - return rv, "", nil, nil + annos, err := parseGetResp(respCtx.OktaResponse) + if err != nil { + return nil, "", nil, err + } + return rv, "", annos, nil } else { // TODO(lauren) what resource type should this be bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to parse page token: %w", err) } qp := queryParamsExpand(token.Size, page, "group") appGroups, respCtx, err := listApplicationGroupsHelper(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list application groups: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to list application groups: %w", err) } nextPage, annos, err := parseResp(respCtx.OktaResponse) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to parse response: %w", err) } err = bag.Next(nextPage) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to fetch bag.Next: %w", err) } for _, group := range appGroups { accountId, roleName, matchesRolePattern, err := parseAccountIDAndRoleFromGroupName(ctx, awsConfig.RoleRegex, group.Profile.Name) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse account id and role from group name: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to parse account id and role from group name: %w", err) } if !matchesRolePattern || accountId != resource.GetId().Resource { continue @@ -232,7 +213,7 @@ func (o *accountResourceType) Grants( //bag, page, err := parsePageToken(token.Token, resource.Id) //bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse page token: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to parse page token: %w", err) } switch bag.ResourceTypeID() { @@ -240,7 +221,7 @@ func (o *accountResourceType) Grants( qp := queryParams(token.Size, page) appUsers, respContext, err := listApplicationUsers(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: list application users %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: list application users %w", err) } for _, appUser := range appUsers { @@ -258,7 +239,7 @@ func (o *accountResourceType) Grants( } appUserSAMLRoles, err := getSAMLRoles(appUserProfile) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to get saml roles for user '%s': %w", appUser.Id, err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to get saml roles for user '%s': %w", appUser.Id, err) } appUserSAMLRolesMap.Append(appUserSAMLRoles...) } @@ -269,7 +250,7 @@ func (o *accountResourceType) Grants( if appUser.Scope == "GROUP" && !awsConfig.JoinAllRoles { userGroups, _, err := listUsersGroupsClient(ctx, o.connector.client, appUser.Id) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to groups for user '%s': %w", appUser.Id, err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to groups for user '%s': %w", appUser.Id, err) } for _, userGroup := range userGroups { @@ -292,12 +273,12 @@ func (o *accountResourceType) Grants( } nextPage, annos, err := parseResp(respContext.OktaResponse) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to parse response: %w", err) } err = bag.Next(nextPage) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to fetch bag.Next: %w", err) } pageToken, err := bag.Marshal() @@ -309,18 +290,18 @@ func (o *accountResourceType) Grants( qp := queryParamsExpand(token.Size, page, "group") appGroups, respCtx, err := listApplicationGroupAssignments(ctx, o.connector.client, o.connector.awsConfig.OktaAppId, token, qp) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list application groups: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to list application groups: %w", err) } nextPage, annos, err := parseResp(respCtx.OktaResponse) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to parse response: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to parse response: %w", err) } for _, group := range appGroups { - oktaAppGroup, err := o.oktaAppGroup(ctx, group) + oktaAppGroup, err := awsConfig.oktaAppGroup(ctx, group) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to list application groups: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to list application groups: %w", err) } if oktaAppGroup.accountID != resource.GetId().GetResource() { continue @@ -339,7 +320,7 @@ func (o *accountResourceType) Grants( err = bag.Next(nextPage) if err != nil { - return nil, "", nil, fmt.Errorf("okta-connectorv2: failed to fetch bag.Next: %w", err) + return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to fetch bag.Next: %w", err) } if !awsConfig.UseGroupMapping { @@ -427,65 +408,6 @@ func getSAMLRoles(profile map[string]interface{}) ([]string, error) { return ret, nil } -func (o *accountResourceType) oktaAppGroup(ctx context.Context, appGroup *okta.ApplicationGroupAssignment) (*OktaAppGroupWrapper, error) { - embedded := appGroup.Embedded - if embedded == nil { - return nil, fmt.Errorf("app group '%s' embedded data was nil", appGroup.Id) - } - embeddedMap, ok := embedded.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("app group embedded data was not a map for group with id '%s'", appGroup.Id) - } - embeddedGroup, ok := embeddedMap["group"] - if !ok { - return nil, fmt.Errorf("embedded group data was nil for app group '%s'", appGroup.Id) - } - groupJSON, err := json.Marshal(embeddedGroup) - if err != nil { - return nil, fmt.Errorf("error marshalling embedded group data for app group '%s': %w", appGroup.Id, err) - } - oktaGroup := &okta.Group{} - err = json.Unmarshal(groupJSON, &oktaGroup) - if err != nil { - return nil, fmt.Errorf("error unmarshalling embedded group data for app group '%s': %w", appGroup.Id, err) - } - - appGroupProfile, ok := appGroup.Profile.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("error converting app group profile '%s'", appGroup.Id) - } - - awsAppSettings, err := o.connector.getAWSApplicationConfig(ctx) - if err != nil { - return nil, err - } - samlRoles := make([]string, 0) - accountId := awsAppSettings.IdentityProviderArnAccountID - var roleName string - matchesRolePattern := false - - if awsAppSettings.UseGroupMapping { - accountId, roleName, matchesRolePattern, err = parseAccountIDAndRoleFromGroupName(ctx, awsAppSettings.RoleRegex, oktaGroup.Profile.Name) - if err != nil { - return nil, err - } - if matchesRolePattern { - samlRoles = append(samlRoles, roleName) - } - } else { - samlRoles, err = getSAMLRoles(appGroupProfile) - if err != nil { - return nil, err - } - } - - return &OktaAppGroupWrapper{ - oktaGroup: oktaGroup, - samlRoles: samlRoles, - accountID: accountId, - }, nil -} - func (o *accountResourceType) getOktaAppGroupFromCacheOrFetch(ctx context.Context, groupId string) (*OktaAppGroupWrapper, error) { l := ctxzap.Extract(ctx) awsConfig, err := o.connector.getAWSApplicationConfig(ctx) @@ -520,13 +442,13 @@ func (o *accountResourceType) getOktaAppGroupFromCacheOrFetch(ctx context.Contex } if errOkta.ErrorCode != ResourceNotFoundExceptionErrorCode { l.Warn("okta-aws-connector: ", zap.String("ErrorCode", errOkta.ErrorCode), zap.String("ErrorSummary", errOkta.ErrorSummary)) - return nil, fmt.Errorf("okta-connector: %v", errOkta) + return nil, fmt.Errorf("okta-aws-connector: %v", errOkta) } awsConfig.notAppGroupCache.Store(groupId, true) return nil, nil } - appGroup, err = o.oktaAppGroup(ctx, oktaAppGroup) + appGroup, err = awsConfig.oktaAppGroup(ctx, oktaAppGroup) if err != nil { return nil, err } diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 58d10d26..07ad9a52 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -371,19 +371,19 @@ func (c *Okta) getAWSApplicationConfig(ctx context.Context) (*oktaAWSAppSettings roleRegex := strings.Replace(groupFilterRegex, `\\`, `\`, -1) // TODO(lauren) only do this if use group mapping not enabled? - re, err := regexp.Compile(strings.ToLower(identityProviderRegex)) + identityProvideArnAccountIDRegex, err := regexp.Compile(strings.ToLower(identityProviderRegex)) if err != nil { return nil, fmt.Errorf("okta-connector: error compiling 'identityProviderRegex' regex") } - match := re.FindStringSubmatch(strings.ToLower(identityProviderArnString)) + identityProviderArnAccountID := identityProvideArnAccountIDRegex.FindStringSubmatch(strings.ToLower(identityProviderArnString)) // First element is full string - if len(match) != 2 { + if len(identityProviderArnAccountID) != 2 { if err != nil { return nil, fmt.Errorf("okta-aws-connector: error getting account id from identityProviderArn") } } - accountId := match[1] + accountId := identityProviderArnAccountID[1] oktaAWSAppSettings := &oktaAWSAppSettings{ JoinAllRoles: joinAllRolesBool, @@ -440,3 +440,41 @@ func (a *oktaAWSAppSettings) checkIfNotAppGroupFromCache(ctx context.Context, gr } return notAppGroup, nil } + +func (a *oktaAWSAppSettings) oktaAppGroup(ctx context.Context, appGroup *okta.ApplicationGroupAssignment) (*OktaAppGroupWrapper, error) { + oktaGroup, err := embeddedOktaGroupFromAppGroup(appGroup) + if err != nil { + return nil, err + } + + appGroupProfile, ok := appGroup.Profile.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("error converting app group profile '%s'", appGroup.Id) + } + + samlRoles := make([]string, 0) + accountId := a.IdentityProviderArnAccountID + var roleName string + matchesRolePattern := false + + if a.UseGroupMapping { + accountId, roleName, matchesRolePattern, err = parseAccountIDAndRoleFromGroupName(ctx, a.RoleRegex, oktaGroup.Profile.Name) + if err != nil { + return nil, err + } + if matchesRolePattern { + samlRoles = append(samlRoles, roleName) + } + } else { + samlRoles, err = getSAMLRoles(appGroupProfile) + if err != nil { + return nil, err + } + } + + return &OktaAppGroupWrapper{ + oktaGroup: oktaGroup, + samlRoles: samlRoles, + accountID: accountId, + }, nil +} diff --git a/pkg/connector/group.go b/pkg/connector/group.go index 78f224d9..1c2fcc01 100644 --- a/pkg/connector/group.go +++ b/pkg/connector/group.go @@ -190,7 +190,7 @@ func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pa groups := make([]*okta.Group, 0, len(appGroups)) for _, appGroup := range appGroups { - oktaAppGroup, err := o.oktaAppGroup(ctx, appGroup) + oktaAppGroup, err := awsConfig.oktaAppGroup(ctx, appGroup) if err != nil { return nil, nil, err } @@ -202,24 +202,7 @@ func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pa } func (o *groupResourceType) oktaAppGroup(ctx context.Context, appGroup *okta.ApplicationGroupAssignment) (*OktaAppGroupWrapper, error) { - embedded := appGroup.Embedded - if embedded == nil { - return nil, fmt.Errorf("app group '%s' embedded data was nil", appGroup.Id) - } - embeddedMap, ok := embedded.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("app group embedded data was not a map for group with id '%s'", appGroup.Id) - } - embeddedGroup, ok := embeddedMap["group"] - if !ok { - return nil, fmt.Errorf("embedded group data was nil for app group '%s'", appGroup.Id) - } - groupJSON, err := json.Marshal(embeddedGroup) - if err != nil { - return nil, fmt.Errorf("error marshalling embedded group data for app group '%s': %w", appGroup.Id, err) - } - oktaGroup := &okta.Group{} - err = json.Unmarshal(groupJSON, &oktaGroup) + oktaGroup, err := embeddedOktaGroupFromAppGroup(appGroup) if err != nil { return nil, fmt.Errorf("error unmarshalling embedded group data for app group '%s': %w", appGroup.Id, err) } @@ -275,27 +258,11 @@ func listApplicationGroupsHelper( groups := make([]*okta.Group, 0, len(appGroups)) for _, appGroup := range appGroups { - embedded := appGroup.Embedded - if embedded == nil { - return nil, nil, fmt.Errorf("app group '%s' embedded data was nil", appGroup.Id) - } - embeddedMap, ok := embedded.(map[string]interface{}) - if !ok { - return nil, nil, fmt.Errorf("app group embedded data was not a map for group with id '%s'", appGroup.Id) - } - embeddedGroup, ok := embeddedMap["group"] - if !ok { - return nil, nil, fmt.Errorf("embedded group data was nil for app group '%s'", appGroup.Id) - } - groupJSON, err := json.Marshal(embeddedGroup) - if err != nil { - return nil, nil, fmt.Errorf("error marshalling embedded group data for app group '%s': %w", appGroup.Id, err) - } - oktaGroup := &okta.Group{} - err = json.Unmarshal(groupJSON, &oktaGroup) + oktaGroup, err := embeddedOktaGroupFromAppGroup(appGroup) if err != nil { - return nil, nil, fmt.Errorf("error unmarshalling embedded group data for app group '%s': %w", appGroup.Id, err) + return nil, nil, err } + groups = append(groups, oktaGroup) } @@ -358,11 +325,6 @@ func (o *groupResourceType) groupResource(ctx context.Context, group *okta.Group annos.Update(&v2.V1Identifier{ Id: fmtResourceIdV1(group.Id), }) - if o.connector.awsConfig != nil && o.connector.awsConfig.Enabled { - annos.Update(&v2.ChildResourceType{ - ResourceTypeId: resourceTypeAccount.Id, - }) - } etagMd, err := o.etagMd(group) if err != nil { @@ -466,6 +428,31 @@ func (g *groupResourceType) Revoke(ctx context.Context, grant *v2.Grant) (annota return nil, nil } +func embeddedOktaGroupFromAppGroup(appGroup *okta.ApplicationGroupAssignment) (*okta.Group, error) { + embedded := appGroup.Embedded + if embedded == nil { + return nil, fmt.Errorf("app group '%s' embedded data was nil", appGroup.Id) + } + embeddedMap, ok := embedded.(map[string]interface{}) + if !ok { + return nil, fmt.Errorf("app group embedded data was not a map for group with id '%s'", appGroup.Id) + } + embeddedGroup, ok := embeddedMap["group"] + if !ok { + return nil, fmt.Errorf("embedded group data was nil for app group '%s'", appGroup.Id) + } + groupJSON, err := json.Marshal(embeddedGroup) + if err != nil { + return nil, fmt.Errorf("error marshalling embedded group data for app group '%s': %w", appGroup.Id, err) + } + oktaGroup := &okta.Group{} + err = json.Unmarshal(groupJSON, &oktaGroup) + if err != nil { + return nil, fmt.Errorf("error unmarshalling embedded group data for app group '%s': %w", appGroup.Id, err) + } + return oktaGroup, nil +} + func groupBuilder(connector *Okta) *groupResourceType { return &groupResourceType{ resourceType: resourceTypeGroup, diff --git a/pkg/connector/pagination.go b/pkg/connector/pagination.go index ba1d0cfa..6c1abd9a 100644 --- a/pkg/connector/pagination.go +++ b/pkg/connector/pagination.go @@ -11,6 +11,16 @@ import ( const defaultLimit = 100 +func parseGetResp(resp *okta.Response) (annotations.Annotations, error) { + var annos annotations.Annotations + if resp != nil { + if desc, err := extractRateLimitData(resp); err == nil { + annos.WithRateLimiting(desc) + } + } + return annos, nil +} + func parseResp(resp *okta.Response) (string, annotations.Annotations, error) { var annos annotations.Annotations var nextPage string From e9320ceab85dfbeb4764b5e8d27a26fc45bf7358 Mon Sep 17 00:00:00 2001 From: Lauren Leach Date: Thu, 17 Oct 2024 12:44:13 -0700 Subject: [PATCH 6/7] fix lint/add comments/address review comments --- cmd/baton-okta/config.go | 2 +- pkg/connector/aws_account.go | 10 +++--- pkg/connector/connector.go | 56 +++++++++++++++++----------------- pkg/connector/group.go | 59 +++++++++--------------------------- 4 files changed, 49 insertions(+), 78 deletions(-) diff --git a/cmd/baton-okta/config.go b/cmd/baton-okta/config.go index 4ec8b4eb..a387106b 100644 --- a/cmd/baton-okta/config.go +++ b/cmd/baton-okta/config.go @@ -20,7 +20,7 @@ var ( cacheTTL = field.IntField("cache-ttl", field.WithDescription("Response cache time to live in seconds"), field.WithDefaultValue(300)) awsIdentityCenterMode = field.BoolField("aws-identity-center-mode", field.WithDescription("Whether to run in AWS Identity center mode or not. In AWS mode, only samlRoles for groups and the users assigned to groups are synced")) - awsOktaAppId = field.StringField("aws-okta-app-id", field.WithDescription("The Okta app id for the AWS application")) + awsOktaAppId = field.StringField("aws-okta-app-id", field.WithDescription("The Okta app id for the AWS application")) ) var relationships = []field.SchemaFieldRelationship{ diff --git a/pkg/connector/aws_account.go b/pkg/connector/aws_account.go index 21af94d6..430f004a 100644 --- a/pkg/connector/aws_account.go +++ b/pkg/connector/aws_account.go @@ -196,7 +196,7 @@ func samlRoleEntitlement(resource *v2.Resource, role string) *v2.Entitlement { // Use expand grant if join all roles/use group mapping enabled to get user grants // Otherwise: // list application users, if direct assignment, give those role, if group scope, look at all the users groups -// if join all roles also do the above JUST for direct assignments +// if join all roles also do the above JUST for direct assignments. func (o *accountResourceType) Grants( ctx context.Context, resource *v2.Resource, @@ -210,8 +210,8 @@ func (o *accountResourceType) Grants( // TODO(lauren) what resource type should this be bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resource.Id.ResourceType}) - //bag, page, err := parsePageToken(token.Token, resource.Id) - //bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) + // bag, page, err := parsePageToken(token.Token, resource.Id) + // bag, page, err := parsePageToken(token.Token, &v2.ResourceId{ResourceType: resourceTypeUser.Id}) if err != nil { return nil, "", nil, fmt.Errorf("okta-aws-connector: failed to parse page token: %w", err) } @@ -266,7 +266,6 @@ func (o *accountResourceType) Grants( } } - // TODO(lauren) use ToSlice instead? for samlRole := range appUserSAMLRolesMap.Iterator().C { rv = append(rv, accountGrant(resource, samlRole, appUser.Id)) } @@ -382,6 +381,9 @@ func (o *accountResourceType) listAWSSamlRoles(ctx context.Context) (*AWSRoles, return nil, nil, err } respCtx, err := responseToContext(&pagination.Token{}, resp) + if err != nil { + return nil, nil, err + } return awsRoles, respCtx, nil } diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index 07ad9a52..f170ad38 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -19,6 +19,8 @@ import ( const awsApp = "amazon_aws" const ResourceNotFoundExceptionErrorCode = "E0000007" +const ExpectedIdentityProviderArnRegexCaptureGroups = 2 +const ExpectedGroupNameCaptureGroupsWithGroupFilterForMultipleAWSInstances = 3 type Okta struct { client *okta.Client @@ -34,8 +36,6 @@ type ciamConfig struct { EmailDomains []string } -// "groupFilter": "aws_(?{{accountid}}\\d+)_(?{{role}}[a-zA-Z0-9+=,.@\\-_]+)", -// arn:aws:iam::${accountid}:saml-provider/OKTA,arn:aws:iam::${accountid}:role/${role}" type awsConfig struct { Enabled bool OktaAppId string @@ -43,6 +43,26 @@ type awsConfig struct { oktaAWSAppSettings *oktaAWSAppSettings } +/* +JoinAllRoles: This option enables merging all available roles assigned to a user as follows: + +For example, if a user is directly assigned Role1 and Role2 (user to app assignment), +and the user belongs to group GroupAWS with RoleA and RoleB assigned (group to app assignment), then: + +Join all roles OFF: Role1 and Role2 are available upon login to AWS +Join all roles ON: Role1, Role2, RoleA, and RoleB are available upon login to AWS + +UseGroupMapping: Use Group Mapping enables CONNECT OKTA TO MULTIPLE AWS INSTANCES VIA USER GROUPS functionality. + +IdentityProviderArnRegex: Uses the "Role Value Pattern" to obtain a regular expression to extract the account id. +This is only used when UseGroupMapping is not enabled. + +Role Value Pattern: This field takes the AWS role and account ID captured within the syntax of your AWS role groups, +and translates it into the proper syntax AWS requires in Okta’s SAML assertion to allow users to view their accounts and roles when they sign in. + +This field should always follow this specific syntax: +arn:aws:iam::${accountid}:saml-provider/[SAML Provider Name],arn:aws:iam::${accountid}:role/${role} +*/ type oktaAWSAppSettings struct { JoinAllRoles bool IdentityProviderArn string @@ -368,7 +388,7 @@ func (c *Okta) getAWSApplicationConfig(ctx context.Context) (*oktaAWSAppSettings groupFilterRegex = strings.Replace(groupFilterRegex, `(?{{role}}`, `([a-zA-Z0-9+=,.@\\-_]+`, 1) // Unescape the groupFilterRegex regex string - roleRegex := strings.Replace(groupFilterRegex, `\\`, `\`, -1) + roleRegex := strings.ReplaceAll(groupFilterRegex, `\\`, `\`) // TODO(lauren) only do this if use group mapping not enabled? identityProvideArnAccountIDRegex, err := regexp.Compile(strings.ToLower(identityProviderRegex)) @@ -378,10 +398,8 @@ func (c *Okta) getAWSApplicationConfig(ctx context.Context) (*oktaAWSAppSettings identityProviderArnAccountID := identityProvideArnAccountIDRegex.FindStringSubmatch(strings.ToLower(identityProviderArnString)) // First element is full string - if len(identityProviderArnAccountID) != 2 { - if err != nil { - return nil, fmt.Errorf("okta-aws-connector: error getting account id from identityProviderArn") - } + if len(identityProviderArnAccountID) != ExpectedIdentityProviderArnRegexCaptureGroups { + return nil, fmt.Errorf("okta-aws-connector: error getting account id from identityProviderArn") } accountId := identityProviderArnAccountID[1] @@ -397,26 +415,6 @@ func (c *Okta) getAWSApplicationConfig(ctx context.Context) (*oktaAWSAppSettings return oktaAWSAppSettings, nil } -func (c *Okta) listAWSSamlRoles(ctx context.Context, appID string) (*AWSRoles, *responseContext, error) { - apiUrl := fmt.Sprintf("/api/v1/internal/apps/%s/types", appID) - - rq := c.client.CloneRequestExecutor() - - req, err := rq.WithAccept("application/json").WithContentType("application/json").NewRequest(http.MethodGet, apiUrl, nil) - if err != nil { - return nil, nil, err - } - - var awsRoles *AWSRoles - resp, err := rq.Do(ctx, req, &awsRoles) - if err != nil { - return nil, nil, err - } - respCtx, err := responseToContext(&pagination.Token{}, resp) - - return awsRoles, respCtx, nil -} - func (a *oktaAWSAppSettings) getAppGroupFromCache(ctx context.Context, groupId string) (*OktaAppGroupWrapper, error) { appGroupCacheVal, ok := a.appGroupCache.Load(groupId) if !ok { @@ -424,7 +422,7 @@ func (a *oktaAWSAppSettings) getAppGroupFromCache(ctx context.Context, groupId s } oktaAppGroup, ok := appGroupCacheVal.(*OktaAppGroupWrapper) if !ok { - return nil, fmt.Errorf("error converting app group '%s' from cache", oktaAppGroup) + return nil, fmt.Errorf("error converting app group '%s' from cache", groupId) } return oktaAppGroup, nil } @@ -436,7 +434,7 @@ func (a *oktaAWSAppSettings) checkIfNotAppGroupFromCache(ctx context.Context, gr } notAppGroup, ok := notAppGroupCacheVal.(bool) if !ok { - return false, fmt.Errorf("error converting not a app group bool '%s' ", notAppGroup) + return false, fmt.Errorf("error converting not a app group bool for group '%s' ", groupId) } return notAppGroup, nil } diff --git a/pkg/connector/group.go b/pkg/connector/group.go index 1c2fcc01..107aae6f 100644 --- a/pkg/connector/group.go +++ b/pkg/connector/group.go @@ -201,49 +201,6 @@ func (o *groupResourceType) listApplicationGroups(ctx context.Context, token *pa return groups, reqCtx, nil } -func (o *groupResourceType) oktaAppGroup(ctx context.Context, appGroup *okta.ApplicationGroupAssignment) (*OktaAppGroupWrapper, error) { - oktaGroup, err := embeddedOktaGroupFromAppGroup(appGroup) - if err != nil { - return nil, fmt.Errorf("error unmarshalling embedded group data for app group '%s': %w", appGroup.Id, err) - } - - appGroupProfile, ok := appGroup.Profile.(map[string]interface{}) - if !ok { - return nil, fmt.Errorf("error converting app group profile '%s'", appGroup.Id) - } - - awsAppSettings, err := o.connector.getAWSApplicationConfig(ctx) - if err != nil { - return nil, err - } - samlRoles := make([]string, 0) - accountId := awsAppSettings.IdentityProviderArnAccountID - var roleName string - matchesRolePattern := false - - if awsAppSettings.UseGroupMapping { - accountId, roleName, matchesRolePattern, err = parseAccountIDAndRoleFromGroupName(ctx, awsAppSettings.RoleRegex, oktaGroup.Profile.Name) - if err != nil { - return nil, err - } - if matchesRolePattern { - samlRoles = append(samlRoles, roleName) - } - } else { - samlRoles, err = getSAMLRoles(appGroupProfile) - if err != nil { - return nil, err - } - } - - return &OktaAppGroupWrapper{ - oktaGroup: oktaGroup, - samlRoles: samlRoles, - accountID: accountId, - }, nil -} - -// TODO(lauren) move shared code into helper func listApplicationGroupsHelper( ctx context.Context, client *okta.Client, @@ -269,6 +226,20 @@ func listApplicationGroupsHelper( return groups, reqCtx, nil } +/* +This filter field uses a regular expression to filter AWS-related groups and extract the accountid and role. + +If you use the default AWS role group syntax (aws#[account alias]#[role name]#[account #]), then you can use this Regex string: +^aws\#\S+\#(?{{role}}[\w\-]+)\#(?{{accountid}}\d+)$ + +This Regex expression logically equates to: +find groups that start with AWS, then #, then a string of text, then #, then the AWS role, then #, then the AWS account ID. + +You can also use this Regex expression: +aws_(?{{accountid}}\d+)_(?{{role}}[a-zA-Z0-9+=,.@\-_]+) +If you don't use a default Regex expression, create on that properly filters your AWS role groups. +The expression should capture the AWS role name and account ID within two distinct Regex groups named {{role}} and {{accountid}}. +*/ func parseAccountIDAndRoleFromGroupName(ctx context.Context, roleRegex string, groupName string) (string, string, bool, error) { // TODO(lauren) move to get app config re, err := regexp.Compile(roleRegex) @@ -276,7 +247,7 @@ func parseAccountIDAndRoleFromGroupName(ctx context.Context, roleRegex string, g return "", "", false, fmt.Errorf("error compiling regex '%s': %w", roleRegex, err) } match := re.FindStringSubmatch(groupName) - if len(match) != 3 { + if len(match) != ExpectedGroupNameCaptureGroupsWithGroupFilterForMultipleAWSInstances { return "", "", false, nil } // First element is full string From 76715bcb62ff6a7ae068a4a170c6e9156e1ea490 Mon Sep 17 00:00:00 2001 From: Lauren Leach Date: Thu, 17 Oct 2024 13:47:44 -0700 Subject: [PATCH 7/7] error check --- pkg/connector/connector.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index f170ad38..c742d6fb 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -318,9 +318,12 @@ func (c *Okta) getAWSApplicationConfig(ctx context.Context) (*oktaAWSAppSettings app, awsAppResp, err := c.client.Application.GetApplication(ctx, c.awsConfig.OktaAppId, okta.NewApplication(), nil) if err != nil { - return nil, fmt.Errorf("okta-connector: verify failed to fetch aws app: %w", err) + return nil, fmt.Errorf("okta-aws-connector: verify failed to fetch aws app: %w", err) } awsAppRespCtx, err := responseToContext(&pagination.Token{}, awsAppResp) + if err != nil { + return nil, fmt.Errorf("okta-aws-connector: verify failed to convert get aws app response: %w", err) + } if awsAppRespCtx.OktaResponse.StatusCode != http.StatusOK { err := fmt.Errorf("okta-connector: verify returned non-200 for aws app: '%d'", awsAppRespCtx.OktaResponse.StatusCode) return nil, err