Alias JIT Saml "team" attribute to FLEET_JIT_USER_ROLE_FLEET_<FLEET ID> (#41402)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #40642 

# Details

This PR adds `FLEET_JIT_USER_ROLE_FLEET_` as an expected Saml attribute
alongside `FLEET_JIT_USER_ROLE_TEAM_`.

# Checklist for submitter

If some of the following don't apply, delete the relevant line.

- [X] Changes file added for user-visible changes in `changes/`,
`orbit/changes/` or `ee/fleetd-chrome/changes`.
See [Changes
files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files)
for more information.

## Testing

- [X] Added/updated automated tests
- [X] QA'd all new/changed functionality manually
Tested with SimpleSAML SSO. 
- [x] Updated `users.php` to use both the new attribute and the old
attribute for a user, and was able to log in with that user and see them
created using JIT with the correct permissions
This commit is contained in:
Scott Gress 2026-03-13 08:34:29 -05:00 committed by GitHub
parent 9424db4858
commit 2d4e72ac7a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 150 additions and 13 deletions

View file

@ -0,0 +1 @@
- Added ability to use `FLEET_JIT_USER_ROLE_FLEET_` as a prefix on SAML attributes

View file

@ -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_<TEAM_ID>` are used
// for setting role for a team with ID <TEAM_ID>.
// - Custom attributes of the form `FLEET_JIT_USER_ROLE_TEAM_<FLEET_ID>` or
// `FLEET_JIT_USER_ROLE_FLEET_<FLEET_ID>` are used for setting role for a fleet with ID <FLEET_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)

View file

@ -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{

View file

@ -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.

View file

@ -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(