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 all commits
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
12 changes: 12 additions & 0 deletions changelog/unreleased/auth-by-user-agent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Enhancement: Add auth protocol based on user agent

Previously, all available credential challenges are given to the client,
for example, basic auth, bearer token, etc ...
Different clients have different priorities to use one method or another,
and before it was not possible to force a client to use one method without
having a side effect on other clients.

This PR adds the functionality to target a specific auth protocol based
on the user agent HTTP header.

https://github.com/cs3org/reva/pull/1350
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,31 @@ description: >
# _struct: config_

{{% dir name="mount_path" type="string" default="/" %}}
The path where the file system would be mounted. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L53)
The path where the file system would be mounted. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L52)
{{< highlight toml >}}
[grpc.services.storageprovider]
mount_path = "/"
{{< /highlight >}}
{{% /dir %}}

{{% dir name="mount_id" type="string" default="-" %}}
The ID of the mounted file system. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L54)
The ID of the mounted file system. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L53)
{{< highlight toml >}}
[grpc.services.storageprovider]
mount_id = "-"
{{< /highlight >}}
{{% /dir %}}

{{% dir name="driver" type="string" default="localhome" %}}
The storage driver to be used. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L55)
The storage driver to be used. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L54)
{{< highlight toml >}}
[grpc.services.storageprovider]
driver = "localhome"
{{< /highlight >}}
{{% /dir %}}

{{% dir name="drivers" type="map[string]map[string]interface{}" default="localhome" %}}
[[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L56)
[[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L55)
{{< highlight toml >}}
[grpc.services.storageprovider.drivers.localhome]
root = "/var/tmp/reva/"
Expand All @@ -44,39 +44,39 @@ user_layout = "{{.Username}}"
{{% /dir %}}

{{% dir name="tmp_folder" type="string" default="/var/tmp" %}}
Path to temporary folder. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L57)
Path to temporary folder. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L56)
{{< highlight toml >}}
[grpc.services.storageprovider]
tmp_folder = "/var/tmp"
{{< /highlight >}}
{{% /dir %}}

{{% dir name="data_server_url" type="string" default="http://localhost/data" %}}
The URL for the data server. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L58)
The URL for the data server. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L57)
{{< highlight toml >}}
[grpc.services.storageprovider]
data_server_url = "http://localhost/data"
{{< /highlight >}}
{{% /dir %}}

{{% dir name="expose_data_server" type="bool" default=false %}}
Whether to expose data server. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L59)
Whether to expose data server. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L58)
{{< highlight toml >}}
[grpc.services.storageprovider]
expose_data_server = false
{{< /highlight >}}
{{% /dir %}}

{{% dir name="available_checksums" type="map[string]uint32" default=nil %}}
List of available checksums. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L60)
List of available checksums. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L59)
{{< highlight toml >}}
[grpc.services.storageprovider]
available_checksums = nil
{{< /highlight >}}
{{% /dir %}}

{{% dir name="mimetypes" type="map[string]string" default=nil %}}
List of supported mime types and corresponding file extensions. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L61)
List of supported mime types and corresponding file extensions. [[Ref]](https://github.com/cs3org/reva/tree/master/internal/grpc/services/storageprovider/storageprovider.go#L60)
{{< highlight toml >}}
[grpc.services.storageprovider]
mimetypes = nil
Expand Down
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 %}}

72 changes: 52 additions & 20 deletions internal/http/interceptors/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,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 +94,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 +109,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 @@ -158,27 +163,34 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err
return
}

// check for token
tkn := tokenStrategy.GetToken(r)
if tkn == "" {
log.Warn().Msg("core access token not set")

userAgentCredKeys := getCredsForUserAgent(r.UserAgent(), conf.CredentialsByUserAgent, conf.CredentialChain)

// obtain credentials (basic auth, bearer token, ...) based on user agent
var creds *auth.Credentials
for i := range credChain {
creds, err = credChain[i].GetCredentials(w, r)
for _, k := range userAgentCredKeys {
creds, err = credChain[k].GetCredentials(w, r)
if err != nil {
log.Debug().Err(err).Msg("error retrieving credentials")
}

if creds != nil {
log.Debug().Msgf("credentials obtained from credential strategy: type: %s, client_id: %s", creds.Type, creds.ClientID)
break
}
}

// if no credentials are found, reply with authentication challenge depending on user agent
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)
for _, key := range userAgentCredKeys {
if cred, ok := credChain[key]; ok {
cred.AddWWWAuthenticate(w, r, conf.Realm)
} else {
panic("auth credential strategy: " + key + "must have been loaded in init method")
}
}
w.WriteHeader(http.StatusUnauthorized)
return
Expand Down Expand Up @@ -247,3 +259,23 @@ func New(m map[string]interface{}, unprotected []string) (global.Middleware, err
}
return chain, nil
}

// getCredsForUserAgent returns the WWW Authenticate challenges keys to use given an http request
// and available credentials.
func getCredsForUserAgent(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 creds
}
109 changes: 109 additions & 0 deletions internal/http/interceptors/auth/auth_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// 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 TestGetCredsForUserAgent(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{"basic"},
expected: []string{"basic"},
},

// map set but user agent not in map
&test{
userAgent: "curl",
userAgentMap: map[string]string{"mirall": "basic"},
availableCredentials: []string{"basic", "bearer"},
expected: []string{"basic", "bearer"},
},

// 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 := getCredsForUserAgent(
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)
}