Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proxy: Assign user roles from OIDC claim #5784

Merged
merged 9 commits into from
Mar 20, 2023
97 changes: 97 additions & 0 deletions services/proxy/pkg/autoprovision/creator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package autoprovision

import (
"context"
"encoding/json"

cs3 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
types "github.com/cs3org/go-cs3apis/cs3/types/v1beta1"
"github.com/cs3org/reva/v2/pkg/auth/scope"
"github.com/cs3org/reva/v2/pkg/token"
settingsService "github.com/owncloud/ocis/v2/services/settings/pkg/service/v0"
)

// Creator provides an interface to get a user or reva token with admin privileges
type Creator interface {
// GetAutoProvisionAdmin returns a user with the Admin role assigned
GetAutoProvisionAdmin() (*cs3.User, error)
// GetAutoProvisionAdminToken returns a reva token with admin privileges
GetAutoProvisionAdminToken(ctx context.Context) (string, error)
}

// Options defines the available options for this package.
type Options struct {
tokenManager token.Manager
}

// Option defines a single option function.
type Option func(o *Options)

// WithTokenManager sets the reva token manager
func WithTokenManager(t token.Manager) Option {
return func(o *Options) {
o.tokenManager = t
}
}

type creator struct {
Options
}

// NewCreator returns a new Creator instance
func NewCreator(opts ...Option) creator {
opt := Options{}
for _, o := range opts {
o(&opt)
}

return creator{
Options: opt,
}
}

// This returns an hardcoded internal User, that is privileged to create new User via
// the Graph API. This user is needed for autoprovisioning of users from incoming OIDC
// claims.
func (c creator) GetAutoProvisionAdmin() (*cs3.User, error) {
roleIDsJSON, err := json.Marshal([]string{settingsService.BundleUUIDRoleAdmin})
if err != nil {
return nil, err
}

autoProvisionUserCreator := &cs3.User{
DisplayName: "Autoprovision User",
Username: "autoprovisioner",
Id: &cs3.UserId{
Idp: "internal",
OpaqueId: "autoprov-user-id00-0000-000000000000",
},
Opaque: &types.Opaque{
Map: map[string]*types.OpaqueEntry{
"roles": {
Decoder: "json",
Value: roleIDsJSON,
},
},
},
}
return autoProvisionUserCreator, nil
}

func (c creator) GetAutoProvisionAdminToken(ctx context.Context) (string, error) {
userCreator, err := c.GetAutoProvisionAdmin()
if err != nil {
return "", err
}

s, err := scope.AddOwnerScope(nil)
if err != nil {
return "", err
}

token, err := c.tokenManager.MintToken(ctx, userCreator, s)
if err != nil {
return "", err
}
return token, nil
}
52 changes: 41 additions & 11 deletions services/proxy/pkg/command/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"github.com/owncloud/ocis/v2/ocis-pkg/version"
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
storesvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/store/v0"
"github.com/owncloud/ocis/v2/services/proxy/pkg/autoprovision"
"github.com/owncloud/ocis/v2/services/proxy/pkg/config"
"github.com/owncloud/ocis/v2/services/proxy/pkg/config/parser"
"github.com/owncloud/ocis/v2/services/proxy/pkg/logging"
Expand All @@ -31,6 +32,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"
)
Expand Down Expand Up @@ -136,22 +138,51 @@ func Server(cfg *config.Config) *cli.Command {
func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) alice.Chain {
rolesClient := settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient())
revaClient, err := pool.GetGatewayServiceClient(cfg.Reva.Address, cfg.Reva.GetRevaOptions()...)
if err != nil {
logger.Fatal().Err(err).Msg("Failed to get gateway client")
}
tokenManager, err := jwt.New(map[string]interface{}{
"secret": cfg.TokenManager.JWTSecret,
})
if err != nil {
logger.Fatal().Err(err).
Msg("Failed to create token manager")
}
autoProvsionCreator := autoprovision.NewCreator(autoprovision.WithTokenManager(tokenManager))
var userProvider backend.UserBackend
switch cfg.AccountBackend {
case "cs3":
tokenManager, err := jwt.New(map[string]interface{}{
"secret": cfg.TokenManager.JWTSecret,
})
if err != nil {
logger.Error().Err(err).
Msg("Failed to create token manager")
}

userProvider = backend.NewCS3UserBackend(rolesClient, revaClient, cfg.MachineAuthAPIKey, cfg.OIDC.Issuer, tokenManager, logger)
userProvider = backend.NewCS3UserBackend(
backend.WithLogger(logger),
backend.WithRevaAuthenticator(revaClient),
backend.WithMachineAuthAPIKey(cfg.MachineAuthAPIKey),
backend.WithOIDCissuer(cfg.OIDC.Issuer),
backend.WithAutoProvisonCreator(autoProvsionCreator),
)
default:
logger.Fatal().Msgf("Invalid accounts backend type '%s'", cfg.AccountBackend)
}

