feature/257-add_policy_checking #272

Merged
alexvanin merged 1 commit from dkirillov/frostfs-s3-gw:feature/257-add_policy_checking into master 2024-09-04 19:51:12 +00:00
8 changed files with 64 additions and 11 deletions

View file

@ -18,6 +18,7 @@ import (
type PolicySettings interface { type PolicySettings interface {
ResolveNamespaceAlias(ns string) string ResolveNamespaceAlias(ns string) string
PolicyDenyByDefault() bool
} }
func PolicyCheck(storage engine.ChainRouter, settings PolicySettings, domains []string, log *zap.Logger) Func { func PolicyCheck(storage engine.ChainRouter, settings PolicySettings, domains []string, log *zap.Logger) Func {
@ -27,7 +28,7 @@ func PolicyCheck(storage engine.ChainRouter, settings PolicySettings, domains []
st, err := policyCheck(storage, settings, domains, r) st, err := policyCheck(storage, settings, domains, r)
if err == nil { if err == nil {
if st != chain.Allow && st != chain.NoRuleFound { // todo drop 'st != chain.NoRuleFound' if st != chain.Allow && (st != chain.NoRuleFound || settings.PolicyDenyByDefault()) {
err = apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String())) err = apiErr.GetAPIErrorWithError(apiErr.ErrAccessDenied, fmt.Errorf("policy check: %s", st.String()))
} }
} }

View file

@ -20,7 +20,9 @@ func (c *centerMock) Authenticate(*http.Request) (*middleware.Box, error) {
return &middleware.Box{}, nil return &middleware.Box{}, nil
} }
type middlewareSettingsMock struct{} type middlewareSettingsMock struct {
denyByDefault bool
}
func (r *middlewareSettingsMock) NamespaceHeader() string { func (r *middlewareSettingsMock) NamespaceHeader() string {
return FrostfsNamespaceHeader return FrostfsNamespaceHeader
@ -30,6 +32,10 @@ func (r *middlewareSettingsMock) ResolveNamespaceAlias(ns string) string {
return ns return ns
} }
func (r *middlewareSettingsMock) PolicyDenyByDefault() bool {
return r.denyByDefault
}
type handlerMock struct { type handlerMock struct {
t *testing.T t *testing.T
} }

View file

@ -24,8 +24,9 @@ import (
) )
type routerMock struct { type routerMock struct {
router *chi.Mux router *chi.Mux
cfg Config cfg Config
middlewareSettings *middlewareSettingsMock
} }
func (m *routerMock) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (m *routerMock) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@ -33,6 +34,8 @@ func (m *routerMock) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
func prepareRouter(t *testing.T) *routerMock { func prepareRouter(t *testing.T) *routerMock {
middlewareSettings := &middlewareSettingsMock{}
cfg := Config{ cfg := Config{
Throttle: middleware.ThrottleOpts{ Throttle: middleware.ThrottleOpts{
Limit: 10, Limit: 10,
@ -42,12 +45,13 @@ func prepareRouter(t *testing.T) *routerMock {
Center: &centerMock{}, Center: &centerMock{},
Log: zaptest.NewLogger(t), Log: zaptest.NewLogger(t),
Metrics: &metrics.AppMetrics{}, Metrics: &metrics.AppMetrics{},
MiddlewareSettings: &middlewareSettingsMock{}, MiddlewareSettings: middlewareSettings,
PolicyStorage: inmemory.NewInMemoryLocalOverrides(), PolicyStorage: inmemory.NewInMemoryLocalOverrides(),
} }
return &routerMock{ return &routerMock{
router: NewRouter(cfg), router: NewRouter(cfg),
cfg: cfg, cfg: cfg,
middlewareSettings: middlewareSettings,
} }
} }
@ -183,6 +187,24 @@ func TestPolicyChecker(t *testing.T) {
assertAPIError(t, w, apiErrors.ErrAccessDenied) assertAPIError(t, w, apiErrors.ErrAccessDenied)
} }
func TestDefaultBehaviorPolicyChecker(t *testing.T) {
chiRouter := prepareRouter(t)
bktName, objName := "bucket", "object"
target := fmt.Sprintf("/%s/%s", bktName, objName)
// check we can access bucket if rules not found
w, r := httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, target, nil)
chiRouter.ServeHTTP(w, r)
resp := readResponse(t, w)
require.Equal(t, s3middleware.PutObjectOperation, resp.Method)
// check we cannot access if rules not found when settings is enabled
chiRouter.middlewareSettings.denyByDefault = true
w, r = httptest.NewRecorder(), httptest.NewRequest(http.MethodPut, target, nil)
chiRouter.ServeHTTP(w, r)
assertAPIError(t, w, apiErrors.ErrAccessDenied)
}
func readResponse(t *testing.T, w *httptest.ResponseRecorder) handlerResult { func readResponse(t *testing.T, w *httptest.ResponseRecorder) handlerResult {
var res handlerResult var res handlerResult

View file

@ -102,6 +102,7 @@ type (
namespaceHeader string namespaceHeader string
defaultNamespaces []string defaultNamespaces []string
authorizedControlAPIKeys [][]byte authorizedControlAPIKeys [][]byte
policyDenyByDefault bool
} }
maxClientsConfig struct { maxClientsConfig struct {
@ -212,6 +213,7 @@ func newAppSettings(log *Logger, v *viper.Viper, key *keys.PrivateKey) *appSetti
settings.setBufferMaxSizeForPut(v.GetUint64(cfgBufferMaxSizeForPut)) settings.setBufferMaxSizeForPut(v.GetUint64(cfgBufferMaxSizeForPut))
settings.setMD5Enabled(v.GetBool(cfgMD5Enabled)) settings.setMD5Enabled(v.GetBool(cfgMD5Enabled))
settings.setAuthorizedControlAPIKeys(append(fetchAuthorizedKeys(log.logger, v), key.PublicKey())) settings.setAuthorizedControlAPIKeys(append(fetchAuthorizedKeys(log.logger, v), key.PublicKey()))
settings.setPolicyDenyByDefault(v.GetBool(cfgPolicyDenyByDefault))
return settings return settings
} }
@ -397,6 +399,18 @@ func (s *appSettings) ResolveNamespaceAlias(namespace string) string {
return namespace return namespace
} }
func (s *appSettings) PolicyDenyByDefault() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.policyDenyByDefault
}
func (s *appSettings) setPolicyDenyByDefault(policyDenyByDefault bool) {
s.mu.Lock()
s.policyDenyByDefault = policyDenyByDefault
s.mu.Unlock()
}
func (a *App) initAPI(ctx context.Context) { func (a *App) initAPI(ctx context.Context) {
a.initLayer(ctx) a.initLayer(ctx)
a.initHandler() a.initHandler()
@ -775,6 +789,7 @@ func (a *App) updateSettings() {
a.settings.setMD5Enabled(a.cfg.GetBool(cfgMD5Enabled)) a.settings.setMD5Enabled(a.cfg.GetBool(cfgMD5Enabled))
a.settings.setDefaultNamespaces(a.cfg.GetStringSlice(cfgKludgeDefaultNamespaces)) a.settings.setDefaultNamespaces(a.cfg.GetStringSlice(cfgKludgeDefaultNamespaces))
a.settings.setAuthorizedControlAPIKeys(append(fetchAuthorizedKeys(a.log, a.cfg), a.key.PublicKey())) a.settings.setAuthorizedControlAPIKeys(append(fetchAuthorizedKeys(a.log, a.cfg), a.key.PublicKey()))
a.settings.setPolicyDenyByDefault(a.cfg.GetBool(cfgPolicyDenyByDefault))
} }
func (a *App) startServices() { func (a *App) startServices() {

View file

@ -198,7 +198,8 @@ const ( // Settings.
cfgSoftMemoryLimit = "runtime.soft_memory_limit" cfgSoftMemoryLimit = "runtime.soft_memory_limit"
// Enable return MD5 checksum in ETag. // Enable return MD5 checksum in ETag.
cfgMD5Enabled = "features.md5.enabled" cfgMD5Enabled = "features.md5.enabled"
cfgPolicyDenyByDefault = "features.policy.deny_by_default"
// FrostfsID. // FrostfsID.
cfgFrostfsIDEnabled = "frostfsid.enabled" cfgFrostfsIDEnabled = "frostfsid.enabled"

View file

@ -160,6 +160,8 @@ S3_GW_TRACING_EXPORTER="otlp_grpc"
S3_GW_RUNTIME_SOFT_MEMORY_LIMIT=1073741824 S3_GW_RUNTIME_SOFT_MEMORY_LIMIT=1073741824
S3_GW_FEATURES_MD5_ENABLED=false S3_GW_FEATURES_MD5_ENABLED=false
# Enable denying access for request that doesn't match any policy chain rules.
S3_GW_FEATURES_POLICY_DENY_BY_DEFAULT=false
# ReadTimeout is the maximum duration for reading the entire # ReadTimeout is the maximum duration for reading the entire
# request, including the body. A zero or negative value means # request, including the body. A zero or negative value means

View file

@ -189,6 +189,9 @@ runtime:
soft_memory_limit: 1gb soft_memory_limit: 1gb
features: features:
policy:
# Enable denying access for request that doesn't match any policy chain rules.
deny_by_default: false
md5: md5:
enabled: false enabled: false

View file

@ -595,13 +595,16 @@ Contains parameters for enabling features.
```yaml ```yaml
features: features:
policy:
deny_by_default: false
md5: md5:
enabled: false enabled: false
``` ```
| Parameter | Type | SIGHUP reload | Default value | Description | | Parameter | Type | SIGHUP reload | Default value | Description |
|---------------|--------|---------------|---------------|----------------------------------------------------------------| |--------------------------|--------|---------------|---------------|------------------------------------------------------------------------------|
| `md5.enabled` | `bool` | yes | false | Flag to enable return MD5 checksum in ETag headers and fields. | | `md5.enabled` | `bool` | yes | false | Flag to enable return MD5 checksum in ETag headers and fields. |
| `policy.deny_by_default` | `bool` | yes | false | Enable denying access for request that doesn't match any policy chain rules. |
# `web` section # `web` section
Contains web server configuration parameters. Contains web server configuration parameters.