[#371] Add custom Source IP header configuration #371

Merged
dkirillov merged 1 commit from mbiryukova/frostfs-s3-gw:feature/source_ip_header into master 2024-04-22 07:42:47 +00:00
11 changed files with 54 additions and 13 deletions
Showing only changes of commit e3f03f4599 - Show all commits

View file

@ -30,6 +30,7 @@ This document outlines major changes between releases.
- Add new `reconnect_interval` config param (#291) - Add new `reconnect_interval` config param (#291)
- Support `GetBucketPolicyStatus` (#301) - Support `GetBucketPolicyStatus` (#301)
- Add FrostfsID cache (#269) - Add FrostfsID cache (#269)
- Add new `source_ip_header` config param (#371)
### Changed ### Changed
- Generalise config param `use_default_xmlns_for_complete_multipart` to `use_default_xmlns` so that use default xmlns for all requests (#221) - Generalise config param `use_default_xmlns_for_complete_multipart` to `use_default_xmlns` so that use default xmlns for all requests (#221)

View file

@ -415,7 +415,7 @@ func prepareTestRequestWithQuery(hc *handlerContext, bktName, objName string, qu
r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(body)) r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(body))
r.URL.RawQuery = query.Encode() r.URL.RawQuery = query.Encode()
reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}) reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "")
r = r.WithContext(middleware.SetReqInfo(hc.Context(), reqInfo)) r = r.WithContext(middleware.SetReqInfo(hc.Context(), reqInfo))
return w, r return w, r
@ -425,7 +425,7 @@ func prepareTestPayloadRequest(hc *handlerContext, bktName, objName string, payl
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPut, defaultURL, payload) r := httptest.NewRequest(http.MethodPut, defaultURL, payload)
reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}) reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "")
r = r.WithContext(middleware.SetReqInfo(hc.Context(), reqInfo)) r = r.WithContext(middleware.SetReqInfo(hc.Context(), reqInfo))
return w, r return w, r

View file

@ -315,7 +315,7 @@ func TestPutBucketLockConfigurationHandler(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(body)) r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(body))
r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket}))) r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket}, "")))
hc.Handler().PutBucketObjectLockConfigHandler(w, r) hc.Handler().PutBucketObjectLockConfigHandler(w, r)
@ -388,7 +388,7 @@ func TestGetBucketLockConfigurationHandler(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
w := httptest.NewRecorder() w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(nil)) r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(nil))
r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket}))) r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket}, "")))
hc.Handler().GetBucketObjectLockConfigHandler(w, r) hc.Handler().GetBucketObjectLockConfigHandler(w, r)

View file

