Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

auth: add www-authenticate based on user agent #1350

Merged
merged 6 commits into from
Dec 3, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,23 @@ 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"
{{< /highlight >}}
{{% /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"
{{< /highlight >}}
{{% /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/"
Expand All @@ -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 %}}

87 changes: 73 additions & 14 deletions internal/http/interceptors/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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) {
Expand Down Expand Up @@ -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])
Expand All @@ -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]
Expand Down Expand Up @@ -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
labkode marked this conversation as resolved.
Show resolved Hide resolved
log.Warn().Msgf("auth: configured user-agent credential is not loaded: %s", cred)
}
}

}
// if no user agent is match, return all available challengues
labkode marked this conversation as resolved.
Show resolved Hide resolved
if !match {
for i := range credChain {
credChain[i].AddWWWAuthenticate(w, r, conf.Realm)
}
}
}

w.WriteHeader(http.StatusUnauthorized)
return
}
Expand Down Expand Up @@ -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
}
101 changes: 101 additions & 0 deletions internal/http/interceptors/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -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"},
labkode marked this conversation as resolved.
Show resolved Hide resolved
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)
}