package userroles import ( "context" "errors" "sync" "time" cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" "github.com/qsfera/server/pkg/middleware" "github.com/qsfera/server/pkg/oidc" settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0" "github.com/opencloud-eu/reva/v2/pkg/utils" "go-micro.dev/v4/metadata" ) type oidcRoleAssigner struct { Options } // NewOIDCRoleAssigner returns an implementation of the UserRoleAssigner interface func NewOIDCRoleAssigner(opts ...Option) UserRoleAssigner { opt := Options{} for _, o := range opts { o(&opt) } return oidcRoleAssigner{ Options: opt, } } func extractRoles(rolesClaim string, claims map[string]any) (map[string]struct{}, error) { claimRoles := map[string]struct{}{} // happy path value, _ := claims[rolesClaim].(string) if value != "" { claimRoles[value] = struct{}{} return claimRoles, nil } claim, err := oidc.WalkSegments(oidc.SplitWithEscaping(rolesClaim, ".", "\\"), claims) if err != nil { return nil, err } switch v := claim.(type) { case []string: for _, cr := range v { claimRoles[cr] = struct{}{} } case []any: for _, cri := range v { cr, ok := cri.(string) if !ok { err := errors.New("invalid role in claims") return nil, err } claimRoles[cr] = struct{}{} } case string: claimRoles[v] = struct{}{} default: return nil, errors.New("no roles in user claims") } return claimRoles, nil } // 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]any) (*cs3.User, error) { logger := ra.logger.SubloggerWithRequestID(ctx).With().Str("userid", user.GetId().GetOpaqueId()).Logger() roleNamesToRoleIDs, err := ra.roleNamesToRoleIDs() if err != nil { logger.Error().Err(err).Msg("Error mapping role names to role ids") return nil, err } claimRoles, err := extractRoles(ra.rolesClaim, claims) if err != nil { logger.Error().Err(err).Msg("Error mapping role names to role ids") return nil, err } if len(claimRoles) == 0 { err := errors.New("no roles set in claim") logger.Error().Err(err).Msg("") return nil, err } // the roleMapping config is supposed to have the role mappings ordered from the highest privileged role // down to the lowest privileged role. Since КуСфера currently only can handle a single role assignment we // pick the highest privileged role that matches a value from the claims roleIDFromClaim := "" for _, mapping := range ra.Options.roleMapping { if _, ok := claimRoles[mapping.ClaimValue]; ok { logger.Debug().Str("qsferaRole", mapping.RoleName).Str("role id", roleNamesToRoleIDs[mapping.RoleName]).Msg("first matching role") roleIDFromClaim = roleNamesToRoleIDs[mapping.RoleName] break } } if roleIDFromClaim == "" { err := errors.New("no role in claim maps to an КуСфера role") logger.Error().Err(err).Msg("") return nil, err } assignedRoles, err := loadRolesIDs(ctx, user.GetId().GetOpaqueId(), ra.roleService) if err != nil { logger.Error().Err(err).Msg("Could not load roles") return nil, err } if len(assignedRoles) > 1 { logger.Error().Str("userID", user.GetId().GetOpaqueId()).Int("numRoles", len(assignedRoles)).Msg("The user has too many roles assigned") } logger.Debug().Interface("assignedRoleIds", assignedRoles).Msg("Currently assigned roles") if len(assignedRoles) != 1 || (assignedRoles[0] != roleIDFromClaim) { logger.Debug().Interface("assignedRoleIds", assignedRoles).Interface("newRoleId", roleIDFromClaim).Msg("Updating role assignment for user") newctx, err := ra.prepareAdminContext() if err != nil { logger.Error().Err(err).Msg("Error creating admin context") return nil, err } if _, err = ra.roleService.AssignRoleToUser(newctx, &settingssvc.AssignRoleToUserRequest{ AccountUuid: user.GetId().GetOpaqueId(), RoleId: roleIDFromClaim, }); err != nil { logger.Error().Err(err).Msg("Role assignment failed") return nil, err } } user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", []string{roleIDFromClaim}) 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).Msg("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) { gatewayClient, err := ra.gatewaySelector.Next() if err != nil { ra.logger.Error().Err(err).Msg("could not select next gateway client") return nil, err } newctx, err := utils.GetServiceUserContext(ra.serviceAccount.ServiceAccountID, gatewayClient, ra.serviceAccount.ServiceAccountSecret) if err != nil { ra.logger.Error().Err(err).Msg("Error preparing request context for provisioning role assignments.") return nil, err } newctx = metadata.Set(newctx, middleware.AccountID, ra.serviceAccount.ServiceAccountID) return newctx, nil } type roleNameToIDCache struct { roleNameToID map[string]string lastRead time.Time lock sync.RWMutex } var roleNameToID roleNameToIDCache func (ra oidcRoleAssigner) roleNamesToRoleIDs() (map[string]string, error) { cacheTTL := 5 * time.Minute roleNameToID.lock.RLock() if !roleNameToID.lastRead.IsZero() && time.Since(roleNameToID.lastRead) < cacheTTL { defer roleNameToID.lock.RUnlock() return roleNameToID.roleNameToID, nil } ra.logger.Debug().Msg("refreshing roles ids") // cache needs Refresh get a write lock roleNameToID.lock.RUnlock() roleNameToID.lock.Lock() defer roleNameToID.lock.Unlock() // check again, another goroutine might have updated while we "upgraded" the lock if !roleNameToID.lastRead.IsZero() && time.Since(roleNameToID.lastRead) < cacheTTL { return roleNameToID.roleNameToID, nil } // Get all roles to find the role IDs. // To list roles we need some elevated access to the settings service // prepare a new request context for that until we have service accounts ctx, err := ra.prepareAdminContext() if err != nil { ra.logger.Error().Err(err).Msg("Error creating admin context") return nil, err } 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 map[string]string{}, err } newIDs := map[string]string{} for _, role := range res.Bundles { ra.logger.Debug().Str("role", role.Name).Str("id", role.Id).Msg("Got Role") newIDs[role.Name] = role.Id } ra.logger.Debug().Interface("roleMap", newIDs).Msg("Role Name to role ID map") roleNameToID.roleNameToID = newIDs roleNameToID.lastRead = time.Now() return roleNameToID.roleNameToID, nil }