From 67549b4ebd556f31f691539ac86dff87d83afffc Mon Sep 17 00:00:00 2001 From: David Christofas Date: Tue, 21 Feb 2023 15:26:21 +0100 Subject: [PATCH] add config option to set default quota per role --- changelog/unreleased/role-quota.md | 12 ++++++ go.mod | 3 +- go.sum | 4 +- services/proxy/README.md | 13 ++++++ services/proxy/pkg/command/server.go | 1 + services/proxy/pkg/config/config.go | 29 ++++++------- services/proxy/pkg/middleware/create_home.go | 43 ++++++++++++++++++++ services/proxy/pkg/middleware/options.go | 12 +++++- 8 files changed, 99 insertions(+), 18 deletions(-) create mode 100644 changelog/unreleased/role-quota.md diff --git a/changelog/unreleased/role-quota.md b/changelog/unreleased/role-quota.md new file mode 100644 index 0000000000..1f8af9c52f --- /dev/null +++ b/changelog/unreleased/role-quota.md @@ -0,0 +1,12 @@ +Enhancement: Added option to configure default quota per role + +Admins can assign default quotas to users with certain roles by adding the following config to the `proxy.yaml`. +E.g.: +``` +role_quotas: + d7beeea8-8ff4-406b-8fb6-ab2dd81e6b11: 2300000 +``` + +It maps a role ID to the quota in bytes. + +https://github.com/owncloud/ocis/pull/5616 diff --git a/go.mod b/go.mod index 11a39e07d4..2acb88e989 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/blevesearch/bleve/v2 v2.3.5 github.com/coreos/go-oidc/v3 v3.4.0 github.com/cs3org/go-cs3apis v0.0.0-20221012090518-ef2996678965 - github.com/cs3org/reva/v2 v2.12.1-0.20230214085134-ec27f5f8feb3 + github.com/cs3org/reva/v2 v2.12.1-0.20230221142843-06fcf4e500bf github.com/disintegration/imaging v1.6.2 github.com/ggwhite/go-masker v1.0.9 github.com/go-chi/chi/v5 v5.0.7 @@ -251,6 +251,7 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/statsd_exporter v0.22.8 // indirect github.com/rivo/uniseg v0.4.2 // indirect + github.com/rogpeppe/go-internal v1.8.0 // indirect github.com/rs/cors v1.8.2 // indirect github.com/rs/xid v1.4.0 // indirect github.com/russellhaering/goxmldsig v1.1.1 // indirect diff --git a/go.sum b/go.sum index c6ed746d9c..596bbbabb1 100644 --- a/go.sum +++ b/go.sum @@ -343,8 +343,8 @@ github.com/crewjam/httperr v0.2.0/go.mod h1:Jlz+Sg/XqBQhyMjdDiC+GNNRzZTD7x39Gu3p github.com/crewjam/saml v0.4.6/go.mod h1:ZBOXnNPFzB3CgOkRm7Nd6IVdkG+l/wF+0ZXLqD96t1A= github.com/crewjam/saml v0.4.9 h1:X2jDv4dv3IvfT9t+RhADavzNFAcq3fVxzTCIH3G605U= github.com/crewjam/saml v0.4.9/go.mod h1:9Zh6dWPtB3MSzTRt8fIFH60Z351QQ+s7hCU3J/tTlA4= -github.com/cs3org/reva/v2 v2.12.1-0.20230214085134-ec27f5f8feb3 h1:KaFl1ZfjjKSlDsq/zvhBV9f+mXYqnLdK5IhAaZBoXDo= -github.com/cs3org/reva/v2 v2.12.1-0.20230214085134-ec27f5f8feb3/go.mod h1:u73Df9JAZsDj43GIjQIb3DO1PLJuPutZXkRqQH0oGXA= +github.com/cs3org/reva/v2 v2.12.1-0.20230221142843-06fcf4e500bf h1:OZwvSm5O4t/jxoVVgJcwbM2BHYv10rGq0KqqVAwTERY= +github.com/cs3org/reva/v2 v2.12.1-0.20230221142843-06fcf4e500bf/go.mod h1:dbaNP2U3nGQA5BHLc5w/hqviq7b0F4eygNwC38jeaiU= github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8 h1:Z9lwXumT5ACSmJ7WGnFl+OMLLjpz5uR2fyz7dC255FI= github.com/cubewise-code/go-mime v0.0.0-20200519001935-8c5762b177d8/go.mod h1:4abs/jPXcmJzYoYGF91JF9Uq9s/KL5n1jvFDix8KcqY= github.com/cyberdelia/templates v0.0.0-20141128023046-ca7fffd4298c/go.mod h1:GyV+0YP4qX0UQ7r2MoYZ+AvYDp12OF5yg4q8rGnyNh4= diff --git a/services/proxy/README.md b/services/proxy/README.md index 6c31a29748..eb515e9349 100644 --- a/services/proxy/README.md +++ b/services/proxy/README.md @@ -13,6 +13,19 @@ The following request authentication schemes are implemented: - Signed URL - Public Share Token +## Automatic quota assignments + +It is possible to automatically assign a specific quota amount to new users depending on their role. +To do this you need to add the following config snippet to the proxy.yaml config file. + +```yaml +role_quotas: + : + : +``` + +There you need to configure the mapping between the roles by their ID and the quota in Bytes. + ## Recommendations for Production Deployments In a production deployment, you want to have basic authentication (`PROXY_ENABLE_BASIC_AUTH`) disabled which is the default state. You also want to setup a firewall to only allow requests to the proxy service or the reverse proxy if you have one. Requests to the other services should be blocked by the firewall. diff --git a/services/proxy/pkg/command/server.go b/services/proxy/pkg/command/server.go index 492a6448fc..8c265a5a39 100644 --- a/services/proxy/pkg/command/server.go +++ b/services/proxy/pkg/command/server.go @@ -249,6 +249,7 @@ func loadMiddlewares(ctx context.Context, logger log.Logger, cfg *config.Config) middleware.Logger(logger), middleware.TokenManagerConfig(*cfg.TokenManager), middleware.RevaGatewayClient(revaClient), + middleware.RoleQuotas(cfg.RoleQuotas), ), ) } diff --git a/services/proxy/pkg/config/config.go b/services/proxy/pkg/config/config.go index 234ca828ec..d7ac632dd0 100644 --- a/services/proxy/pkg/config/config.go +++ b/services/proxy/pkg/config/config.go @@ -21,20 +21,21 @@ type Config struct { Reva *shared.Reva `yaml:"reva"` GRPCClientTLS *shared.GRPCClientTLS `yaml:"grpc_client_tls"` - Policies []Policy `yaml:"policies"` - OIDC OIDC `yaml:"oidc"` - TokenManager *TokenManager `mask:"struct" yaml:"token_manager"` - 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."` - UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that is used for resolving users with the account backend. The value of the claim must hold a per user unique, stable and non re-assignable identifier. The availability of claims depends on your Identity Provider. There are common claims available for most Identity providers like 'email' or 'preferred_user' but you can also add your own claim."` - UserCS3Claim string `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Supported values are 'username', 'mail' and 'userid'."` - MachineAuthAPIKey string `mask:"password" yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;PROXY_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services."` - AutoprovisionAccounts bool `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS" desc:"Set this to 'true' to automatically provision users that do not yet exist in the users service on-demand upon first sign-in. To use this a write-enabled libregraph user backend needs to be setup an running."` - EnableBasicAuth bool `yaml:"enable_basic_auth" env:"PROXY_ENABLE_BASIC_AUTH" desc:"Set this to true to enable 'basic authentication' (username/password)."` - InsecureBackends bool `yaml:"insecure_backends" env:"PROXY_INSECURE_BACKENDS" desc:"Disable TLS certificate validation for all HTTP backend connections."` - BackendHTTPSCACert string `yaml:"backend_https_cacert" env:"PROXY_HTTPS_CACERT" desc:"Path/File for the root CA certificate used to validate the server’s TLS certificate for https enabled backend services."` - AuthMiddleware AuthMiddleware `yaml:"auth_middleware"` + RoleQuotas map[string]uint64 `yaml:"role_quotas"` + Policies []Policy `yaml:"policies"` + OIDC OIDC `yaml:"oidc"` + TokenManager *TokenManager `mask:"struct" yaml:"token_manager"` + 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."` + UserOIDCClaim string `yaml:"user_oidc_claim" env:"PROXY_USER_OIDC_CLAIM" desc:"The name of an OpenID Connect claim that is used for resolving users with the account backend. The value of the claim must hold a per user unique, stable and non re-assignable identifier. The availability of claims depends on your Identity Provider. There are common claims available for most Identity providers like 'email' or 'preferred_user' but you can also add your own claim."` + UserCS3Claim string `yaml:"user_cs3_claim" env:"PROXY_USER_CS3_CLAIM" desc:"The name of a CS3 user attribute (claim) that should be mapped to the 'user_oidc_claim'. Supported values are 'username', 'mail' and 'userid'."` + MachineAuthAPIKey string `mask:"password" yaml:"machine_auth_api_key" env:"OCIS_MACHINE_AUTH_API_KEY;PROXY_MACHINE_AUTH_API_KEY" desc:"Machine auth API key used to validate internal requests necessary to access resources from other services."` + AutoprovisionAccounts bool `yaml:"auto_provision_accounts" env:"PROXY_AUTOPROVISION_ACCOUNTS" desc:"Set this to 'true' to automatically provision users that do not yet exist in the users service on-demand upon first sign-in. To use this a write-enabled libregraph user backend needs to be setup an running."` + EnableBasicAuth bool `yaml:"enable_basic_auth" env:"PROXY_ENABLE_BASIC_AUTH" desc:"Set this to true to enable 'basic authentication' (username/password)."` + InsecureBackends bool `yaml:"insecure_backends" env:"PROXY_INSECURE_BACKENDS" desc:"Disable TLS certificate validation for all HTTP backend connections."` + BackendHTTPSCACert string `yaml:"backend_https_cacert" env:"PROXY_HTTPS_CACERT" desc:"Path/File for the root CA certificate used to validate the server’s TLS certificate for https enabled backend services."` + AuthMiddleware AuthMiddleware `yaml:"auth_middleware"` Context context.Context `yaml:"-" json:"-"` } diff --git a/services/proxy/pkg/middleware/create_home.go b/services/proxy/pkg/middleware/create_home.go index 5fa30a1a3e..cc4abf7787 100644 --- a/services/proxy/pkg/middleware/create_home.go +++ b/services/proxy/pkg/middleware/create_home.go @@ -2,13 +2,17 @@ package middleware import ( "net/http" + "strconv" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" + userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" revactx "github.com/cs3org/reva/v2/pkg/ctx" "github.com/cs3org/reva/v2/pkg/rgrpc/status" + "github.com/cs3org/reva/v2/pkg/utils" "github.com/owncloud/ocis/v2/ocis-pkg/log" + "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0/errorcode" "google.golang.org/grpc/metadata" ) @@ -22,6 +26,7 @@ func CreateHome(optionSetters ...Option) func(next http.Handler) http.Handler { next: next, logger: logger, revaGatewayClient: options.RevaGatewayClient, + roleQuotas: options.RoleQuotas, } } } @@ -30,6 +35,7 @@ type createHome struct { next http.Handler logger log.Logger revaGatewayClient gateway.GatewayAPIClient + roleQuotas map[string]uint64 } func (m createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { @@ -45,6 +51,19 @@ func (m createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { ctx := metadata.AppendToOutgoingContext(req.Context(), revactx.TokenHeader, token) createHomeReq := &provider.CreateHomeRequest{} + u, ok := revactx.ContextGetUser(ctx) + if ok { + roleIDs, err := m.getUserRoles(u) + if err != nil { + m.logger.Error().Err(err).Str("userid", u.Id.OpaqueId).Msg("failed to get roles for user") + errorcode.GeneralException.Render(w, req, http.StatusInternalServerError, "Unauthorized") + return + } + if limit, hasLimit := m.checkRoleQuotaLimit(roleIDs); hasLimit { + createHomeReq.Opaque = utils.AppendPlainToOpaque(nil, "quota", strconv.FormatUint(limit, 10)) + } + } + createHomeRes, err := m.revaGatewayClient.CreateHome(ctx, createHomeReq) if err != nil { @@ -62,3 +81,27 @@ func (m createHome) ServeHTTP(w http.ResponseWriter, req *http.Request) { func (m createHome) shouldServe(req *http.Request) bool { return req.Header.Get("x-access-token") != "" } + +func (m createHome) getUserRoles(user *userv1beta1.User) ([]string, error) { + var roleIDs []string + if err := utils.ReadJSONFromOpaque(user.Opaque, "roles", &roleIDs); err != nil { + return nil, err + } + + tmp := make(map[string]struct{}) + for _, id := range roleIDs { + tmp[id] = struct{}{} + } + + dedup := make([]string, 0, len(tmp)) + for k := range tmp { + dedup = append(dedup, k) + } + return dedup, nil +} + +func (m createHome) checkRoleQuotaLimit(roleIDs []string) (uint64, bool) { + id := roleIDs[0] // At the moment a user can only have one role. + quota, ok := m.roleQuotas[id] + return quota, ok +} diff --git a/services/proxy/pkg/middleware/options.go b/services/proxy/pkg/middleware/options.go index 2d07b2b772..c45bdd6682 100644 --- a/services/proxy/pkg/middleware/options.go +++ b/services/proxy/pkg/middleware/options.go @@ -60,6 +60,9 @@ type Options struct { AccessTokenVerifyMethod string // JWKS sets the options for fetching the JWKS from the IDP JWKS config.JWKS + // RoleQuotas hold userid:quota mappings. These will be used when provisioning new users. + // The users will get as much quota as is set for their role. + RoleQuotas map[string]uint64 } // newOptions initializes the available default options. @@ -206,9 +209,16 @@ func AccessTokenVerifyMethod(method string) Option { } } -// JWKS sets the options for fetching the JWKS from the IDP +// JWKSOptions sets the options for fetching the JWKS from the IDP func JWKSOptions(jo config.JWKS) Option { return func(o *Options) { o.JWKS = jo } } + +// RoleQuotas sets the role quota mapping setting +func RoleQuotas(roleQuotas map[string]uint64) Option { + return func(o *Options) { + o.RoleQuotas = roleQuotas + } +}