From a448c75c759954e18a1226fd616f7c4bafdd8897 Mon Sep 17 00:00:00 2001 From: Ralf Haferkamp Date: Wed, 15 Mar 2023 18:59:25 +0100 Subject: [PATCH] Extract role assignments from claims Add a UserRoleAssigner implementation that extract role names from the users' claims and creates role assignments in the settings service based on a configured mapping of claim values to ocis role names. Closes: #5669 --- services/proxy/pkg/autoprovision/creator.go | 97 +++++++++++ services/proxy/pkg/command/server.go | 45 +++-- services/proxy/pkg/config/config.go | 13 ++ .../pkg/config/defaults/defaultconfig.go | 15 +- services/proxy/pkg/user/backend/cs3.go | 77 ++------- services/proxy/pkg/userroles/oidcroles.go | 162 ++++++++++++++++++ services/proxy/pkg/userroles/userroles.go | 34 +++- 7 files changed, 361 insertions(+), 82 deletions(-) create mode 100644 services/proxy/pkg/autoprovision/creator.go create mode 100644 services/proxy/pkg/userroles/oidcroles.go diff --git a/services/proxy/pkg/autoprovision/creator.go b/services/proxy/pkg/autoprovision/creator.go new file mode 100644 index 000000000..7714d78c1 --- /dev/null +++ b/services/proxy/pkg/autoprovision/creator.go @@ -0,0 +1,97 @@ +package autoprovision + +import ( + "context" + "encoding/json" + + cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" + "github.com/cs3org/reva/v2/pkg/auth/scope" + "github.com/cs3org/reva/v2/pkg/token" + settingsService "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0" +) + +// Creator provides an interface to get a user or reva token with admin privileges +type Creator interface { + // GetAutoProvisionAdmin returns a user with the Admin role assigned + GetAutoProvisionAdmin() (*cs3.User, error) + // GetAutoProvisionAdminToken returns a reva token with admin privileges + GetAutoProvisionAdminToken(ctx context.Context) (string, error) +} + +// Options defines the available options for this package. +type Options struct { + tokenManager token.Manager +} + +// Option defines a single option function. +type Option func(o *Options) + +// WithTokenManager sets the reva token manager +func WithTokenManager(t token.Manager) Option { + return func(o *Options) { + o.tokenManager = t + } +} + +type creator struct { + Options +} + +// NewCreator returns a new Creator instance +func NewCreator(opts ...Option) creator { + opt := Options{} + for _, o := range opts { + o(&opt) + } + + return creator{ + Options: opt, + } +} + +// This returns an hardcoded internal User, that is privileged to create new User via +// the Graph API. This user is needed for autoprovisioning of users from incoming OIDC +// claims. +func (c creator) GetAutoProvisionAdmin() (*cs3.User, error) { + roleIDsJSON, err := json.Marshal([]string{settingsService.BundleUUIDRoleAdmin}) + if err != nil { + return nil, err + } + + autoProvisionUserCreator := &cs3.User{ + DisplayName: "Autoprovision User", + Username: "autoprovisioner", + Id: &cs3.UserId{ + Idp: "internal", + OpaqueId: "autoprov-user-id00-0000-000000000000", + }, + Opaque: &types.Opaque{ + Map: map[string]*types.OpaqueEntry{ + "roles": { + Decoder: "json", + Value: roleIDsJSON, + }, + }, + }, + } + return autoProvisionUserCreator, nil +} + +func (c creator) GetAutoProvisionAdminToken(ctx context.Context) (string, error) { + userCreator, err := c.GetAutoProvisionAdmin() + if err != nil { + return "", err + } + + s, err := scope.AddOwnerScope(nil) + if err != nil { + return "", err + } + + token, err := c.tokenManager.MintToken(ctx, userCreator, s) + if err != nil { + return "", err + } + return token, nil +} diff --git a/services/proxy/pkg/command/server.go b/services/proxy/pkg/command/server.go index 0f887ce5f..d386ab495 100644 --- a/services/proxy/pkg/command/server.go +++ b/services/proxy/pkg/command/server.go @@ -20,6 +20,7 @@ import ( "github.com/owncloud/ocis/v2/ocis-pkg/version" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0" + "github.com/owncloud/ocis/v2/services/proxy/pkg/autoprovision" "github.com/owncloud/ocis/v2/services/proxy/pkg/config" "github.com/owncloud/ocis/v2/services/proxy/pkg/config/parser" "github.com/owncloud/ocis/v2/services/proxy/pkg/logging" @@ -137,32 +138,50 @@ func Server(cfg *config.Config) *cli.Command { func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) alice.Chain { rolesClient := settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient()) revaClient, err := pool.GetGatewayServiceClient(cfg.Reva.Address, cfg.Reva.GetRevaOptions()...) + if err != nil { + logger.Fatal().Err(err).Msg("Failed to get gateway client") + } + tokenManager, err := jwt.New(map[string]interface{}{ + "secret": cfg.TokenManager.JWTSecret, + }) + if err != nil { + logger.Fatal().Err(err). + Msg("Failed to create token manager") + } + autoProvsionCreator := autoprovision.NewCreator(autoprovision.WithTokenManager(tokenManager)) var userProvider backend.UserBackend switch cfg.AccountBackend { case "cs3": - tokenManager, err := jwt.New(map[string]interface{}{ - "secret": cfg.TokenManager.JWTSecret, - }) - if err != nil { - logger.Error().Err(err). - Msg("Failed to create token manager") - } userProvider = backend.NewCS3UserBackend( backend.WithLogger(logger), backend.WithRevaAuthenticator(revaClient), backend.WithMachineAuthAPIKey(cfg.MachineAuthAPIKey), backend.WithOIDCissuer(cfg.OIDC.Issuer), - backend.WithTokenManager(tokenManager), + backend.WithAutoProvisonCreator(autoProvsionCreator), ) default: logger.Fatal().Msgf("Invalid accounts backend type '%s'", cfg.AccountBackend) } - roleAssigner := userroles.NewDefaultRoleAssigner( - userroles.WithRoleService(rolesClient), - userroles.WithLogger(logger), - ) + var roleAssigner userroles.UserRoleAssigner + switch cfg.RoleAssignment.Driver { + case "default": + roleAssigner = userroles.NewDefaultRoleAssigner( + userroles.WithRoleService(rolesClient), + userroles.WithLogger(logger), + ) + case "oidc": + roleAssigner = userroles.NewOIDCRoleAssigner( + userroles.WithRoleService(rolesClient), + userroles.WithLogger(logger), + userroles.WithRolesClaim(cfg.RoleAssignment.OIDCRoleMapper.RoleClaim), + userroles.WithRoleMapping(cfg.RoleAssignment.OIDCRoleMapper.RoleMapping), + userroles.WithAutoProvisonCreator(autoProvsionCreator), + ) + default: + logger.Fatal().Msgf("Invalid role assignment driver '%s'", cfg.RoleAssignment.Driver) + } storeClient := storesvc.NewStoreService("com.owncloud.api.store", grpc.DefaultClient()) if err != nil { @@ -243,7 +262,6 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) middleware.Logger(logger), middleware.UserProvider(userProvider), middleware.UserRoleAssigner(roleAssigner), - middleware.TokenManagerConfig(*cfg.TokenManager), middleware.UserOIDCClaim(cfg.UserOIDCClaim), middleware.UserCS3Claim(cfg.UserCS3Claim), middleware.AutoprovisionAccounts(cfg.AutoprovisionAccounts), @@ -256,7 +274,6 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) // finally, trigger home creation when a user logs in middleware.CreateHome( middleware.Logger(logger), - middleware.TokenManagerConfig(*cfg.TokenManager), middleware.RevaGatewayClient(revaClient), middleware.RoleQuotas(cfg.RoleQuotas), ), diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go index 554caebcc..a0e531260 100644 --- a/services/proxy/pkg/config/config.go +++ b/services/proxy/pkg/config/config.go @@ -25,6 +25,7 @@ type Config struct { Policies []Policy `yaml:"policies"` OIDC OIDC `yaml:"oidc"` TokenManager *TokenManager `mask:"struct" yaml:"token_manager"` + RoleAssignment RoleAssignment `yaml:"role_assignment"` PolicySelector *PolicySelector `yaml:"policy_selector"` PreSignedURL PreSignedURL `yaml:"pre_signed_url"` AccountBackend string `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE" desc:"Account backend the PROXY service should use. Currently only 'cs3' is possible here."` @@ -121,6 +122,18 @@ type UserinfoCache struct { TTL int `yaml:"ttl" env:"PROXY_OIDC_USERINFO_CACHE_TTL" desc:"Max TTL in seconds for the OIDC user info cache."` } +// RoleAssignment contains the configuration for how to assign roles to users during login +type RoleAssignment struct { + Driver string `yaml:"driver" env:"PROXY_ROLE_ASSIGNMENT_DRIVER" desc:"The mechanism that should be used to assign roles to user upon login. Supported values: 'default' or 'oidc'. 'default' will assign the role 'user' to users which don't have a role assigned at the time they login. 'oidc' will assign the role based on the value of a claim (configured via PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM) from the users OIDC claims."` + OIDCRoleMapper OIDCRoleMapper `yaml:"oidc_role_mapper"` +} + +// OIDCRoleMapper contains the configuration for the "oidc" role assignment driber +type OIDCRoleMapper struct { + RoleClaim string `yaml:"role_claim" env:"PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM" desc:"The OIDC claim used to create the users role assignment."` + RoleMapping map[string]string `yaml:"role_mapping" desc:"A mapping of ocis role names to PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM claim values. This setting can only be configured in the configuration file and not via environment variables."` +} + // PolicySelector is the toplevel-configuration for different selectors type PolicySelector struct { Static *StaticSelectorConf `yaml:"static"` diff --git a/services/proxy/pkg/config/defaults/defaultconfig.go b/services/proxy/pkg/config/defaults/defaultconfig.go index f1316cf91..c7166fb9a 100644 --- a/services/proxy/pkg/config/defaults/defaultconfig.go +++ b/services/proxy/pkg/config/defaults/defaultconfig.go @@ -52,7 +52,20 @@ func DefaultConfig() *config.Config { }, }, PolicySelector: nil, - Reva: shared.DefaultRevaConfig(), + RoleAssignment: config.RoleAssignment{ + Driver: "default", + // this default is only relevant when Driver is set to "oidc" + OIDCRoleMapper: config.OIDCRoleMapper{ + RoleClaim: "roles", + RoleMapping: map[string]string{ + "admin": "ocisAdmin", + "spaceadmin": "ocisSpaceAdmin", + "user": "ocisUser", + "guest": "ocisGuest", + }, + }, + }, + Reva: shared.DefaultRevaConfig(), PreSignedURL: config.PreSignedURL{ AllowedHTTPMethods: []string{"GET"}, Enabled: true, diff --git a/services/proxy/pkg/user/backend/cs3.go b/services/proxy/pkg/user/backend/cs3.go index 500996e34..03d3cd64d 100644 --- a/services/proxy/pkg/user/backend/cs3.go +++ b/services/proxy/pkg/user/backend/cs3.go @@ -10,16 +10,13 @@ import ( gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" - types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" - "github.com/cs3org/reva/v2/pkg/auth/scope" revactx "github.com/cs3org/reva/v2/pkg/ctx" - "github.com/cs3org/reva/v2/pkg/token" libregraph "github.com/owncloud/libre-graph-api-go" "github.com/owncloud/ocis/v2/ocis-pkg/log" "github.com/owncloud/ocis/v2/ocis-pkg/oidc" "github.com/owncloud/ocis/v2/ocis-pkg/registry" "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" - settingsService "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0" + "github.com/owncloud/ocis/v2/services/proxy/pkg/autoprovision" "go-micro.dev/v4/selector" ) @@ -33,11 +30,11 @@ type Option func(o *Options) // Options defines the available options for this package. type Options struct { - logger log.Logger - tokenManager token.Manager - authProvider RevaAuthenticator - machineAuthAPIKey string - oidcISS string + logger log.Logger + authProvider RevaAuthenticator + machineAuthAPIKey string + oidcISS string + autoProvsionCreator autoprovision.Creator } func WithLogger(l log.Logger) Option { @@ -46,12 +43,6 @@ func WithLogger(l log.Logger) Option { } } -func WithTokenManager(t token.Manager) Option { - return func(o *Options) { - o.tokenManager = t - } -} - func WithRevaAuthenticator(ra RevaAuthenticator) Option { return func(o *Options) { o.authProvider = ra @@ -70,6 +61,12 @@ func WithOIDCissuer(oidcISS string) Option { } } +func WithAutoProvisonCreator(c autoprovision.Creator) Option { + return func(o *Options) { + o.autoProvsionCreator = c + } +} + // NewCS3UserBackend creates a user-provider which fetches users from a CS3 UserBackend func NewCS3UserBackend(opts ...Option) UserBackend { opt := Options{} @@ -133,7 +130,7 @@ func (c *cs3backend) Authenticate(ctx context.Context, username string, password // function will just return the existing user. func (c *cs3backend) CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*cs3.User, error) { newctx := context.Background() - token, err := c.generateAutoProvisionAdminToken(newctx) + token, err := c.autoProvsionCreator.GetAutoProvisionAdminToken(newctx) if err != nil { c.logger.Error().Err(err).Msg("Error generating token for autoprovisioning user.") return nil, err @@ -266,51 +263,3 @@ func (c cs3backend) cs3UserFromLibregraph(ctx context.Context, lu *libregraph.Us cs3user.Mail = lu.GetMail() return cs3user } - -// This returns an hardcoded internal User, that is privileged to create new User via -// the Graph API. This user is needed for autoprovisioning of users from incoming OIDC -// claims. -func getAutoProvisionUserCreator() (*cs3.User, error) { - roleIDsJSON, err := json.Marshal([]string{settingsService.BundleUUIDRoleAdmin}) - if err != nil { - return nil, err - } - - autoProvisionUserCreator := &cs3.User{ - DisplayName: "Autoprovision User", - Username: "autoprovisioner", - Id: &cs3.UserId{ - Idp: "internal", - OpaqueId: "autoprov-user-id00-0000-000000000000", - }, - Opaque: &types.Opaque{ - Map: map[string]*types.OpaqueEntry{ - "roles": &types.OpaqueEntry{ - Decoder: "json", - Value: roleIDsJSON, - }, - }, - }, - } - return autoProvisionUserCreator, nil -} - -func (c cs3backend) generateAutoProvisionAdminToken(ctx context.Context) (string, error) { - userCreator, err := getAutoProvisionUserCreator() - if err != nil { - return "", err - } - - s, err := scope.AddOwnerScope(nil) - if err != nil { - c.logger.Error().Err(err).Msg("could not get owner scope") - return "", err - } - - token, err := c.tokenManager.MintToken(ctx, userCreator, s) - if err != nil { - c.logger.Error().Err(err).Msg("could not mint token") - return "", err - } - return token, nil -} diff --git a/services/proxy/pkg/userroles/oidcroles.go b/services/proxy/pkg/userroles/oidcroles.go new file mode 100644 index 000000000..2b8b5b713 --- /dev/null +++ b/services/proxy/pkg/userroles/oidcroles.go @@ -0,0 +1,162 @@ +package userroles + +import ( + "context" + "errors" + + cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" + revactx "github.com/cs3org/reva/v2/pkg/ctx" + "github.com/cs3org/reva/v2/pkg/utils" + "github.com/owncloud/ocis/v2/ocis-pkg/middleware" + settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + "go-micro.dev/v4/metadata" +) + +type oidcRoleAssigner struct { + Options +} + +// NewOIDCRoleAssigner returns an implemenation of the UserRoleAssigner interface +func NewOIDCRoleAssigner(opts ...Option) UserRoleAssigner { + opt := Options{} + for _, o := range opts { + o(&opt) + } + + return oidcRoleAssigner{ + Options: opt, + } +} + +// UpdateUserRoleAssignment assigns the role "User" to the supplied user. Unless the user +// already has a different role assigned. +func (ra oidcRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]interface{}) (*cs3.User, error) { + + // To list roles and update assignment we need some elevated access to the settings service + // prepare a new request context for that until we have service accounts + newctx, err := ra.prepareAdminContext() + if err != nil { + ra.logger.Error().Err(err).Msg("Error creating admin context") + return nil, err + } + + claimValueToRoleID, err := ra.oidcClaimvaluesToRoleIDs(newctx) + if err != nil { + ra.logger.Error().Err(err).Msg("Error mapping claims to roles ids") + return nil, err + } + + roleIDsFromClaim := make([]string, 0, 1) + ra.logger.Error().Interface("rolesclaim", claims[ra.rolesClaim]).Msg("Got ClaimRoles") + claimRoles, ok := claims[ra.rolesClaim].([]interface{}) + if !ok { + ra.logger.Error().Err(err).Msg("No roles in user claims.") + return nil, err + } + for _, cri := range claimRoles { + cr, ok := cri.(string) + if !ok { + err := errors.New("invalid role in claims") + ra.logger.Error().Err(err).Interface("claim value", cri).Msg("Is not a valid string.") + return nil, err + } + id, ok := claimValueToRoleID[cr] + if !ok { + ra.logger.Error().Str("role", cr).Msg("Skipping unmaped role from claims.") + continue + } + roleIDsFromClaim = append(roleIDsFromClaim, id) + } + ra.logger.Error().Interface("roleIDs", roleIDsFromClaim).Msg("Mapped roles from claim") + + switch len(roleIDsFromClaim) { + default: + err := errors.New("too many roles found in claims") + ra.logger.Error().Err(err).Msg("Only one role per user is allowed.") + return nil, err + case 0: + err := errors.New("no role in claim, maps to a ocis role") + ra.logger.Error().Err(err).Msg("") + return nil, err + case 1: + // exactly one mapping. This is right + } + + assignedRoles, err := loadRolesIDs(newctx, user.GetId().GetOpaqueId(), ra.roleService) + if err != nil { + ra.logger.Error().Err(err).Msgf("Could not load roles") + return nil, err + } + if len(assignedRoles) > 1 { + err := errors.New("too many roles assigned") + ra.logger.Error().Err(err).Msg("The user has too many roles assigned") + return nil, err + } + ra.logger.Error().Interface("assignedRoleIds", assignedRoles).Msg("Currently assigned roles") + if len(assignedRoles) == 0 || (assignedRoles[0] != roleIDsFromClaim[0]) { + if _, err = ra.roleService.AssignRoleToUser(newctx, &settingssvc.AssignRoleToUserRequest{ + AccountUuid: user.GetId().GetOpaqueId(), + RoleId: roleIDsFromClaim[0], + }); err != nil { + ra.logger.Error().Err(err).Msg("Role assignment failed") + return nil, err + } + } + + user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDsFromClaim) + return user, nil +} + +// ApplyUserRole it looks up the user's role in the settings service and adds it +// user's opaque data +func (ra oidcRoleAssigner) ApplyUserRole(ctx context.Context, user *cs3.User) (*cs3.User, error) { + roleIDs, err := loadRolesIDs(ctx, user.Id.OpaqueId, ra.roleService) + if err != nil { + ra.logger.Error().Err(err).Msgf("Could not load roles") + return nil, err + } + + user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDs) + return user, nil +} + +func (ra oidcRoleAssigner) prepareAdminContext() (context.Context, error) { + newctx := context.Background() + autoProvisionUser, err := ra.autoProvsionCreator.GetAutoProvisionAdmin() + if err != nil { + return nil, err + } + token, err := ra.autoProvsionCreator.GetAutoProvisionAdminToken(newctx) + if err != nil { + ra.logger.Error().Err(err).Msg("Error generating token for provisioning role assignments.") + return nil, err + } + newctx = revactx.ContextSetToken(newctx, token) + newctx = metadata.Set(newctx, middleware.AccountID, autoProvisionUser.Id.OpaqueId) + newctx = metadata.Set(newctx, middleware.RoleIDs, string(autoProvisionUser.Opaque.Map["roles"].Value)) + return newctx, nil +} + +func (ra oidcRoleAssigner) oidcClaimvaluesToRoleIDs(ctx context.Context) (map[string]string, error) { + roleClaimToID := map[string]string{} + // Get all roles to find the role IDs. + // TODO: we need to cache this. Roles IDs change rarely and this is a pretty expensiveV call + req := &settingssvc.ListBundlesRequest{} + res, err := ra.roleService.ListRoles(ctx, req) + if err != nil { + ra.logger.Error().Err(err).Msg("Failed to list all roles") + return roleClaimToID, err + } + + for _, role := range res.Bundles { + ra.logger.Error().Str("role", role.Name).Str("id", role.Id).Msg("Got Role") + roleClaim, ok := ra.roleMapping[role.Name] + if !ok { + err := errors.New("Incomplete role mapping") + ra.logger.Error().Err(err).Str("role", role.Name).Msg("Role not mapped to a claim value") + return roleClaimToID, err + } + roleClaimToID[roleClaim] = role.Id + } + return roleClaimToID, nil +} diff --git a/services/proxy/pkg/userroles/userroles.go b/services/proxy/pkg/userroles/userroles.go index c2c54b5bd..8c2dd431f 100644 --- a/services/proxy/pkg/userroles/userroles.go +++ b/services/proxy/pkg/userroles/userroles.go @@ -6,6 +6,7 @@ import ( cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" "github.com/owncloud/ocis/v2/ocis-pkg/log" settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" + "github.com/owncloud/ocis/v2/services/proxy/pkg/autoprovision" ) //go:generate mockery --name=UserRoleAssigner @@ -21,26 +22,53 @@ type UserRoleAssigner interface { ApplyUserRole(ctx context.Context, user *cs3.User) (*cs3.User, error) } +// Options defines the available options for this package. type Options struct { - roleService settingssvc.RoleService - rolesClaim string - logger log.Logger + roleService settingssvc.RoleService + rolesClaim string + roleMapping map[string]string + autoProvsionCreator autoprovision.Creator + logger log.Logger } +// Option defines a single option function. type Option func(o *Options) +// WithLogger configure the logger func WithLogger(l log.Logger) Option { return func(o *Options) { o.logger = l } } +// WithRoleService sets the roleservice instance to use func WithRoleService(rs settingssvc.RoleService) Option { return func(o *Options) { o.roleService = rs } } +// WithRolesClaim sets the OIDC claim for looking up role names +func WithRolesClaim(claim string) Option { + return func(o *Options) { + o.rolesClaim = claim + } +} + +// WithRoleMapping configures the map of ocis role names to claims values +func WithRoleMapping(roleMap map[string]string) Option { + return func(o *Options) { + o.roleMapping = roleMap + } +} + +// WithAutoProvisonCreator configures the autoprovision creator to use +func WithAutoProvisonCreator(c autoprovision.Creator) Option { + return func(o *Options) { + o.autoProvsionCreator = c + } +} + // loadRolesIDs returns the role-ids assigned to an user func loadRolesIDs(ctx context.Context, opaqueUserID string, rs settingssvc.RoleService) ([]string, error) { req := &settingssvc.ListRoleAssignmentsRequest{AccountUuid: opaqueUserID}