diff --git a/changes/40642-add-new-fleet-saml-attribute-prefix b/changes/40642-add-new-fleet-saml-attribute-prefix new file mode 100644 index 0000000000..201a1aa1eb --- /dev/null +++ b/changes/40642-add-new-fleet-saml-attribute-prefix @@ -0,0 +1 @@ +- Added ability to use `FLEET_JIT_USER_ROLE_FLEET_` as a prefix on SAML attributes diff --git a/server/fleet/sessions.go b/server/fleet/sessions.go index 6a1d5cce18..3cb9aea44f 100644 --- a/server/fleet/sessions.go +++ b/server/fleet/sessions.go @@ -111,15 +111,16 @@ func (s SSORolesInfo) IsSet() bool { } const ( - globalUserRoleSSOAttrName = "FLEET_JIT_USER_ROLE_GLOBAL" - teamUserRoleSSOAttrNamePrefix = "FLEET_JIT_USER_ROLE_TEAM_" - ssoAttrNullRoleValue = "null" + globalUserRoleSSOAttrName = "FLEET_JIT_USER_ROLE_GLOBAL" + teamUserRoleSSOAttrNamePrefix = "FLEET_JIT_USER_ROLE_TEAM_" + teamUserRoleSSOAttrNamePrefixV2 = "FLEET_JIT_USER_ROLE_FLEET_" + ssoAttrNullRoleValue = "null" ) // RolesFromSSOAttributes loads Global and Team roles from SAML custom attributes. // - Custom attribute `FLEET_JIT_USER_ROLE_GLOBAL` is used for setting global role. -// - Custom attributes of the form `FLEET_JIT_USER_ROLE_TEAM_` are used -// for setting role for a team with ID . +// - Custom attributes of the form `FLEET_JIT_USER_ROLE_TEAM_` or +// `FLEET_JIT_USER_ROLE_FLEET_` are used for setting role for a fleet with ID . // // For both attributes currently supported values are `admin`, `maintainer`, `observer`, // `observer_plus`, `technician` and `null`. A `null` value is used to ignore the attribute. @@ -137,8 +138,12 @@ func RolesFromSSOAttributes(attributes []SAMLAttribute) (SSORolesInfo, error) { continue } ssoRolesInfo.Global = ptr.String(role) - case strings.HasPrefix(attribute.Name, teamUserRoleSSOAttrNamePrefix): + case strings.HasPrefix(attribute.Name, teamUserRoleSSOAttrNamePrefix), + strings.HasPrefix(attribute.Name, teamUserRoleSSOAttrNamePrefixV2): + // Get rid of any prefix, v1 or v2. teamIDSuffix := strings.TrimPrefix(attribute.Name, teamUserRoleSSOAttrNamePrefix) + teamIDSuffix = strings.TrimPrefix(teamIDSuffix, teamUserRoleSSOAttrNamePrefixV2) + // Parse the fleet ID from what's left. teamID, err := strconv.ParseUint(teamIDSuffix, 10, 32) if err != nil { return SSORolesInfo{}, fmt.Errorf("parse team ID: %w", err) diff --git a/server/fleet/sessions_test.go b/server/fleet/sessions_test.go index 4549977fc6..0e8c3d159a 100644 --- a/server/fleet/sessions_test.go +++ b/server/fleet/sessions_test.go @@ -349,6 +349,133 @@ func TestRolesFromSSOAttributes(t *testing.T) { }, }, }, + { + name: "v2-prefix-all-teams", + attributes: []SAMLAttribute{ + { + Name: "FLEET_JIT_USER_ROLE_FLEET_1", + Values: []SAMLAttributeValue{ + {Value: "observer"}, + }, + }, + { + Name: "FLEET_JIT_USER_ROLE_FLEET_2", + Values: []SAMLAttributeValue{ + {Value: "admin"}, + }, + }, + }, + shouldFail: false, + expectedSSORolesInfo: SSORolesInfo{ + Global: nil, + Teams: []TeamRole{ + { + ID: 1, + Role: "observer", + }, + { + ID: 2, + Role: "admin", + }, + }, + }, + }, + { + name: "v2-prefix-global-and-team", + attributes: []SAMLAttribute{ + { + Name: globalUserRoleSSOAttrName, + Values: []SAMLAttributeValue{ + {Value: "admin"}, + }, + }, + { + Name: "FLEET_JIT_USER_ROLE_FLEET_5", + Values: []SAMLAttributeValue{ + {Value: "observer"}, + }, + }, + }, + shouldFail: true, + expectedSSORolesInfo: SSORolesInfo{}, + }, + { + name: "v2-prefix-invalid-team-id", + attributes: []SAMLAttribute{ + { + Name: "FLEET_JIT_USER_ROLE_FLEET_foo", + Values: []SAMLAttributeValue{ + {Value: "observer"}, + }, + }, + }, + shouldFail: true, + expectedSSORolesInfo: SSORolesInfo{}, + }, + { + name: "v2-prefix-null-value-ignored", + attributes: []SAMLAttribute{ + { + Name: "FLEET_JIT_USER_ROLE_FLEET_1", + Values: []SAMLAttributeValue{ + {Value: "null"}, + }, + }, + }, + shouldFail: false, + expectedSSORolesInfo: SSORolesInfo{}, + }, + { + name: "v2-prefix-team-technician", + attributes: []SAMLAttribute{ + { + Name: "FLEET_JIT_USER_ROLE_FLEET_3", + Values: []SAMLAttributeValue{ + {Value: "technician"}, + }, + }, + }, + shouldFail: false, + expectedSSORolesInfo: SSORolesInfo{ + Teams: []TeamRole{ + { + ID: 3, + Role: "technician", + }, + }, + }, + }, + { + name: "mixed-v1-and-v2-prefixes", + attributes: []SAMLAttribute{ + { + Name: teamUserRoleSSOAttrNamePrefix + "1", + Values: []SAMLAttributeValue{ + {Value: "observer"}, + }, + }, + { + Name: "FLEET_JIT_USER_ROLE_FLEET_2", + Values: []SAMLAttributeValue{ + {Value: "admin"}, + }, + }, + }, + shouldFail: false, + expectedSSORolesInfo: SSORolesInfo{ + Global: nil, + Teams: []TeamRole{ + { + ID: 1, + Role: "observer", + }, + { + ID: 2, + Role: "admin", + }, + }, + }, + }, { name: "global-gitops-not-supported-for-jit", attributes: []SAMLAttribute{ diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index fa58ef0454..6de834a163 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -4865,7 +4865,7 @@ func (s *integrationEnterpriseTestSuite) TestSSOJITProvisioning() { // auto-incremented and other tests cause it to be different than what we need (ID=1). var execErr error mysql.ExecAdhocSQL(t, s.ds, func(db sqlx.ExtContext) error { - _, execErr = db.ExecContext(context.Background(), `INSERT INTO teams (id, name) VALUES (1, 'Foobar') ON DUPLICATE KEY UPDATE name = VALUES(name);`) + _, execErr = db.ExecContext(context.Background(), `INSERT INTO teams (id, name) VALUES (1, 'Foobar'), (2, 'Hollatchaboi') ON DUPLICATE KEY UPDATE name = VALUES(name);`) return execErr }) require.NoError(t, execErr) @@ -4886,9 +4886,11 @@ func (s *integrationEnterpriseTestSuite) TestSSOJITProvisioning() { require.Equal(t, "sso_user_4_team_maintainer@example.com", user4.Email) require.Equal(t, "SSO User 4", user4.Name) require.Nil(t, user4.GlobalRole) - require.Len(t, user4.Teams, 1) + require.Len(t, user4.Teams, 2) require.Equal(t, uint(1), user4.Teams[0].ID) require.Equal(t, fleet.RoleMaintainer, user4.Teams[0].Role) + require.Equal(t, uint(2), user4.Teams[1].ID) + require.Equal(t, fleet.RoleObserver, user4.Teams[1].Role) // A user with pre-configured roles can be created, // see `tools/saml/users.php` for details. diff --git a/tools/saml/users.php b/tools/saml/users.php index 986bc078d1..6aecd498c0 100644 --- a/tools/saml/users.php +++ b/tools/saml/users.php @@ -30,18 +30,20 @@ $config = array( 'email' => 'sso_user_3_global_admin@example.com', 'FLEET_JIT_USER_ROLE_GLOBAL' => 'admin', ), - // sso_user_4_team_maintainer has FLEET_JIT_USER_ROLE_TEAM_1 attribute to be added as maintainer + // sso_user_4_team_maintainer has FLEET_JIT_USER_ROLE_FLEET_1 attribute to be added as maintainer // of team with ID 1, its login will fail if team with ID 1 doesn't exist. + // It also uses the *older* attribute name to add the user to fleet #2 as an observer. 'sso_user_4_team_maintainer:user123#' => array( 'uid' => array('4'), 'eduPersonAffiliation' => array('group1'), 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' => array('SSO User 4'), 'email' => 'sso_user_4_team_maintainer@example.com', - 'FLEET_JIT_USER_ROLE_TEAM_1' => 'maintainer', + 'FLEET_JIT_USER_ROLE_FLEET_1' => 'maintainer', + 'FLEET_JIT_USER_ROLE_TEAM_2' => 'observer', ), // sso_user_5_team_admin has FLEET_JIT_USER_ROLE_TEAM_1 attribute to be added as admin // of team with ID 1, its login will fail if team with ID 1 doesn't exist. - // It also sets FLEET_JIT_USER_ROLE_GLOBAL and FLEET_JIT_USER_ROLE_TEAM_2 to `null` which means + // It also sets FLEET_JIT_USER_ROLE_GLOBAL and FLEET_JIT_USER_ROLE_FLEET_2 to `null` which means // Fleet will ignore such fields. 'sso_user_5_team_admin:user123#' => array( 'uid' => array('5'), @@ -50,7 +52,7 @@ $config = array( 'email' => 'sso_user_5_team_admin@example.com', 'FLEET_JIT_USER_ROLE_TEAM_1' => 'admin', 'FLEET_JIT_USER_ROLE_GLOBAL' => 'null', - 'FLEET_JIT_USER_ROLE_TEAM_2' => 'null', + 'FLEET_JIT_USER_ROLE_FLEET_2' => 'null', ), // sso_user_6_global_observer has all FLEET_JIT_USER_ROLE_* attributes set to null, so it // will be added as global observer (default). @@ -60,7 +62,7 @@ $config = array( 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' => array('SSO User 6'), 'email' => 'sso_user_6_global_observer@example.com', 'FLEET_JIT_USER_ROLE_GLOBAL' => 'null', - 'FLEET_JIT_USER_ROLE_TEAM_1' => 'null', + 'FLEET_JIT_USER_ROLE_FLEET_1' => 'null', ), // sso_user_no_displayname does not have a displayName/fullName 'sso_user_no_displayname:user123#' => array(