diff --git a/services/graph/pkg/service/v0/users.go b/services/graph/pkg/service/v0/users.go index 5d4beb6c46c..5817f7fb1db 100644 --- a/services/graph/pkg/service/v0/users.go +++ b/services/graph/pkg/service/v0/users.go @@ -24,7 +24,6 @@ import ( settings "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0" "github.com/owncloud/ocis/v2/services/graph/pkg/identity" "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" - settingssvc "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0" "golang.org/x/exp/slices" ) @@ -317,20 +316,21 @@ func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) { return } + // TODO make this configurable, role assignments might be coming from the IDP // assign roles if possible - if g.roleService != nil { - // All users get the user role by default currently. - // to all new users for now, as create Account request does not have any role field - if _, err = g.roleService.AssignRoleToUser(r.Context(), &settings.AssignRoleToUserRequest{ - AccountUuid: *u.Id, - RoleId: settingssvc.BundleUUIDRoleUser, - }); err != nil { - // log as error, admin eventually needs to do something - logger.Error().Err(err).Str("id", *u.Id).Str("role", settingssvc.BundleUUIDRoleUser).Msg("could not create user: role assignment failed") - errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "role assignment failed") - return - } - } + // if g.roleService != nil { + // // All users get the user role by default currently. + // // to all new users for now, as create Account request does not have any role field + // if _, err = g.roleService.AssignRoleToUser(r.Context(), &settings.AssignRoleToUserRequest{ + // AccountUuid: *u.Id, + // RoleId: settingssvc.BundleUUIDRoleUser, + // }); err != nil { + // // log as error, admin eventually needs to do something + // logger.Error().Err(err).Str("id", *u.Id).Str("role", settingssvc.BundleUUIDRoleUser).Msg("could not create user: role assignment failed") + // errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "role assignment failed") + // return + // } + // } e := events.UserCreated{UserID: *u.Id} if currentUser, ok := revactx.ContextGetUser(r.Context()); ok { diff --git a/services/proxy/pkg/middleware/account_resolver.go b/services/proxy/pkg/middleware/account_resolver.go index 581b46a1204..8ae4aeb66ab 100644 --- a/services/proxy/pkg/middleware/account_resolver.go +++ b/services/proxy/pkg/middleware/account_resolver.go @@ -25,17 +25,20 @@ func AccountResolver(optionSetters ...Option) func(next http.Handler) http.Handl userOIDCClaim: options.UserOIDCClaim, userCS3Claim: options.UserCS3Claim, autoProvisionAccounts: options.AutoprovisionAccounts, + // TODO turn this into configuration + roleAssignmentFromOIDCClaims: true, } } } type accountResolver struct { - next http.Handler - logger log.Logger - userProvider backend.UserBackend - autoProvisionAccounts bool - userOIDCClaim string - userCS3Claim string + next http.Handler + logger log.Logger + userProvider backend.UserBackend + autoProvisionAccounts bool + roleAssignmentFromOIDCClaims bool + userOIDCClaim string + userCS3Claim string } // TODO do not use the context to store values: https://medium.com/@cep21/how-to-correctly-use-context-context-in-go-1-7-8f2c0fafdf39 @@ -94,12 +97,21 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - // resolve the user's roles - user, err = m.userProvider.GetUserRoles(ctx, user) - if err != nil { - m.logger.Error().Err(err).Msg("Could not get user roles") - w.WriteHeader(http.StatusInternalServerError) - return + if m.roleAssignmentFromOIDCClaims { + user, err = m.userProvider.UpdateRoleAssignmentsFromClaims(ctx, user, claims) + if err != nil { + m.logger.Error().Err(err).Msg("Could not get user roles") + w.WriteHeader(http.StatusInternalServerError) + return + } + } else { + // resolve the user's roles + user, err = m.userProvider.GetUserRoles(ctx, user) + if err != nil { + m.logger.Error().Err(err).Msg("Could not get user roles") + w.WriteHeader(http.StatusInternalServerError) + return + } } // add user to context for selectors diff --git a/services/proxy/pkg/user/backend/backend.go b/services/proxy/pkg/user/backend/backend.go index 34f0c4b5fa6..8f767085045 100644 --- a/services/proxy/pkg/user/backend/backend.go +++ b/services/proxy/pkg/user/backend/backend.go @@ -27,6 +27,7 @@ type UserBackend interface { GetUserRoles(ctx context.Context, user *cs3.User) (*cs3.User, error) Authenticate(ctx context.Context, username string, password string) (*cs3.User, string, error) CreateUserFromClaims(ctx context.Context, claims map[string]interface{}) (*cs3.User, error) + UpdateRoleAssignmentsFromClaims(ctx context.Context, user *cs3.User, claims map[string]interface{}) (*cs3.User, error) } // RevaAuthenticator helper interface to mock auth-method from reva gateway-client. diff --git a/services/proxy/pkg/user/backend/cs3.go b/services/proxy/pkg/user/backend/cs3.go index 3c5dd833e63..65f9aa312ed 100644 --- a/services/proxy/pkg/user/backend/cs3.go +++ b/services/proxy/pkg/user/backend/cs3.go @@ -3,6 +3,7 @@ package backend import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -185,6 +186,132 @@ func (c *cs3backend) Authenticate(ctx context.Context, username string, password return res.User, res.Token, nil } +// UpdateRoleAssignmentsFromClaims updates an authenticated user's role +// assignments in the settings service based on the roles listed in the user's +// claims. +func (c *cs3backend) UpdateRoleAssignmentsFromClaims(ctx context.Context, user *cs3.User, claims map[string]interface{}) (*cs3.User, error) { + // TODO turn this into configuration + roleMapping := map[string]string{ + "ocisAdmin": "admin", + "ocisSpaceAdmin": "spaceadmin", + "ocisUser": "user", + "ocisGuest": "guest", + } + rolesClaim := "roles" + + roleNameToId := map[string]string{} + + // 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 := context.Background() + autoProvisionUser, err := getAutoProvisionUserCreator() + if err != nil { + return nil, err + } + token, err := c.generateAutoProvisionAdminToken(newctx) + if err != nil { + c.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)) + + // Get all roles to find the role IDs. + // TODO: we need to cache this. Roles IDs change rarely and this is a pretty expensive call + req := &settingssvc.ListBundlesRequest{} + res, err := c.roleService.ListRoles(newctx, req) + for _, role := range res.Bundles { + c.logger.Error().Str("role", role.Name).Str("id", role.Id).Msg("Got Role") + roleNameToId[role.Name] = role.Id + } + + if err != nil { + c.logger.Error().Err(err).Msg("Error listing roles.") + return nil, err + } + + var roleIdsFromClaim []string + c.logger.Error().Interface("rolesclaim", claims[rolesClaim]).Msg("Got ClaimRoles") + if claimRoles, ok := claims[rolesClaim].([]interface{}); ok { + for _, cri := range claimRoles { + cr, ok := cri.(string) + if !ok { + err := errors.New("invalid role in claims") + c.logger.Error().Err(err).Interface("claim value", cri).Msg("Is not a valid string.") + return nil, err + } + ocisRole, ok := roleMapping[cr] + if !ok { + c.logger.Error().Str("role", cr).Msg("Skipping unmaped role from claims.") + continue + } + id, ok := roleNameToId[ocisRole] + if !ok { + err := errors.New("ocis role does not exist") + c.logger.Error().Err(err).Str("ocisRole", ocisRole).Msg("Error mapping role to id") + return nil, err + } + roleIdsFromClaim = append(roleIdsFromClaim, id) + } + } else { + c.logger.Error().Err(err).Msg("No roles in user claims.") + return nil, err + } + c.logger.Error().Interface("roleIds", roleIdsFromClaim).Msg("Mapped roles from claim") + switch len(roleIdsFromClaim) { + default: + err := errors.New("too many roles found in claims") + c.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") + c.logger.Error().Err(err).Msg("") + return nil, err + case 1: + // exactly one mapping. This is right + break + } + + assignedRoles, err := loadRolesIDs(newctx, user.GetId().GetOpaqueId(), c.roleService) + if err != nil { + c.logger.Error().Err(err).Msgf("Could not load roles") + return nil, err + } + if len(assignedRoles) > 1 { + err := errors.New("too many roles assigned") + c.logger.Error().Err(err).Msg("The user has too many roles assigned") + return nil, err + } + c.logger.Error().Interface("assignedRoleIds", assignedRoles).Msg("Currently assigned roles") + if len(assignedRoles) == 0 || (assignedRoles[0] != roleIdsFromClaim[0]) { + if _, err = c.roleService.AssignRoleToUser(newctx, &settingssvc.AssignRoleToUserRequest{ + AccountUuid: user.GetId().GetOpaqueId(), + RoleId: roleIdsFromClaim[0], + }); err != nil { + c.logger.Error().Err(err).Msg("Role assignment failed") + return nil, err + } + } + enc, err := encodeRoleIDs(roleIdsFromClaim) + if err != nil { + c.logger.Error().Err(err).Msg("Could not encode loaded roles") + return nil, err + } + + if user.Opaque == nil { + user.Opaque = &types.Opaque{ + Map: map[string]*types.OpaqueEntry{ + "roles": enc, + }, + } + } else { + user.Opaque.Map["roles"] = enc + } + + return user, nil +} + // CreateUserFromClaims creates a new user via libregraph users API, taking the // attributes from the provided `claims` map. On success it returns the new // user. If the user already exist this is not considered an error and the