From f846b3dbebae057ff9d647bc4ed2cade094b4a42 Mon Sep 17 00:00:00 2001 From: Hugo Gonzalez Labrador Date: Tue, 1 Dec 2020 08:52:50 +0100 Subject: [PATCH] auth: add www-authenticate based on user agent --- .../http/services/dataprovider/_index.md | 14 ++- internal/http/interceptors/auth/auth.go | 87 ++++++++++++--- internal/http/interceptors/auth/auth_test.go | 101 ++++++++++++++++++ 3 files changed, 185 insertions(+), 17 deletions(-) create mode 100644 internal/http/interceptors/auth/auth_test.go diff --git a/docs/content/en/docs/config/http/services/dataprovider/_index.md b/docs/content/en/docs/config/http/services/dataprovider/_index.md index 39a32e3637..ac4426a971 100644 --- a/docs/content/en/docs/config/http/services/dataprovider/_index.md +++ b/docs/content/en/docs/config/http/services/dataprovider/_index.md @@ -9,7 +9,7 @@ description: > # _struct: config_ {{% dir name="prefix" type="string" default="data" %}} -The prefix to be used for this HTTP service [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L39) +The prefix to be used for this HTTP service [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L40) {{< highlight toml >}} [http.services.dataprovider] prefix = "data" @@ -17,7 +17,7 @@ prefix = "data" {{% /dir %}} {{% dir name="driver" type="string" default="localhome" %}} -The storage driver to be used. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L40) +The storage driver to be used. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L41) {{< highlight toml >}} [http.services.dataprovider] driver = "localhome" @@ -25,7 +25,7 @@ driver = "localhome" {{% /dir %}} {{% dir name="drivers" type="map[string]map[string]interface{}" default="localhome" %}} -The configuration for the storage driver [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L41) +The configuration for the storage driver [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L42) {{< highlight toml >}} [http.services.dataprovider.drivers.localhome] root = "/var/tmp/reva/" @@ -35,3 +35,11 @@ user_layout = "{{.Username}}" {{< /highlight >}} {{% /dir %}} +{{% dir name="data_txs" type="map[string]map[string]interface{}" default="simple" %}} +The configuration for the data tx protocols [[Ref]](https://github.com/cs3org/reva/tree/master/internal/http/services/dataprovider/dataprovider.go#L43) +{{< highlight toml >}} +[http.services.dataprovider.data_txs.simple] + +{{< /highlight >}} +{{% /dir %}} + diff --git a/internal/http/interceptors/auth/auth.go b/internal/http/interceptors/auth/auth.go index 055552a452..77ee475cac 100644 --- a/internal/http/interceptors/auth/auth.go +++ b/internal/http/interceptors/auth/auth.go @@ -21,6 +21,7 @@ package auth import ( "fmt" "net/http" + "strings" gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1" userpb "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" @@ -47,15 +48,16 @@ type config struct { Priority int `mapstructure:"priority"` GatewaySvc string `mapstructure:"gatewaysvc"` // TODO(jdf): Realm is optional, will be filled with request host if not given? - Realm string `mapstructure:"realm"` - CredentialChain []string `mapstructure:"credential_chain"` - CredentialStrategies map[string]map[string]interface{} `mapstructure:"credential_strategies"` - TokenStrategy string `mapstructure:"token_strategy"` - TokenStrategies map[string]map[string]interface{} `mapstructure:"token_strategies"` - TokenManager string `mapstructure:"token_manager"` - TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"` - TokenWriter string `mapstructure:"token_writer"` - TokenWriters map[string]map[string]interface{} `mapstructure:"token_writers"` + Realm string `mapstructure:"realm"` + CredentialsByUserAgent map[string]string `mapstructure:"credentials_by_user_agent"` + CredentialChain []string `mapstructure:"credential_chain"` + CredentialStrategies map[string]map[string]interface{} `mapstructure:"credential_strategies"` + TokenStrategy string `mapstructure:"token_strategy"` + TokenStrategies map[string]map[string]interface{} `mapstructure:"token_strategies"` + TokenManager string `mapstructure:"token_manager"` + TokenManagers map[string]map[string]interface{} `mapstructure:"token_managers"` + TokenWriter string `mapstructure:"token_writer"` + TokenWriters map[string]map[string]interface{} `mapstructure:"token_writers"` } func parseConfig(m map[string]interface{}) (*config, error) { @@ -93,8 +95,12 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err conf.CredentialChain = []string{"basic", "bearer"} } - credChain := []auth.CredentialStrategy{} - for i := range conf.CredentialChain { + if conf.CredentialsByUserAgent == nil { + conf.CredentialsByUserAgent = map[string]string{} + } + + credChain := map[string]auth.CredentialStrategy{} + for i, key := range conf.CredentialChain { f, ok := registry.NewCredentialFuncs[conf.CredentialChain[i]] if !ok { return nil, fmt.Errorf("credential strategy not found: %s", conf.CredentialChain[i]) @@ -104,7 +110,7 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err if err != nil { return nil, err } - credChain = append(credChain, credStrategy) + credChain[key] = credStrategy } g, ok := tokenregistry.NewTokenFuncs[conf.TokenStrategy] @@ -176,10 +182,43 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err if creds == nil { // TODO read realm from forwarded for header? // see https://github.com/stanvit/go-forwarded as middleware + // indicate all possible authentications to the client - for i := range credChain { - credChain[i].AddWWWAuthenticate(w, r, conf.Realm) + // if the CredentialsByUserAgent is set, forward only configured credential + // challenge. + + userAgent := r.UserAgent() + if len(conf.CredentialsByUserAgent) == 0 || userAgent == "" { + // set all available credentials challenges + for i := range credChain { + credChain[i].AddWWWAuthenticate(w, r, conf.Realm) + } + + } else { + // set credentials depending on user agent. + // if not match, set all available credentials. + var match bool + for k, cred := range conf.CredentialsByUserAgent { + if strings.Contains(userAgent, k) { + if challenge, ok := credChain[cred]; ok { + challenge.AddWWWAuthenticate(w, r, conf.Realm) + match = true + continue + } else { + // warm that configured credential is not loaded + log.Warn().Msgf("auth: configured user-agent credential is not loaded: %s", cred) + } + } + + } + // if no user agent is match, return all available challengues + if !match { + for i := range credChain { + credChain[i].AddWWWAuthenticate(w, r, conf.Realm) + } + } } + w.WriteHeader(http.StatusUnauthorized) return } @@ -247,3 +286,23 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err } return chain, nil } + +// applyWWWAuthenticate returns the WWW Authenticate challenges keys to use given an http request +// and available credentials. +func applyWWWAuthenticate(ua string, uam map[string]string, creds []string) []string { + if ua == "" || len(uam) == 0 { + return creds + } + + cred, ok := uam[ua] + if ok { + for _, v := range creds { + if v == cred { + return []string{cred} + } + } + return creds + } + + return nil +} diff --git a/internal/http/interceptors/auth/auth_test.go b/internal/http/interceptors/auth/auth_test.go new file mode 100644 index 0000000000..50f60025f3 --- /dev/null +++ b/internal/http/interceptors/auth/auth_test.go @@ -0,0 +1,101 @@ +// Copyright 2018-2020 CERN +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +// In applying this license, CERN does not waive the privileges and immunities +// granted to it by virtue of its status as an Intergovernmental Organization +// or submit itself to any jurisdiction. + +package auth + +import ( + "testing" +) + +func TestApplyWWWAuthenticate(t *testing.T) { + type test struct { + userAgent string + userAgentMap map[string]string + availableCredentials []string + expected []string + } + + tests := []*test{ + // no user agent we return all available credentials + &test{ + userAgent: "", + userAgentMap: map[string]string{}, + availableCredentials: []string{}, + expected: []string{}, + }, + + // no user map we return all available credentials + &test{ + userAgent: "mirall", + userAgentMap: map[string]string{}, + availableCredentials: []string{"basic"}, + expected: []string{"basic"}, + }, + + // user agent set but no mapping set we return all credentials + &test{ + userAgent: "mirall", + userAgentMap: map[string]string{}, + availableCredentials: []string{"basic"}, + expected: []string{"basic"}, + }, + + // user mapping set to non available credential, we return all available + &test{ + userAgent: "mirall", + userAgentMap: map[string]string{"mirall": "notfound"}, + availableCredentials: []string{"basic", "bearer"}, + expected: []string{"basic", "bearer"}, + }, + + // user mapping set and we return only desired credential + &test{ + userAgent: "mirall", + userAgentMap: map[string]string{"mirall": "bearer"}, + availableCredentials: []string{"basic", "bearer"}, + expected: []string{"bearer"}, + }, + } + + for _, test := range tests { + got := applyWWWAuthenticate( + test.userAgent, + test.userAgentMap, + test.availableCredentials) + + if !match(got, test.expected) { + fail(t, got, test.expected) + } + } +} + +func match(a, b []string) bool { + if len(a) != len(b) { + return false + } + for i, v := range a { + if v != b[i] { + return false + } + } + return true +} + +func fail(t *testing.T, got, expected []string) { + t.Fatalf("got: %+v expected: %+v", got, expected) +}