@ -351,7 +351,7 @@ func getChunkedRequest(ctx context.Context, t *testing.T, bktName, objName strin
req.Body = io.NopCloser(reqBody) req.Body = io.NopCloser(reqBody)
w := httptest.NewRecorder() w := httptest.NewRecorder()
reqInfo := middleware.NewReqInfo(w, req, middleware.ObjectRequest{Bucket: bktName, Object: objName}) reqInfo := middleware.NewReqInfo(w, req, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "")
req = req.WithContext(middleware.SetReqInfo(ctx, reqInfo)) req = req.WithContext(middleware.SetReqInfo(ctx, reqInfo))
req = req.WithContext(middleware.SetBox(req.Context(), &middleware.Box{ req = req.WithContext(middleware.SetBox(req.Context(), &middleware.Box{
ClientTime: signTime, ClientTime: signTime,

View file

@ -83,17 +83,24 @@ var (
) )
// NewReqInfo returns new ReqInfo based on parameters. // NewReqInfo returns new ReqInfo based on parameters.
func NewReqInfo(w http.ResponseWriter, r *http.Request, req ObjectRequest) *ReqInfo { func NewReqInfo(w http.ResponseWriter, r *http.Request, req ObjectRequest, sourceIPHeader string) *ReqInfo {
return &ReqInfo{ reqInfo := &ReqInfo{
API: req.Method, API: req.Method,
BucketName: req.Bucket, BucketName: req.Bucket,
ObjectName: req.Object, ObjectName: req.Object,
UserAgent: r.UserAgent(), UserAgent: r.UserAgent(),
RemoteHost: getSourceIP(r),
RequestID: GetRequestID(w), RequestID: GetRequestID(w),
DeploymentID: deploymentID.String(), DeploymentID: deploymentID.String(),
URL: r.URL, URL: r.URL,
} }
if sourceIPHeader != "" {
reqInfo.RemoteHost = r.Header.Get(sourceIPHeader)
} else {
reqInfo.RemoteHost = getSourceIP(r)
}
return reqInfo
} }
// AppendTags -- appends key/val to ReqInfo.tags. // AppendTags -- appends key/val to ReqInfo.tags.
@ -193,6 +200,7 @@ func GetReqLog(ctx context.Context) *zap.Logger {
type RequestSettings interface { type RequestSettings interface {
NamespaceHeader() string NamespaceHeader() string
ResolveNamespaceAlias(string) string ResolveNamespaceAlias(string) string
SourceIPHeader() string
} }
func Request(log *zap.Logger, settings RequestSettings) Func { func Request(log *zap.Logger, settings RequestSettings) Func {
@ -211,7 +219,7 @@ func Request(log *zap.Logger, settings RequestSettings) Func {
// set request info into context // set request info into context
// bucket name and object will be set in reqInfo later (limitation of go-chi) // bucket name and object will be set in reqInfo later (limitation of go-chi)
reqInfo := NewReqInfo(w, r, ObjectRequest{}) reqInfo := NewReqInfo(w, r, ObjectRequest{}, settings.SourceIPHeader())
reqInfo.Namespace = settings.ResolveNamespaceAlias(r.Header.Get(settings.NamespaceHeader())) reqInfo.Namespace = settings.ResolveNamespaceAlias(r.Header.Get(settings.NamespaceHeader()))
r = r.WithContext(SetReqInfo(r.Context(), reqInfo)) r = r.WithContext(SetReqInfo(r.Context(), reqInfo))
@ -317,11 +325,14 @@ func getSourceIP(r *http.Request) string {
} }
} }
if addr != "" { if addr == "" {
return addr addr = r.RemoteAddr
} }
// Default to remote address if headers not set. // Default to remote address if headers not set.
addr, _, _ = net.SplitHostPort(r.RemoteAddr) raddr, _, _ := net.SplitHostPort(addr)
return addr if raddr == "" {
return addr
}
return raddr
} }

View file

@ -64,6 +64,10 @@ type middlewareSettingsMock struct {
aclEnabled bool aclEnabled bool
} }
func (r *middlewareSettingsMock) SourceIPHeader() string {
return ""
}
func (r *middlewareSettingsMock) NamespaceHeader() string { func (r *middlewareSettingsMock) NamespaceHeader() string {
return FrostfsNamespaceHeader return FrostfsNamespaceHeader
} }

View file

@ -109,6 +109,7 @@ type (
defaultNamespaces []string defaultNamespaces []string
authorizedControlAPIKeys [][]byte authorizedControlAPIKeys [][]byte
policyDenyByDefault bool policyDenyByDefault bool
sourceIPHeader string
} }
maxClientsConfig struct { maxClientsConfig struct {
@ -235,6 +236,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger, key *keys.PrivateK
s.setMD5Enabled(v.GetBool(cfgMD5Enabled)) s.setMD5Enabled(v.GetBool(cfgMD5Enabled))
s.setAuthorizedControlAPIKeys(append(fetchAuthorizedKeys(log, v), key.PublicKey())) s.setAuthorizedControlAPIKeys(append(fetchAuthorizedKeys(log, v), key.PublicKey()))
s.setPolicyDenyByDefault(v.GetBool(cfgPolicyDenyByDefault)) s.setPolicyDenyByDefault(v.GetBool(cfgPolicyDenyByDefault))
s.setSourceIPHeader(v.GetString(cfgSourceIPHeader))
} }
func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger) { func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger) {
@ -429,6 +431,18 @@ func (s *appSettings) setPolicyDenyByDefault(policyDenyByDefault bool) {
s.mu.Unlock() s.mu.Unlock()
} }
func (s *appSettings) setSourceIPHeader(header string) {
s.mu.Lock()
s.sourceIPHeader = header
s.mu.Unlock()
}
func (s *appSettings) SourceIPHeader() string {
s.mu.RLock()
defer s.mu.RUnlock()
return s.sourceIPHeader
}
func (a *App) initAPI(ctx context.Context) { func (a *App) initAPI(ctx context.Context) {
a.initLayer(ctx) a.initLayer(ctx)
a.initHandler() a.initHandler()

View file

@ -181,6 +181,8 @@ const ( // Settings.
// Namespaces. // Namespaces.
cfgNamespacesConfig = "namespaces.config" cfgNamespacesConfig = "namespaces.config"
cfgSourceIPHeader = "source_ip_header"
// Command line args. // Command line args.
cmdHelp = "help" cmdHelp = "help"
cmdVersion = "version" cmdVersion = "version"

View file

@ -220,3 +220,6 @@ S3_GW_PROXY_CONTRACT=proxy.frostfs
# Namespaces configuration # Namespaces configuration
S3_GW_NAMESPACES_CONFIG=namespaces.json S3_GW_NAMESPACES_CONFIG=namespaces.json
# Custom header to retrieve Source IP
S3_GW_SOURCE_IP_HEADER=Source-Ip

View file

@ -259,3 +259,6 @@ proxy:
namespaces: namespaces:
config: namespaces.json config: namespaces.json
# Custom header to retrieve Source IP
source_ip_header: "Source-Ip"

View file

@ -220,6 +220,8 @@ allowed_access_key_id_prefixes:
- 3stjWenX15YwYzczMr88gy3CQr4NYFBQ8P7keGzH5QFn - 3stjWenX15YwYzczMr88gy3CQr4NYFBQ8P7keGzH5QFn
reconnect_interval: 1m reconnect_interval: 1m
source_ip_header: "Source-Ip"
``` ```
| Parameter | Type | SIGHUP reload | Default value | Description | | Parameter | Type | SIGHUP reload | Default value | Description |
@ -236,6 +238,7 @@ reconnect_interval: 1m
| `max_clients_deadline` | `duration` | no | `30s` | Deadline after which the gate sends error `RequestTimeout` to a client. | | `max_clients_deadline` | `duration` | no | `30s` | Deadline after which the gate sends error `RequestTimeout` to a client. |
| `allowed_access_key_id_prefixes` | `[]string` | no | | List of allowed `AccessKeyID` prefixes which S3 GW serve. If the parameter is omitted, all `AccessKeyID` will be accepted. | | `allowed_access_key_id_prefixes` | `[]string` | no | | List of allowed `AccessKeyID` prefixes which S3 GW serve. If the parameter is omitted, all `AccessKeyID` will be accepted. |
| `reconnect_interval` | `duration` | no | `1m` | Listeners reconnection interval. | | `reconnect_interval` | `duration` | no | `1m` | Listeners reconnection interval. |
| `source_ip_header` | `string` | yes | | Custom header to retrieve Source IP. |
### `wallet` section ### `wallet` section