Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,81 @@
package userroles
import (
"context"
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/qsfera/server/pkg/middleware"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/settings/pkg/store/defaults"
"github.com/opencloud-eu/reva/v2/pkg/utils"
"go-micro.dev/v4/metadata"
)
type defaultRoleAssigner struct {
Options
}
// NewDefaultRoleAssigner returns an implementation of the UserRoleAssigner interface
func NewDefaultRoleAssigner(opts ...Option) UserRoleAssigner {
opt := Options{}
for _, o := range opts {
o(&opt)
}
return defaultRoleAssigner{
Options: opt,
}
}
// UpdateUserRoleAssignment assigns the role "User" to the supplied user. Unless the user
// already has a different role assigned.
func (d defaultRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]any) (*cs3.User, error) {
var roleIDs []string
if user.Id.Type != cs3.UserType_USER_TYPE_LIGHTWEIGHT {
var err error
roleIDs, err = loadRolesIDs(ctx, user.Id.OpaqueId, d.roleService)
if err != nil {
d.logger.Error().Err(err).Msg("Could not load roles")
return nil, err
}
if len(roleIDs) == 0 {
// This user doesn't have a role assignment yet. Assign a
// default user role. At least until proper roles are provided. See
// https://github.com/owncloud/ocis/issues/1825 for more context.
if user.Id.Type == cs3.UserType_USER_TYPE_PRIMARY || user.Id.Type == cs3.UserType_USER_TYPE_GUEST {
roleId := defaults.BundleUUIDRoleUser
if user.Id.Type == cs3.UserType_USER_TYPE_GUEST {
roleId = defaults.BundleUUIDRoleGuest
}
d.logger.Info().Str("userid", user.Id.OpaqueId).Msg("user has no role assigned, assigning default user role")
ctx = metadata.Set(ctx, middleware.AccountID, user.Id.OpaqueId)
_, err := d.roleService.AssignRoleToUser(ctx, &settingssvc.AssignRoleToUserRequest{
AccountUuid: user.Id.OpaqueId,
RoleId: roleId,
})
if err != nil {
d.logger.Error().Err(err).Msg("Could not add default role")
return nil, err
}
roleIDs = append(roleIDs, roleId)
}
}
}
user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDs)
return user, nil
}
// ApplyUserRole it looks up the user's role in the settings service and adds it
// user's opaque data
func (d defaultRoleAssigner) ApplyUserRole(ctx context.Context, user *cs3.User) (*cs3.User, error) {
roleIDs, err := loadRolesIDs(ctx, user.Id.OpaqueId, d.roleService)
if err != nil {
d.logger.Error().Err(err).Msg("Could not load roles")
return nil, err
}
user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDs)
return user, nil
}
@@ -0,0 +1,181 @@
// Code generated by mockery; DO NOT EDIT.
// github.com/vektra/mockery
// template: testify
package mocks
import (
"context"
"github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
mock "github.com/stretchr/testify/mock"
)
// NewUserRoleAssigner creates a new instance of UserRoleAssigner. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewUserRoleAssigner(t interface {
mock.TestingT
Cleanup(func())
}) *UserRoleAssigner {
mock := &UserRoleAssigner{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
// UserRoleAssigner is an autogenerated mock type for the UserRoleAssigner type
type UserRoleAssigner struct {
mock.Mock
}
type UserRoleAssigner_Expecter struct {
mock *mock.Mock
}
func (_m *UserRoleAssigner) EXPECT() *UserRoleAssigner_Expecter {
return &UserRoleAssigner_Expecter{mock: &_m.Mock}
}
// ApplyUserRole provides a mock function for the type UserRoleAssigner
func (_mock *UserRoleAssigner) ApplyUserRole(ctx context.Context, user *userv1beta1.User) (*userv1beta1.User, error) {
ret := _mock.Called(ctx, user)
if len(ret) == 0 {
panic("no return value specified for ApplyUserRole")
}
var r0 *userv1beta1.User
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *userv1beta1.User) (*userv1beta1.User, error)); ok {
return returnFunc(ctx, user)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *userv1beta1.User) *userv1beta1.User); ok {
r0 = returnFunc(ctx, user)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*userv1beta1.User)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *userv1beta1.User) error); ok {
r1 = returnFunc(ctx, user)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UserRoleAssigner_ApplyUserRole_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ApplyUserRole'
type UserRoleAssigner_ApplyUserRole_Call struct {
*mock.Call
}
// ApplyUserRole is a helper method to define mock.On call
// - ctx context.Context
// - user *userv1beta1.User
func (_e *UserRoleAssigner_Expecter) ApplyUserRole(ctx interface{}, user interface{}) *UserRoleAssigner_ApplyUserRole_Call {
return &UserRoleAssigner_ApplyUserRole_Call{Call: _e.mock.On("ApplyUserRole", ctx, user)}
}
func (_c *UserRoleAssigner_ApplyUserRole_Call) Run(run func(ctx context.Context, user *userv1beta1.User)) *UserRoleAssigner_ApplyUserRole_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *userv1beta1.User
if args[1] != nil {
arg1 = args[1].(*userv1beta1.User)
}
run(
arg0,
arg1,
)
})
return _c
}
func (_c *UserRoleAssigner_ApplyUserRole_Call) Return(user1 *userv1beta1.User, err error) *UserRoleAssigner_ApplyUserRole_Call {
_c.Call.Return(user1, err)
return _c
}
func (_c *UserRoleAssigner_ApplyUserRole_Call) RunAndReturn(run func(ctx context.Context, user *userv1beta1.User) (*userv1beta1.User, error)) *UserRoleAssigner_ApplyUserRole_Call {
_c.Call.Return(run)
return _c
}
// UpdateUserRoleAssignment provides a mock function for the type UserRoleAssigner
func (_mock *UserRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *userv1beta1.User, claims map[string]any) (*userv1beta1.User, error) {
ret := _mock.Called(ctx, user, claims)
if len(ret) == 0 {
panic("no return value specified for UpdateUserRoleAssignment")
}
var r0 *userv1beta1.User
var r1 error
if returnFunc, ok := ret.Get(0).(func(context.Context, *userv1beta1.User, map[string]any) (*userv1beta1.User, error)); ok {
return returnFunc(ctx, user, claims)
}
if returnFunc, ok := ret.Get(0).(func(context.Context, *userv1beta1.User, map[string]any) *userv1beta1.User); ok {
r0 = returnFunc(ctx, user, claims)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*userv1beta1.User)
}
}
if returnFunc, ok := ret.Get(1).(func(context.Context, *userv1beta1.User, map[string]any) error); ok {
r1 = returnFunc(ctx, user, claims)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UserRoleAssigner_UpdateUserRoleAssignment_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateUserRoleAssignment'
type UserRoleAssigner_UpdateUserRoleAssignment_Call struct {
*mock.Call
}
// UpdateUserRoleAssignment is a helper method to define mock.On call
// - ctx context.Context
// - user *userv1beta1.User
// - claims map[string]any
func (_e *UserRoleAssigner_Expecter) UpdateUserRoleAssignment(ctx interface{}, user interface{}, claims interface{}) *UserRoleAssigner_UpdateUserRoleAssignment_Call {
return &UserRoleAssigner_UpdateUserRoleAssignment_Call{Call: _e.mock.On("UpdateUserRoleAssignment", ctx, user, claims)}
}
func (_c *UserRoleAssigner_UpdateUserRoleAssignment_Call) Run(run func(ctx context.Context, user *userv1beta1.User, claims map[string]any)) *UserRoleAssigner_UpdateUserRoleAssignment_Call {
_c.Call.Run(func(args mock.Arguments) {
var arg0 context.Context
if args[0] != nil {
arg0 = args[0].(context.Context)
}
var arg1 *userv1beta1.User
if args[1] != nil {
arg1 = args[1].(*userv1beta1.User)
}
var arg2 map[string]any
if args[2] != nil {
arg2 = args[2].(map[string]any)
}
run(
arg0,
arg1,
arg2,
)
})
return _c
}
func (_c *UserRoleAssigner_UpdateUserRoleAssignment_Call) Return(user1 *userv1beta1.User, err error) *UserRoleAssigner_UpdateUserRoleAssignment_Call {
_c.Call.Return(user1, err)
return _c
}
func (_c *UserRoleAssigner_UpdateUserRoleAssignment_Call) RunAndReturn(run func(ctx context.Context, user *userv1beta1.User, claims map[string]any) (*userv1beta1.User, error)) *UserRoleAssigner_UpdateUserRoleAssignment_Call {
_c.Call.Return(run)
return _c
}
@@ -0,0 +1,224 @@
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
}
@@ -0,0 +1,120 @@
package userroles
import (
"encoding/json"
"testing"
)
func TestExtractRolesArray(t *testing.T) {
byt := []byte(`{"roles":["a","b"]}`)
claims := map[string]any{}
err := json.Unmarshal(byt, &claims)
if err != nil {
t.Fatal(err)
}
roles, err := extractRoles("roles", claims)
if err != nil {
t.Fatal(err)
}
if _, ok := roles["a"]; !ok {
t.Fatal("must contain 'a'")
}
if _, ok := roles["b"]; !ok {
t.Fatal("must contain 'b'")
}
}
func TestExtractRolesString(t *testing.T) {
byt := []byte(`{"roles":"a"}`)
claims := map[string]any{}
err := json.Unmarshal(byt, &claims)
if err != nil {
t.Fatal(err)
}
roles, err := extractRoles("roles", claims)
if err != nil {
t.Fatal(err)
}
if _, ok := roles["a"]; !ok {
t.Fatal("must contain 'a'")
}
}
func TestExtractRolesPathArray(t *testing.T) {
byt := []byte(`{"sub":{"roles":["a","b"]}}`)
claims := map[string]any{}
err := json.Unmarshal(byt, &claims)
if err != nil {
t.Fatal(err)
}
roles, err := extractRoles("sub.roles", claims)
if err != nil {
t.Fatal(err)
}
if _, ok := roles["a"]; !ok {
t.Fatal("must contain 'a'")
}
if _, ok := roles["b"]; !ok {
t.Fatal("must contain 'b'")
}
}
func TestExtractRolesPathString(t *testing.T) {
byt := []byte(`{"sub":{"roles":"a"}}`)
claims := map[string]any{}
err := json.Unmarshal(byt, &claims)
if err != nil {
t.Fatal(err)
}
roles, err := extractRoles("sub.roles", claims)
if err != nil {
t.Fatal(err)
}
if _, ok := roles["a"]; !ok {
t.Fatal("must contain 'a'")
}
}
func TestExtractEscapedRolesPathString(t *testing.T) {
byt := []byte(`{"sub.roles":"a"}`)
claims := map[string]any{}
err := json.Unmarshal(byt, &claims)
if err != nil {
t.Fatal(err)
}
roles, err := extractRoles("sub\\.roles", claims)
if err != nil {
t.Fatal(err)
}
if _, ok := roles["a"]; !ok {
t.Fatal("must contain 'a'")
}
}
func TestNoRoles(t *testing.T) {
byt := []byte(`{"sub":{"foo":"a"}}`)
claims := map[string]any{}
err := json.Unmarshal(byt, &claims)
if err != nil {
t.Fatal(err)
}
roles, err := extractRoles("sub.roles", claims)
if err == nil {
t.Fatal("must not find a role")
}
if len(roles) != 0 {
t.Fatal("length of roles mut be 0")
}
}
@@ -0,0 +1,96 @@
package userroles
import (
"context"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"github.com/qsfera/server/pkg/log"
settingssvc "github.com/qsfera/server/protogen/gen/qsfera/services/settings/v0"
"github.com/qsfera/server/services/proxy/pkg/config"
"github.com/opencloud-eu/reva/v2/pkg/rgrpc/todo/pool"
)
// UserRoleAssigner allows providing different implementations for how users get their default roles
// assigned by the proxy during authentication
type UserRoleAssigner interface {
// UpdateUserRoleAssignment is called by the account resolver middleware. It updates the user's role assignment
// based on the user's (OIDC) claims. It adds the user's roles to the opaque data of the cs3.User struct
UpdateUserRoleAssignment(ctx context.Context, user *cs3.User, claims map[string]any) (*cs3.User, error)
// ApplyUserRole can be called by proxy middlewares, it looks up the user's roles and adds them
// the users "roles" key in the user's opaque data
ApplyUserRole(ctx context.Context, user *cs3.User) (*cs3.User, error)
}
// Options defines the available options for this package.
type Options struct {
gatewaySelector pool.Selectable[gateway.GatewayAPIClient]
roleService settingssvc.RoleService
rolesClaim string
roleMapping []config.RoleMapping
serviceAccount config.ServiceAccount
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 КуСфера role names to claims values
func WithRoleMapping(roleMap []config.RoleMapping) Option {
return func(o *Options) {
o.roleMapping = roleMap
}
}
// WithRevaGatewaySelector set the gatewaySelector option
func WithRevaGatewaySelector(selectable pool.Selectable[gateway.GatewayAPIClient]) Option {
return func(o *Options) {
o.gatewaySelector = selectable
}
}
// WithServiceAccount configures the service account creator to use
func WithServiceAccount(c config.ServiceAccount) Option {
return func(o *Options) {
o.serviceAccount = 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}
assignmentResponse, err := rs.ListRoleAssignments(ctx, req)
if err != nil {
return nil, err
}
roleIDs := make([]string, 0)
for _, assignment := range assignmentResponse.Assignments {
roleIDs = append(roleIDs, assignment.RoleId)
}
return roleIDs, nil
}