Merge pull request #5617 from owncloud/role-quota

add config option to set default quota per role
This commit is contained in:
David Christofas
2023-02-22 17:08:16 +01:00
committed by GitHub
12 changed files with 104 additions and 18 deletions

View File

@@ -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

3
go.mod
View File

@@ -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

4
go.sum
View File

@@ -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=

View File

@@ -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)."`

View File

@@ -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{}{

View File

@@ -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:
<role ID1>: <quota1>
<role ID2>: <quota2>
```
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.

View File

@@ -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),
),
)
}

View File

@@ -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 servers 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 servers TLS certificate for https enabled backend services."`
AuthMiddleware AuthMiddleware `yaml:"auth_middleware"`
Context context.Context `yaml:"-" json:"-"`
}

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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 {

View File

@@ -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,