var roleAssigner userroles.UserRoleAssigner
switch cfg.RoleAssignment.Driver {
case "default":
roleAssigner = userroles.NewDefaultRoleAssigner(
userroles.WithRoleService(rolesClient),
userroles.WithLogger(logger),
)
case "oidc":
roleAssigner = userroles.NewOIDCRoleAssigner(
userroles.WithRoleService(rolesClient),
userroles.WithLogger(logger),
userroles.WithRolesClaim(cfg.RoleAssignment.OIDCRoleMapper.RoleClaim),
userroles.WithRoleMapping(cfg.RoleAssignment.OIDCRoleMapper.RoleMapping),
userroles.WithAutoProvisonCreator(autoProvsionCreator),
)
default:
logger.Fatal().Msgf("Invalid role assignment driver '%s'", cfg.RoleAssignment.Driver)
}

storeClient := storesvc.NewStoreService("com.owncloud.api.store", grpc.DefaultClient())
if err != nil {
logger.Error().Err(err).
Expand Down Expand Up @@ -203,6 +234,7 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
Logger: logger,
PreSignedURLConfig: cfg.PreSignedURL,
UserProvider: userProvider,
UserRoleAssigner: roleAssigner,
Store: storeClient,
})

Expand All @@ -229,21 +261,19 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config)
middleware.AccountResolver(
middleware.Logger(logger),
middleware.UserProvider(userProvider),
middleware.TokenManagerConfig(*cfg.TokenManager),
middleware.UserRoleAssigner(roleAssigner),
middleware.UserOIDCClaim(cfg.UserOIDCClaim),
middleware.UserCS3Claim(cfg.UserCS3Claim),
middleware.AutoprovisionAccounts(cfg.AutoprovisionAccounts),
),
middleware.SelectorCookie(
middleware.Logger(logger),
middleware.UserProvider(userProvider),
middleware.PolicySelectorConfig(*cfg.PolicySelector),
),
middleware.Policies(logger, cfg.PoliciesMiddleware.Query),
// finally, trigger home creation when a user logs in
middleware.CreateHome(
middleware.Logger(logger),
middleware.TokenManagerConfig(*cfg.TokenManager),
middleware.RevaGatewayClient(revaClient),
middleware.RoleQuotas(cfg.RoleQuotas),
),
Expand Down
13 changes: 13 additions & 0 deletions services/proxy/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Config struct {
Policies []Policy `yaml:"policies"`
OIDC OIDC `yaml:"oidc"`
TokenManager *TokenManager `mask:"struct" yaml:"token_manager"`
RoleAssignment RoleAssignment `yaml:"role_assignment"`
PolicySelector *PolicySelector `yaml:"policy_selector"`
PreSignedURL PreSignedURL `yaml:"pre_signed_url"`
AccountBackend string `yaml:"account_backend" env:"PROXY_ACCOUNT_BACKEND_TYPE" desc:"Account backend the PROXY service should use. Currently only 'cs3' is possible here."`
Expand Down Expand Up @@ -121,6 +122,18 @@ type UserinfoCache struct {
TTL int `yaml:"ttl" env:"PROXY_OIDC_USERINFO_CACHE_TTL" desc:"Max TTL in seconds for the OIDC user info cache."`
}

// RoleAssignment contains the configuration for how to assign roles to users during login
type RoleAssignment struct {
Driver string `yaml:"driver" env:"PROXY_ROLE_ASSIGNMENT_DRIVER" desc:"The mechanism that should be used to assign roles to user upon login. Supported values: 'default' or 'oidc'. 'default' will assign the role 'user' to users which don't have a role assigned at the time they login. 'oidc' will assign the role based on the value of a claim (configured via PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM) from the users OIDC claims."`
OIDCRoleMapper OIDCRoleMapper `yaml:"oidc_role_mapper"`
}

// OIDCRoleMapper contains the configuration for the "oidc" role assignment driber
type OIDCRoleMapper struct {
RoleClaim string `yaml:"role_claim" env:"PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM" desc:"The OIDC claim used to create the users role assignment."`
RoleMapping map[string]string `yaml:"role_mapping" desc:"A mapping of ocis role names to PROXY_ROLE_ASSIGNMENT_OIDC_CLAIM claim values. This setting can only be configured in the configuration file and not via environment variables."`
}

// PolicySelector is the toplevel-configuration for different selectors
type PolicySelector struct {
Static *StaticSelectorConf `yaml:"static"`
Expand Down
15 changes: 14 additions & 1 deletion services/proxy/pkg/config/defaults/defaultconfig.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,20 @@ func DefaultConfig() *config.Config {
},
},
PolicySelector: nil,
Reva: shared.DefaultRevaConfig(),
RoleAssignment: config.RoleAssignment{
Driver: "default",
// this default is only relevant when Driver is set to "oidc"
OIDCRoleMapper: config.OIDCRoleMapper{
RoleClaim: "roles",
RoleMapping: map[string]string{
"admin": "ocisAdmin",
"spaceadmin": "ocisSpaceAdmin",
"user": "ocisUser",
"guest": "ocisGuest",
},
},
},
Reva: shared.DefaultRevaConfig(),
PreSignedURL: config.PreSignedURL{
AllowedHTTPMethods: []string{"GET"},
Enabled: true,
Expand Down
17 changes: 14 additions & 3 deletions services/proxy/pkg/middleware/account_resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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,
}
}
Expand All @@ -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
Expand Down Expand Up @@ -62,7 +65,7 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}

