From a61211d32c20f08b28aa605af7c3918a9a7a7941 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Wed, 10 Aug 2022 21:42:58 +0200 Subject: [PATCH] OIDC: allow to get the role field from a sub-struct Signed-off-by: Nicola Murino --- docs/full-configuration.md | 2 +- go.mod | 4 +-- go.sum | 8 +++--- internal/httpd/oidc.go | 42 ++++++++++++++++++++++++----- internal/httpd/oidc_test.go | 54 +++++++++++++++++++++++++++++++++++++ 5 files changed, 97 insertions(+), 13 deletions(-) diff --git a/docs/full-configuration.md b/docs/full-configuration.md index f1f125d1..f74c8a7d 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -281,7 +281,7 @@ The configuration file contains the following sections: - `redirect_base_url`, string. Defines the base URL to redirect to after OpenID authentication. The suffix `/web/oidc/redirect` will be added to this base URL, adding also the `web_root` if configured. Default: blank. - `username_field`, string. Defines the ID token claims field to map to the SFTPGo username. Default: blank. - `scopes`, list of strings. Request the OAuth provider to provide the scope information from an authenticated users. The `openid` scope is mandatory. Default: `"openid", "profile", "email"`. - - `role_field`, string. Defines the optional ID token claims field to map to a SFTPGo role. If the defined ID token claims field is set to `admin` the authenticated user is mapped to an SFTPGo admin. You don't need to specify this field if you want to use OpenID only for the Web Client UI. Default: blank. + - `role_field`, string. Defines the optional ID token claims field to map to a SFTPGo role. If the defined ID token claims field is set to `admin` the authenticated user is mapped to an SFTPGo admin. You don't need to specify this field if you want to use OpenID only for the Web Client UI. If the field is inside a nested structure, you can use the dot notation to traverse the structures. Default: blank. - `implicit_roles`, boolean. If set, the `role_field` is ignored and the SFTPGo role is assumed based on the login link used. Default: `false`. - `custom_fields`, list of strings. Custom token claims fields to pass to the pre-login hook. Default: empty. - `debug`, boolean. If set, the received id tokens will be logged at debug level. Default: `false`. diff --git a/go.mod b/go.mod index ff7b17d0..64572d10 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,10 @@ require ( github.com/GehirnInc/crypt v0.0.0-20200316065508-bb7000b8a962 github.com/alexedwards/argon2id v0.0.0-20211130144151-3585854a6387 github.com/aws/aws-sdk-go-v2 v1.16.10 - github.com/aws/aws-sdk-go-v2/config v1.15.17 + github.com/aws/aws-sdk-go-v2/config v1.16.0 github.com/aws/aws-sdk-go-v2/credentials v1.12.12 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.11 - github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.23 + github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.24 github.com/aws/aws-sdk-go-v2/service/marketplacemetering v1.13.11 github.com/aws/aws-sdk-go-v2/service/s3 v1.27.4 github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.15.16 diff --git a/go.sum b/go.sum index 7303ca41..4f9414ee 100644 --- a/go.sum +++ b/go.sum @@ -150,8 +150,8 @@ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1/go.mod h1:n8Bs1ElDD github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4 h1:zfT11pa7ifu/VlLDpmc5OY2W4nYmnKkFDGeMVnmqAI0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.4/go.mod h1:ES0I1GBs+YYgcDS1ek47Erbn4TOL811JKqBXtgzqyZ8= github.com/aws/aws-sdk-go-v2/config v1.15.3/go.mod h1:9YL3v07Xc/ohTsxFXzan9ZpFpdTOFl4X65BAKYaz8jg= -github.com/aws/aws-sdk-go-v2/config v1.15.17 h1:cM/4dqEPc5SjBOeYVdUI7iL/B6jDupCesXzg3AuUzRE= -github.com/aws/aws-sdk-go-v2/config v1.15.17/go.mod h1:eatrtwIm5WdvASoYCy5oPkinfiwiYFg2jLG9tJoKzkE= +github.com/aws/aws-sdk-go-v2/config v1.16.0 h1:LxHC50cwOLxYo67NEpwpNUiOi6ngXfDpEETphSZ6bAw= +github.com/aws/aws-sdk-go-v2/config v1.16.0/go.mod h1:eatrtwIm5WdvASoYCy5oPkinfiwiYFg2jLG9tJoKzkE= github.com/aws/aws-sdk-go-v2/credentials v1.11.2/go.mod h1:j8YsY9TXTm31k4eFhspiQicfXPLZ0gYXA50i4gxPE8g= github.com/aws/aws-sdk-go-v2/credentials v1.12.12 h1:iShu6VaWZZZfUZvlGtRjl+g1lWk44g1QmiCTD4KS0jI= github.com/aws/aws-sdk-go-v2/credentials v1.12.12/go.mod h1:vFHC2HifIWHebmoVsfpqliKuqbAY2LaVlvy03JzF4c4= @@ -159,8 +159,8 @@ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.3/go.mod h1:uk1vhHHERfSVCUnq github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.11 h1:zZHPdM2x09/0F8D7XyVvQnP2/jaW7bEMmtcSCPYq/iI= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.12.11/go.mod h1:38Asv/UyQbDNpSXCurZRlDMjzIl6J+wUe8vY3TtUuzA= github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.3/go.mod h1:0dHuD2HZZSiwfJSy1FO5bX1hQ1TxVV1QXXjpn3XUE44= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.23 h1:lzS1GSHBzvBMlCA030/ecL5tF2ip8RLr/LBq5fBpv/4= -github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.23/go.mod h1:yGuKwoNVv2eGUHlp7ciCQLHmFNeESebnHucZfRL9EkA= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.24 h1:9MflwbI3Ua4PFyCNo39nnJ2ZYaQ/GabPUPdutegSJUs= +github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.11.24/go.mod h1:W970x9QKWWb0Y30Num5dFFji/qRQSt0UP4UzbM3sYCo= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.9/go.mod h1:AnVH5pvai0pAF4lXRq0bmhbes1u9R8wTE+g+183bZNM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.17 h1:U8DZvyFFesBmK62dYC6BRXm4Cd/wPP3aPcecu3xv/F4= github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.17/go.mod h1:6qtGip7sJEyvgsLjphRZWF9qPe3xJf1mL/MM01E35Wc= diff --git a/internal/httpd/oidc.go b/internal/httpd/oidc.go index 9d4118e5..5a8a4bb8 100644 --- a/internal/httpd/oidc.go +++ b/internal/httpd/oidc.go @@ -225,12 +225,7 @@ func (t *oidcToken) parseClaims(claims map[string]any, usernameField, roleField if forcedRole != "" { t.Role = forcedRole } else { - if roleField != "" { - role, ok := claims[roleField] - if ok { - t.Role = role - } - } + t.getRoleFromField(claims, roleField) } t.CustomFields = nil if len(customFields) > 0 { @@ -254,6 +249,41 @@ func (t *oidcToken) parseClaims(claims map[string]any, usernameField, roleField return nil } +func (t *oidcToken) getRoleFromField(claims map[string]any, roleField string) { + if roleField != "" { + role, ok := claims[roleField] + if ok { + t.Role = role + return + } + if !strings.Contains(roleField, ".") { + return + } + + getStructValue := func(outer any, field string) (any, bool) { + switch val := outer.(type) { + case map[string]any: + res, ok := val[field] + return res, ok + } + return nil, false + } + + for idx, field := range strings.Split(roleField, ".") { + if idx == 0 { + role, ok = getStructValue(claims, field) + } else { + role, ok = getStructValue(role, field) + } + if !ok { + return + } + } + + t.Role = role + } +} + func (t *oidcToken) isAdmin() bool { switch v := t.Role.(type) { case string: diff --git a/internal/httpd/oidc_test.go b/internal/httpd/oidc_test.go index 71df439f..7780c003 100644 --- a/internal/httpd/oidc_test.go +++ b/internal/httpd/oidc_test.go @@ -1156,6 +1156,7 @@ func TestOIDCIsAdmin(t *testing.T) { {input: append(emptySlice, 1), want: false}, {input: 1, want: false}, {input: nil, want: false}, + {input: map[string]string{"admin": "admin"}, want: false}, } for _, tc := range tests { token := oidcToken{ @@ -1165,6 +1166,59 @@ func TestOIDCIsAdmin(t *testing.T) { } } +func TestParseAdminRole(t *testing.T) { + claims := make(map[string]any) + rawClaims := []byte(`{ + "sub": "35666371", + "email": "example@example.com", + "preferred_username": "Sally", + "name": "Sally Tyler", + "updated_at": "2018-04-13T22:08:45Z", + "given_name": "Sally", + "family_name": "Tyler", + "params": { + "sftpgo_role": "admin", + "subparams": { + "sftpgo_role": "admin", + "inner": { + "sftpgo_role": ["user","admin"] + } + } + }, + "at_hash": "lPLhxI2wjEndc-WfyroDZA", + "rt_hash": "mCmxPtA04N-55AxlEUbq-A", + "aud": "78d1d040-20c9-0136-5146-067351775fae92920", + "exp": 1523664997, + "iat": 1523657797 + }`) + err := json.Unmarshal(rawClaims, &claims) + assert.NoError(t, err) + + type test struct { + input string + want bool + } + + tests := []test{ + {input: "sftpgo_role", want: false}, + {input: "params.sftpgo_role", want: true}, + {input: "params.subparams.sftpgo_role", want: true}, + {input: "params.subparams.inner.sftpgo_role", want: true}, + {input: "email", want: false}, + {input: "missing", want: false}, + {input: "params.email", want: false}, + {input: "missing.sftpgo_role", want: false}, + {input: "params", want: false}, + {input: "params.subparams.inner.sftpgo_role.missing", want: false}, + } + + for _, tc := range tests { + token := oidcToken{} + token.getRoleFromField(claims, tc.input) + assert.Equal(t, tc.want, token.isAdmin(), "%q should return %t", tc.input, tc.want) + } +} + func TestOIDCWithLoginFormsDisabled(t *testing.T) { oidcMgr, ok := oidcMgr.(*memoryOIDCManager) require.True(t, ok)