diff --git a/changelog/unreleased/role-quota.md b/changelog/unreleased/role-quota.md new file mode 100644 index 000000000..1f8af9c52 --- /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 7474b8aa8..6fd920715 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.20230222151731-83c7b4d26b2b 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 a47897b7c..cc997cd58 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.20230222151731-83c7b4d26b2b h1:wIwnuSyH8tM4dbr16UYEoYF7ESlfxah2q99oz/FscU0= +github.com/cs3org/reva/v2 v2.12.1-0.20230222151731-83c7b4d26b2b/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/frontend/pkg/config/config.go b/services/frontend/pkg/config/config.go index 73df20e43..a6ce2eb9d 100644 --- a/services/frontend/pkg/config/config.go +++ b/services/frontend/pkg/config/config.go @@ -28,6 +28,7 @@ type Config struct { EnableFavorites bool `yaml:"enable_favorites" env:"FRONTEND_ENABLE_FAVORITES" desc:"Enables the support for favorites in the clients."` EnableProjectSpaces bool `yaml:"enable_project_spaces" env:"FRONTEND_ENABLE_PROJECT_SPACES" desc:"Changing this value is NOT supported. Indicates to clients that project spaces are supposed to be made available."` EnableShareJail bool `yaml:"enable_share_jail" env:"FRONTEND_ENABLE_SHARE_JAIL" desc:"Changing this value is NOT supported. Indicates to clients that the share jail is supposed to be used."` + MaxQuota uint64 `yaml:"max_quota" env:"OCIS_SPACES_MAX_QUOTA;FRONTEND_MAX_QUOTA" desc:"Set the global max quota value in the capabilities."` UploadMaxChunkSize int `yaml:"upload_max_chunk_size" env:"FRONTEND_UPLOAD_MAX_CHUNK_SIZE" desc:"Sets the max chunk sizes in bytes for uploads via the clients."` UploadHTTPMethodOverride string `yaml:"upload_http_method_override" env:"FRONTEND_UPLOAD_HTTP_METHOD_OVERRIDE" desc:"Advise TUS to replace PATCH requests by POST requests."` DefaultUploadProtocol string `yaml:"default_upload_protocol" env:"FRONTEND_DEFAULT_UPLOAD_PROTOCOL" desc:"The default upload protocol to use in the clients (e.g. tus)."` diff --git a/services/frontend/pkg/revaconfig/config.go b/services/frontend/pkg/revaconfig/config.go index 82926ab6a..4fe63418f 100644 --- a/services/frontend/pkg/revaconfig/config.go +++ b/services/frontend/pkg/revaconfig/config.go @@ -265,6 +265,7 @@ func FrontendConfigFromStruct(cfg *config.Config) (map[string]interface{}, error "enabled": cfg.EnableProjectSpaces || cfg.EnableShareJail, "projects": cfg.EnableProjectSpaces, "share_jail": cfg.EnableShareJail, + "max_quota": cfg.MaxQuota, }, }, "version": map[string]interface{}{ diff --git a/services/proxy/README.md b/services/proxy/README.md index 6c31a2974..eb515e934 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 492a6448f..8c265a5a3 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 234ca828e..d7ac632dd 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 5fa30a1a3..cc4abf778 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 2d07b2b77..c45bdd668 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 + } +} diff --git a/services/storage-users/pkg/config/config.go b/services/storage-users/pkg/config/config.go index 27038b346..5bf644c7f 100644 --- a/services/storage-users/pkg/config/config.go +++ b/services/storage-users/pkg/config/config.go @@ -103,6 +103,7 @@ type OCISDriver struct { MaxAcquireLockCycles int `yaml:"max_acquire_lock_cycles" env:"STORAGE_USERS_OCIS_MAX_ACQUIRE_LOCK_CYCLES" desc:"When trying to lock files, ocis will try this amount of times to acquire the lock before failing. After each try it will wait for an increasing amount of time. Values of 0 or below will be ignored and the default value of 20 will be used."` LockCycleDurationFactor int `yaml:"lock_cycle_duration_factor" env:"STORAGE_USERS_OCIS_LOCK_CYCLE_DURATION_FACTOR" desc:"When trying to lock files, ocis will multiply the cycle with this factor and use it as a millisecond timeout. Values of 0 or below will be ignored and the default value of 30 will be used."` AsyncUploads bool `yaml:"async_uploads" env:"STORAGE_USERS_OCIS_ASYNC_UPLOADS" desc:"Enable asynchronous file uploads."` + MaxQuota uint64 `yaml:"max_quota" env:"OCIS_SPACES_MAX_QUOTA;STORAGE_USERS_OCIS_MAX_QUOTA" desc:"Set a global max quota for spaces. If you are not setting OCIS_SPACES_MAX_QUOTA then don't forget to set FRONTEND_MAX_QUOTA."` } type S3NGDriver struct { diff --git a/services/storage-users/pkg/revaconfig/drivers.go b/services/storage-users/pkg/revaconfig/drivers.go index 86758ba77..b9f5a03a0 100644 --- a/services/storage-users/pkg/revaconfig/drivers.go +++ b/services/storage-users/pkg/revaconfig/drivers.go @@ -123,6 +123,7 @@ func Ocis(cfg *config.Config) map[string]interface{} { "max_acquire_lock_cycles": cfg.Drivers.OCIS.MaxAcquireLockCycles, "lock_cycle_duration_factor": cfg.Drivers.OCIS.LockCycleDurationFactor, "asyncfileuploads": cfg.Drivers.OCIS.AsyncUploads, + "max_quota": cfg.Drivers.OCIS.MaxQuota, "statcache": map[string]interface{}{ "cache_store": cfg.Cache.Store, "cache_nodes": cfg.Cache.Nodes, @@ -158,6 +159,7 @@ func OcisNoEvents(cfg *config.Config) map[string]interface{} { "permissionssvc_tls_mode": cfg.Commons.GRPCClientTLS.Mode, "max_acquire_lock_cycles": cfg.Drivers.OCIS.MaxAcquireLockCycles, "lock_cycle_duration_factor": cfg.Drivers.OCIS.LockCycleDurationFactor, + "max_quota": cfg.Drivers.OCIS.MaxQuota, "statcache": map[string]interface{}{ "cache_store": cfg.Cache.Store, "cache_nodes": cfg.Cache.Nodes,