user, token, err = m.userProvider.GetUserByClaims(req.Context(), m.userCS3Claim, value, true)
user, token, err = m.userProvider.GetUserByClaims(req.Context(), m.userCS3Claim, value)

if errors.Is(err, backend.ErrAccountNotFound) {
m.logger.Debug().Str("claim", m.userOIDCClaim).Str("value", value).Msg("User by claim not found")
Expand All @@ -76,7 +79,7 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
if err != nil {
m.logger.Error().Err(err).Msg("Autoprovisioning user failed")
}
user, token, err = m.userProvider.GetUserByClaims(req.Context(), "userid", user.Id.OpaqueId, true)
user, token, err = m.userProvider.GetUserByClaims(req.Context(), "userid", user.Id.OpaqueId)
if err != nil {
m.logger.Error().Err(err).Str("userid", user.Id.OpaqueId).Msg("Error getting token for autoprovisioned user")
}
Expand All @@ -94,14 +97,22 @@ func (m accountResolver) ServeHTTP(w http.ResponseWriter, req *http.Request) {
return
}

// resolve the user's roles
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)
return
}

// add user to context for selectors
ctx = revactx.ContextSetUser(ctx, user)
req = req.WithContext(ctx)

m.logger.Debug().Interface("claims", claims).Interface("user", user).Msg("associated claims with user")
} else if user != nil {
var err error
_, token, err = m.userProvider.GetUserByClaims(req.Context(), "username", user.Username, true)
_, token, err = m.userProvider.GetUserByClaims(req.Context(), "username", user.Username)

if errors.Is(err, backend.ErrAccountDisabled) {
m.logger.Debug().Interface("user", user).Msg("Disabled")
Expand Down
18 changes: 11 additions & 7 deletions services/proxy/pkg/middleware/account_resolver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,10 @@ import (
"github.com/owncloud/ocis/v2/ocis-pkg/oidc"
"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/test"
"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"
)

func TestTokenIsAddedWithMailClaim(t *testing.T) {
Expand Down Expand Up @@ -119,15 +121,17 @@ func newMockAccountResolver(userBackendResult *userv1beta1.User, userBackendErr
token, _ = tokenManager.MintToken(context.Background(), userBackendResult, s)
}

mock := &test.UserBackendMock{
GetUserByClaimsFunc: func(ctx context.Context, claim string, value string, withRoles bool) (*userv1beta1.User, string, error) {
return userBackendResult, token, userBackendErr
},
}
ub := mocks.UserBackend{}
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(mock),
UserProvider(&ub),
UserRoleAssigner(&ra),
TokenManagerConfig(config.TokenManager{JWTSecret: "secret"}),
UserOIDCClaim(oidcclaim),
UserCS3Claim(cs3claim),
Expand Down
Loading