瀏覽代碼

OIDC: allow to get the role field from a sub-struct

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
Nicola Murino 3 年之前
父節點
當前提交
88bfdb9910
共有 3 個文件被更改,包括 90 次插入7 次删除
  1. 1 1
      docs/full-configuration.md
  2. 36 6
      httpd/oidc.go
  3. 53 0
      httpd/oidc_test.go

+ 1 - 1
docs/full-configuration.md

@@ -282,7 +282,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.
       - `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.
       - `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"`.
       - `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`.
       - `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.
       - `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`.
       - `debug`, boolean. If set, the received id tokens will be logged at debug level. Default: `false`.

+ 36 - 6
httpd/oidc.go

@@ -225,12 +225,7 @@ func (t *oidcToken) parseClaims(claims map[string]any, usernameField, roleField
 	if forcedRole != "" {
 	if forcedRole != "" {
 		t.Role = forcedRole
 		t.Role = forcedRole
 	} else {
 	} else {
-		if roleField != "" {
-			role, ok := claims[roleField]
-			if ok {
-				t.Role = role
-			}
-		}
+		t.getRoleFromField(claims, roleField)
 	}
 	}
 	t.CustomFields = nil
 	t.CustomFields = nil
 	if len(customFields) > 0 {
 	if len(customFields) > 0 {
@@ -254,6 +249,41 @@ func (t *oidcToken) parseClaims(claims map[string]any, usernameField, roleField
 	return nil
 	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 {
 func (t *oidcToken) isAdmin() bool {
 	switch v := t.Role.(type) {
 	switch v := t.Role.(type) {
 	case string:
 	case string:

+ 53 - 0
httpd/oidc_test.go

@@ -1163,6 +1163,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) {
 func TestOIDCWithLoginFormsDisabled(t *testing.T) {
 	oidcMgr, ok := oidcMgr.(*memoryOIDCManager)
 	oidcMgr, ok := oidcMgr.(*memoryOIDCManager)
 	require.True(t, ok)
 	require.True(t, ok)