From 2910e88ba5d8af0586d25c36bd99b3a15860d24a Mon Sep 17 00:00:00 2001 From: "A.Unger" Date: Wed, 2 Dec 2020 15:31:17 +0100 Subject: [PATCH] ugly working draft --- proxy/pkg/command/server.go | 15 ++++++++++-- proxy/pkg/config/config.go | 11 ++++++++- proxy/pkg/flagset/flagset.go | 9 +++++++- proxy/pkg/middleware/authentication.go | 2 ++ proxy/pkg/middleware/basic_auth.go | 32 +++++++++++++++++++++++--- proxy/pkg/middleware/oidc_auth.go | 15 ++++++------ proxy/pkg/middleware/options.go | 9 ++++++++ storage/pkg/command/frontend.go | 2 +- storage/pkg/flagset/frontend.go | 2 +- 9 files changed, 81 insertions(+), 16 deletions(-) diff --git a/proxy/pkg/command/server.go b/proxy/pkg/command/server.go index 1d4636679..573570768 100644 --- a/proxy/pkg/command/server.go +++ b/proxy/pkg/command/server.go @@ -3,6 +3,7 @@ package command import ( "context" "crypto/tls" + "fmt" "net/http" "os" "os/signal" @@ -48,8 +49,17 @@ func Server(cfg *config.Config) *cli.Command { } cfg.PreSignedURL.AllowedHTTPMethods = ctx.StringSlice("presignedurl-allow-method") - // When running on single binary mode the before hook from the root command won't get called. We manually - // call this before hook from ocis command, so the configuration can be loaded. + cfg.Reva.Middleware.Auth.CredentialsByUserAgent = make(map[string]string, 0) + uaw := ctx.StringSlice("proxy-user-agent-whitelist") + for _, v := range uaw { + parts := strings.Split(v, ":") + if len(parts) != 2 { + return fmt.Errorf("unexpected config value for user-agent whitelist: %v, expected format is userAgent:challenge", v) + } + + cfg.Reva.Middleware.Auth.CredentialsByUserAgent[parts[0]] = parts[1] + } + return ParseConfig(ctx, cfg) }, Action: func(c *cli.Context) error { @@ -288,6 +298,7 @@ func loadMiddlewares(ctx context.Context, l log.Logger, cfg *config.Config) alic middleware.EnableBasicAuth(cfg.EnableBasicAuth), middleware.AccountsClient(accountsClient), middleware.OIDCIss(cfg.OIDC.Issuer), + middleware.CredentialsByUserAgent(cfg.Reva.Middleware.Auth.CredentialsByUserAgent), ), middleware.SignedURLAuth( middleware.Logger(l), diff --git a/proxy/pkg/config/config.go b/proxy/pkg/config/config.go index 388d81914..769f597f1 100644 --- a/proxy/pkg/config/config.go +++ b/proxy/pkg/config/config.go @@ -80,7 +80,16 @@ var ( // Reva defines all available REVA configuration. type Reva struct { - Address string + Address string + Middleware Middleware +} + +type Middleware struct { + Auth Auth +} + +type Auth struct { + CredentialsByUserAgent map[string]string } // Cache is a TTL cache configuration. diff --git a/proxy/pkg/flagset/flagset.go b/proxy/pkg/flagset/flagset.go index 3a1371a40..35a60852b 100644 --- a/proxy/pkg/flagset/flagset.go +++ b/proxy/pkg/flagset/flagset.go @@ -248,8 +248,15 @@ func ServerWithConfig(cfg *config.Config) []cli.Flag { EnvVars: []string{"PROXY_ENABLE_BASIC_AUTH"}, Destination: &cfg.EnableBasicAuth, }, - } + // Reva Middlewares Config + &cli.StringSliceFlag{ + Name: "proxy-user-agent-whitelist", // TODO naming? + Value: cli.NewStringSlice(""), + Usage: "TODO", + EnvVars: []string{"PROXY_MIDDLEWARE_AUTH_CREDENTIALS_BY_USER_AGENT"}, + }, + } } // ListProxyWithConfig applies the config to the list commands flags. diff --git a/proxy/pkg/middleware/authentication.go b/proxy/pkg/middleware/authentication.go index 61d8be138..79a3964d0 100644 --- a/proxy/pkg/middleware/authentication.go +++ b/proxy/pkg/middleware/authentication.go @@ -42,6 +42,7 @@ func Authentication(opts ...Option) func(next http.Handler) http.Handler { OIDCIss(options.OIDCIss), TokenCacheSize(options.UserinfoCacheSize), TokenCacheTTL(time.Second*time.Duration(options.UserinfoCacheTTL)), + CredentialsByUserAgent(options.CredentialsByUserAgent), ) basic := BasicAuth( @@ -49,6 +50,7 @@ func Authentication(opts ...Option) func(next http.Handler) http.Handler { EnableBasicAuth(options.EnableBasicAuth), AccountsClient(options.AccountsClient), OIDCIss(options.OIDCIss), + CredentialsByUserAgent(options.CredentialsByUserAgent), ) return func(next http.Handler) http.Handler { diff --git a/proxy/pkg/middleware/basic_auth.go b/proxy/pkg/middleware/basic_auth.go index eb4cf7f3d..c0bea0fb7 100644 --- a/proxy/pkg/middleware/basic_auth.go +++ b/proxy/pkg/middleware/basic_auth.go @@ -37,21 +37,47 @@ func BasicAuth(optionSetters ...Option) func(next http.Handler) http.Handler { if !h.isPublicLink(req) { for i := 0; i < len(ProxyWwwAuthenticate); i++ { if strings.Contains(req.RequestURI, fmt.Sprintf("/%v/", ProxyWwwAuthenticate[i])) { + for k, v := range options.CredentialsByUserAgent { + if strings.Contains(k, req.UserAgent()) { + w.Header().Del("Www-Authenticate") + w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(v), req.Host)) + goto OUT + } + } w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", "Basic", req.Host)) } } } + OUT: next.ServeHTTP(w, req) return } + w.Header().Del("Www-Authenticate") + account, ok := h.getAccount(req) + + // touch is a user agent locking guard, when touched changes to true it indicates the User-Agent on the + // request is configured to support only one challenge, it it remains untouched, there are no considera- + // tions and we should write all available authentication challenges to the response. + touch := false + if !ok { - for i := 0; i < len(ProxyWwwAuthenticate); i++ { - if strings.Contains(req.RequestURI, fmt.Sprintf("/%v/", ProxyWwwAuthenticate[i])) { - w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", "Basic", req.Host)) + // if the request is bound to a user agent the locked write Www-Authenticate for such user + for k, v := range options.CredentialsByUserAgent { + if strings.Contains(k, req.UserAgent()) { + w.Header().Del("Www-Authenticate") + w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(v), req.Host)) + touch = true + break } } + + // if the request is not bound to any user agent, write all available challenges + if !touch { + writeSupportedAuthenticateHeader(w, req) + } + w.WriteHeader(http.StatusUnauthorized) return } diff --git a/proxy/pkg/middleware/oidc_auth.go b/proxy/pkg/middleware/oidc_auth.go index 73b46cc0e..b91c3b395 100644 --- a/proxy/pkg/middleware/oidc_auth.go +++ b/proxy/pkg/middleware/oidc_auth.go @@ -41,18 +41,19 @@ func OIDCAuth(optionSetters ...Option) func(next http.Handler) http.Handler { // there is no bearer token on the request, if !h.shouldServe(req) { // oidc supported but token not present, add header and handover to the next middleware. - - // TODO for this logic to work and we don't return superfluous Www-Authenticate headers we would need to - // add Www-Authenticate only on selected endpoints, because Reva won't cleanup already written headers. - // this means that requests such as: - // curl -v -k -u admin:admin -H "depth: 0" -X PROPFIND https://localhost:9200/remote.php/dav/files | xmllint --format - - // even when succeeding, will contain a Www-Authenticate header. - for i := 0; i < len(ProxyWwwAuthenticate); i++ { if strings.Contains(req.RequestURI, fmt.Sprintf("/%v/", ProxyWwwAuthenticate[i])) { + for k, v := range options.CredentialsByUserAgent { + if strings.Contains(k, req.UserAgent()) { + w.Header().Del("Www-Authenticate") + w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", strings.Title(v), req.Host)) + goto OUT + } + } w.Header().Add("Www-Authenticate", fmt.Sprintf("%v realm=\"%s\", charset=\"UTF-8\"", "Bearer", req.Host)) } } + OUT: next.ServeHTTP(w, req) return } diff --git a/proxy/pkg/middleware/options.go b/proxy/pkg/middleware/options.go index 9d5d0c1ca..27906f9a5 100644 --- a/proxy/pkg/middleware/options.go +++ b/proxy/pkg/middleware/options.go @@ -46,6 +46,8 @@ type Options struct { UserinfoCacheSize int // UserinfoCacheTTL sets the max cache duration for the userinfo cache, intended for the oidc_auth middleware UserinfoCacheTTL time.Duration + // CredentialsByUserAgent sets the auth challenges on a per user-agent basis + CredentialsByUserAgent map[string]string } // newOptions initializes the available default options. @@ -108,6 +110,13 @@ func OIDCIss(iss string) Option { } } +// CredentialsByUserAgent sets UserAgentChallenges. +func CredentialsByUserAgent(v map[string]string) Option { + return func(o *Options) { + o.CredentialsByUserAgent = v + } +} + // RevaGatewayClient provides a function to set the the reva gateway service client option. func RevaGatewayClient(gc gateway.GatewayAPIClient) Option { return func(o *Options) { diff --git a/storage/pkg/command/frontend.go b/storage/pkg/command/frontend.go index 80e1225a6..7b6dac0f1 100644 --- a/storage/pkg/command/frontend.go +++ b/storage/pkg/command/frontend.go @@ -32,7 +32,7 @@ func Frontend(cfg *config.Config) *cli.Command { for _, v := range uaw { parts := strings.Split(v, ":") if len(parts) != 2 { - return fmt.Errorf("unexpected config value for user-agent whitelist: %v, expected format is user-agent:challenge", v) // TODO wording + error wrapping? + return fmt.Errorf("unexpected config value for user-agent whitelist: %v, expected format is user-agent:challenge", v) } cfg.Reva.Frontend.Middleware.Auth.CredentialsByUserAgent[parts[0]] = parts[1] diff --git a/storage/pkg/flagset/frontend.go b/storage/pkg/flagset/frontend.go index a3267ed73..74c60ebdc 100644 --- a/storage/pkg/flagset/frontend.go +++ b/storage/pkg/flagset/frontend.go @@ -134,7 +134,7 @@ func FrontendWithConfig(cfg *config.Config) []cli.Flag { Destination: &cfg.Reva.UploadHTTPMethodOverride, }, - // Middlewares + // Reva Middlewares Config &cli.StringSliceFlag{ Name: "user-agent-whitelist", // TODO naming? Value: cli.NewStringSlice("test"),