Files
opencloud/services/proxy/pkg/userroles/oidcroles.go
2025-01-21 11:16:38 +01:00

225 lines
7.0 KiB
Go

package userroles
import (
"context"
"errors"
"sync"
"time"
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/opencloud-eu/opencloud/pkg/middleware"
"github.com/opencloud-eu/opencloud/pkg/oidc"
settingssvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/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]interface{}) (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 []interface{}:
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]interface{}) (*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 OpenCloud 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("opencloudRole", 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 OpenCloud 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
}