proxy: Add an interface for user role assignment

This moves the lookup and the creation of the users' role assignemt out
of the user backend into its own interface. This makes the user backend
a bit simpler and allows to provide different implemenation for the user
role assignment more easily.
This commit is contained in:
Ralf Haferkamp
2023-03-15 17:20:14 +01:00
committed by Ralf Haferkamp
parent 490a835a3a
commit d57d52b33d
11 changed files with 250 additions and 122 deletions
+8 -1
View File
@@ -31,6 +31,7 @@ import (
proxyHTTP "github.com/owncloud/ocis/v2/services/proxy/pkg/server/http"
"github.com/owncloud/ocis/v2/services/proxy/pkg/tracing"
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
"github.com/urfave/cli/v2"
"golang.org/x/oauth2"
)
@@ -149,7 +150,6 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
userProvider = backend.NewCS3UserBackend(
backend.WithLogger(logger),
backend.WithRoleService(rolesClient),
backend.WithRevaAuthenticator(revaClient),
backend.WithMachineAuthAPIKey(cfg.MachineAuthAPIKey),
backend.WithOIDCissuer(cfg.OIDC.Issuer),
@@ -159,6 +159,11 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
logger.Fatal().Msgf("Invalid accounts backend type '%s'", cfg.AccountBackend)
}
roleAssigner := userroles.NewDefaultRoleAssigner(
userroles.WithRoleService(rolesClient),
userroles.WithLogger(logger),
)
storeClient := storesvc.NewStoreService("com.owncloud.api.store", grpc.DefaultClient())
if err != nil {
logger.Error().Err(err).
@@ -210,6 +215,7 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
Logger: logger,
PreSignedURLConfig: cfg.PreSignedURL,
UserProvider: userProvider,
UserRoleAssigner: roleAssigner,
Store: storeClient,
})
@@ -236,6 +242,7 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
middleware.AccountResolver(
middleware.Logger(logger),
middleware.UserProvider(userProvider),
middleware.UserRoleAssigner(roleAssigner),
middleware.TokenManagerConfig(*cfg.TokenManager),
middleware.UserOIDCClaim(cfg.UserOIDCClaim),
middleware.UserCS3Claim(cfg.UserCS3Claim),
@@ -5,6 +5,7 @@ import (
"net/http"
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
revactx "github.com/cs3org/reva/v2/pkg/ctx"
"github.com/owncloud/ocis/v2/ocis-pkg/log"
@@ -24,6 +25,7 @@ func AccountResolver(optionSetters ...Option) func(next http.Handler) http.Handl
userProvider: options.UserProvider,
userOIDCClaim: options.UserOIDCClaim,
userCS3Claim: options.UserCS3Claim,
userRoleAssigner: options.UserRoleAssigner,
autoProvisionAccounts: options.AutoprovisionAccounts,
}
}
@@ -33,6 +35,7 @@ type accountResolver struct {
next http.Handler
logger log.Logger
userProvider backend.UserBackend
userRoleAssigner userroles.UserRoleAssigner
autoProvisionAccounts bool
userOIDCClaim string
userCS3Claim string
@@ -95,7 +98,7 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
}
// resolve the user's roles
user, err = m.userProvider.GetUserRoles(ctx, user)
user, err = m.userRoleAssigner.UpdateUserRoleAssignment(ctx, user, claims)
if err != nil {
m.logger.Error().Err(err).Msg("Could not get user roles")
w.WriteHeader(http.StatusInternalServerError)
@@ -15,6 +15,7 @@ import (
"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend/mocks"
userRoleMocks "github.com/owncloud/ocis/v2/services/proxy/pkg/userroles/mocks"
"github.com/stretchr/testify/assert"
"github.com/test-go/testify/mock"
)
@@ -124,9 +125,13 @@ func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr
ub.On("GetUserByClaims", mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(userBackendResult, token, userBackendErr)
ub.On("GetUserRoles", mock.Anything, mock.Anything).Return(userBackendResult, nil)
ra := userRoleMocks.UserRoleAssigner{}
ra.On("UpdateUserRoleAssignment", mock.Anything, mock.Anything, mock.Anything).Return(userBackendResult, nil)
return AccountResolver(
Logger(log.NewLogger()),
UserProvider(&ub),
UserRoleAssigner(&ra),
TokenManagerConfig(config.TokenManager{JWTSecret: "secret"}),
UserOIDCClaim(oidcclaim),
UserCS3Claim(cs3claim),
+11 -1
View File
@@ -5,6 +5,7 @@ import (
"time"
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
@@ -27,8 +28,10 @@ type Options struct {
PolicySelector config.PolicySelector
// HTTPClient to use for communication with the oidcAuth provider
HTTPClient *http.Client
// UP
// UserProvider backend to use for resolving User
UserProvider backend.UserBackend
// UserRoleAssigner to user for assign a users default role
UserRoleAssigner userroles.UserRoleAssigner
// SettingsRoleService for the roles API in settings
SettingsRoleService settingssvc.RoleService
// OIDCProviderFunc to lazily initialize an oidc provider, must be set for the oidc_auth middleware
@@ -202,6 +205,13 @@ func UserProvider(up backend.UserBackend) Option {
}
}
// UserRoleAssigner sets the mechanism for assigning the default user roles
func UserRoleAssigner(ra userroles.UserRoleAssigner) Option {
return func(o *Options) {
o.UserRoleAssigner = ra
}
}
// AccessTokenVerifyMethod set the mechanism for access token verification
func AccessTokenVerifyMethod(method string) Option {
return func(o *Options) {
@@ -17,6 +17,7 @@ import (
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
"github.com/owncloud/ocis/v2/services/proxy/pkg/user/backend"
"github.com/owncloud/ocis/v2/services/proxy/pkg/userroles"
"golang.org/x/crypto/pbkdf2"
)
@@ -43,6 +44,7 @@ type SignedURLAuthenticator struct {
Logger log.Logger
PreSignedURLConfig config.PreSignedURL
UserProvider backend.UserBackend
UserRoleAssigner userroles.UserRoleAssigner
Store storesvc.StoreService
}
@@ -202,7 +204,7 @@ func (m SignedURLAuthenticator) Authenticate(r *http.Request) (*http.Request, bo
return nil, false
}
user, err = m.UserProvider.GetUserRoles(r.Context(), user)
user, err = m.UserRoleAssigner.ApplyUserRole(r.Context(), user)
if err != nil {
m.Logger.Error().
Err(err).
@@ -2,13 +2,10 @@ package backend
import (
"context"
"encoding/json"
"errors"
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"google.golang.org/grpc"
)
@@ -26,7 +23,6 @@ var (
// UserBackend allows the proxy to retrieve users from different user-backends (accounts-service, CS3)
type UserBackend interface {
GetUserByClaims(ctx context.Context, claim, value string) (*cs3.User, string, error)
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)
}
@@ -35,34 +31,3 @@ type UserBackend interface {
type RevaAuthenticator interface {
Authenticate(ctx context.Context, in *gateway.AuthenticateRequest, opts ...grpc.CallOption) (*gateway.AuthenticateResponse, error)
}
// 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
}
// encodeRoleIDs encoded the given role id's in to reva-specific format to be able to mint a token from them
func encodeRoleIDs(roleIDs []string) (*types.OpaqueEntry, error) {
roleIDsJSON, err := json.Marshal(roleIDs)
if err != nil {
return nil, err
}
return &types.OpaqueEntry{
Decoder: "json",
Value: roleIDsJSON,
}, nil
}
+5 -60
View File
@@ -16,13 +16,10 @@ import (
"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/middleware"
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
"github.com/owncloud/ocis/v2/ocis-pkg/registry"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
"github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode"
settingsService "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
"go-micro.dev/v4/metadata"
"go-micro.dev/v4/selector"
)
@@ -38,7 +35,6 @@ type Option func(o *Options)
type Options struct {
logger log.Logger
tokenManager token.Manager
roleService settingssvc.RoleService
authProvider RevaAuthenticator
machineAuthAPIKey string
oidcISS string
@@ -56,12 +52,6 @@ func WithTokenManager(t token.Manager) Option {
}
}
func WithRoleService(rs settingssvc.RoleService) Option {
return func(o *Options) {
o.roleService = rs
}
}
func WithRevaAuthenticator(ra RevaAuthenticator) Option {
return func(o *Options) {
o.authProvider = ra
@@ -120,54 +110,6 @@ func (c *cs3backend) GetUserByClaims(ctx context.Context, claim, value string) (
return user, res.Token, nil
}
func (c *cs3backend) GetUserRoles(ctx context.Context, user *cs3.User) (*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, c.roleService)
if err != nil {
c.logger.Error().Err(err).Msgf("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/v2/issues/1825 for more context.
if user.Id.Type == cs3.UserType_USER_TYPE_PRIMARY {
c.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 := c.roleService.AssignRoleToUser(ctx, &settingssvc.AssignRoleToUserRequest{
AccountUuid: user.Id.OpaqueId,
RoleId: settingsService.BundleUUIDRoleUser,
})
if err != nil {
c.logger.Error().Err(err).Msg("Could not add default role")
return nil, err
}
roleIDs = append(roleIDs, settingsService.BundleUUIDRoleUser)
}
}
}
enc, err := encodeRoleIDs(roleIDs)
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
}
func (c *cs3backend) Authenticate(ctx context.Context, username string, password string) (*cs3.User, string, error) {
res, err := c.authProvider.Authenticate(ctx, &gateway.AuthenticateRequest{
Type: "basic",
@@ -329,7 +271,7 @@ func (c cs3backend) cs3UserFromLibregraph(ctx context.Context, lu *libregraph.Us
// the Graph API. This user is needed for autoprovisioning of users from incoming OIDC
// claims.
func getAutoProvisionUserCreator() (*cs3.User, error) {
encRoleID, err := encodeRoleIDs([]string{settingsService.BundleUUIDRoleAdmin})
roleIDsJSON, err := json.Marshal([]string{settingsService.BundleUUIDRoleAdmin})
if err != nil {
return nil, err
}
@@ -343,7 +285,10 @@ func getAutoProvisionUserCreator() (*cs3.User, error) {
},
Opaque: &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"roles": encRoleID,
"roles": &types.OpaqueEntry{
Decoder: "json",
Value: roleIDsJSON,
},
},
},
}
@@ -97,29 +97,6 @@ func (_m *UserBackend) GetUserByClaims(ctx context.Context, claim string, value
return r0, r1, r2
}
// GetUserRoles provides a mock function with given fields: ctx, user
func (_m *UserBackend) GetUserRoles(ctx context.Context, user *userv1beta1.User) (*userv1beta1.User, error) {
ret := _m.Called(ctx, user)
var r0 *userv1beta1.User
if rf, ok := ret.Get(0).(func(context.Context, *userv1beta1.User) *userv1beta1.User); ok {
r0 = rf(ctx, user)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*userv1beta1.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *userv1beta1.User) error); ok {
r1 = rf(ctx, user)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewUserBackend interface {
mock.TestingT
Cleanup(func())
@@ -0,0 +1,77 @@
package userroles
import (
"context"
cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
"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"
settingsService "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
"go-micro.dev/v4/metadata"
)
type defaultRoleAssigner struct {
Options
}
// NewDefaultRoleAssigner returns an implemenation 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]interface{}) (*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).Msgf("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/v2/issues/1825 for more context.
if user.Id.Type == cs3.UserType_USER_TYPE_PRIMARY {
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: settingsService.BundleUUIDRoleUser,
})
if err != nil {
d.logger.Error().Err(err).Msg("Could not add default role")
return nil, err
}
roleIDs = append(roleIDs, settingsService.BundleUUIDRoleUser)
}
}
}
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).Msgf("Could not load roles")
return nil, err
}
user.Opaque = utils.AppendJSONToOpaque(user.Opaque, "roles", roleIDs)
return user, nil
}
@@ -0,0 +1,77 @@
// Code generated by mockery v2.14.0. DO NOT EDIT.
package mocks
import (
context "context"
mock "github.com/stretchr/testify/mock"
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
)
// UserRoleAssigner is an autogenerated mock type for the UserRoleAssigner type
type UserRoleAssigner struct {
mock.Mock
}
// ApplyUserRole provides a mock function with given fields: ctx, user
func (_m *UserRoleAssigner) ApplyUserRole(ctx context.Context, user *userv1beta1.User) (*userv1beta1.User, error) {
ret := _m.Called(ctx, user)
var r0 *userv1beta1.User
if rf, ok := ret.Get(0).(func(context.Context, *userv1beta1.User) *userv1beta1.User); ok {
r0 = rf(ctx, user)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*userv1beta1.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *userv1beta1.User) error); ok {
r1 = rf(ctx, user)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// UpdateUserRoleAssignment provides a mock function with given fields: ctx, user, claims
func (_m *UserRoleAssigner) UpdateUserRoleAssignment(ctx context.Context, user *userv1beta1.User, claims map[string]interface{}) (*userv1beta1.User, error) {
ret := _m.Called(ctx, user, claims)
var r0 *userv1beta1.User
if rf, ok := ret.Get(0).(func(context.Context, *userv1beta1.User, map[string]interface{}) *userv1beta1.User); ok {
r0 = rf(ctx, user, claims)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*userv1beta1.User)
}
}
var r1 error
if rf, ok := ret.Get(1).(func(context.Context, *userv1beta1.User, map[string]interface{}) error); ok {
r1 = rf(ctx, user, claims)
} else {
r1 = ret.Error(1)
}
return r0, r1
}
type mockConstructorTestingTNewUserRoleAssigner interface {
mock.TestingT
Cleanup(func())
}
// 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.
func NewUserRoleAssigner(t mockConstructorTestingTNewUserRoleAssigner) *UserRoleAssigner {
mock := &UserRoleAssigner{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}
+60
View File
@@ -0,0 +1,60 @@
package userroles
import (
"context"
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"
)
//go:generate mockery --name=UserRoleAssigner
// UserRoleAssigner allows to provide different implemenation 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]interface{}) (*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)
}
type Options struct {
roleService settingssvc.RoleService
rolesClaim string
logger log.Logger
}
type Option func(o *Options)
func WithLogger(l log.Logger) Option {
return func(o *Options) {
o.logger = l
}
}
func WithRoleService(rs settingssvc.RoleService) Option {
return func(o *Options) {
o.roleService = rs
}
}
// 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
}