From 0a98ce5582a4e0764a0f2191de22bf579f5adf63 Mon Sep 17 00:00:00 2001 From: Nico <32375741+nulmete@users.noreply.github.com> Date: Tue, 10 Mar 2026 10:15:01 -0300 Subject: [PATCH] Enable JIT provisioning for Technician role (#41286) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Related issue:** Resolves #41242 # Checklist for submitter - [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 Configured SAML app in Okta following https://fleetdm.com/docs/deploy/single-sign-on-sso#okta (needs update): Screenshot 2026-03-10 at 9 10 05 AM Defined a Custom SAML Attribute Statement following https://support.okta.com/help/s/article/How-to-define-and-configure-a-custom-SAML-attribute-statement?language=en_US. This is to add `FLEET_JIT_USER_ROLE_GLOBAL` to the default User profile. Screenshot 2026-03-10 at 9 22
03 AM Within the new Okta app > Sign On tab, added this expression: Screenshot 2026-03-10 at 9 35 41 AM Within Okta, added a new user in Directory > People and assigned it to the new Okta app. Screenshot 2026-03-10 at 9 36 14 AM Logged in to fleet with the new user via SSO and verified its role is Technician: Screenshot 2026-03-10 at 9 32 15 AM Screenshot 2026-03-10 at 9 37 11 AM For unreleased bug fixes in a release candidate, one of: - [x] Confirmed that the fix is not expected to adversely impact load test results - [ ] Alerted the release DRI if additional load testing is needed Co-authored-by: Claude Sonnet 4.6 --- changes/41242-jit-technician-role | 1 + server/fleet/sessions.go | 3 +- server/fleet/sessions_test.go | 59 +++++++++++++++++ server/service/integration_enterprise_test.go | 11 ++++ server/service/sessions_test.go | 65 +++++++++++++++++++ tools/saml/users.php | 8 +++ 6 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 changes/41242-jit-technician-role diff --git a/changes/41242-jit-technician-role b/changes/41242-jit-technician-role new file mode 100644 index 0000000000..1a83261592 --- /dev/null +++ b/changes/41242-jit-technician-role @@ -0,0 +1 @@ +* Added support for JIT provisioning of the Technician role via SSO SAML attributes. diff --git a/server/fleet/sessions.go b/server/fleet/sessions.go index c3f606c0fa..6a1d5cce18 100644 --- a/server/fleet/sessions.go +++ b/server/fleet/sessions.go @@ -122,7 +122,7 @@ const ( // for setting role for a team with ID . // // For both attributes currently supported values are `admin`, `maintainer`, `observer`, -// `observer_plus` and `null`. A `null` value is used to ignore the attribute. +// `observer_plus`, `technician` and `null`. A `null` value is used to ignore the attribute. func RolesFromSSOAttributes(attributes []SAMLAttribute) (SSORolesInfo, error) { ssoRolesInfo := SSORolesInfo{} for _, attribute := range attributes { @@ -175,6 +175,7 @@ func parseRole(values []SAMLAttributeValue) (string, error) { value != RoleMaintainer && value != RoleObserver && value != RoleObserverPlus && + value != RoleTechnician && value != ssoAttrNullRoleValue { return "", fmt.Errorf("invalid role: %s", value) } diff --git a/server/fleet/sessions_test.go b/server/fleet/sessions_test.go index 2511c9016e..4549977fc6 100644 --- a/server/fleet/sessions_test.go +++ b/server/fleet/sessions_test.go @@ -314,6 +314,65 @@ func TestRolesFromSSOAttributes(t *testing.T) { }, shouldFail: true, }, + { + name: "global-technician", + attributes: []SAMLAttribute{ + { + Name: globalUserRoleSSOAttrName, + Values: []SAMLAttributeValue{ + {Value: "technician"}, + }, + }, + }, + shouldFail: false, + expectedSSORolesInfo: SSORolesInfo{ + Global: ptr.String("technician"), + }, + }, + { + name: "team-technician", + attributes: []SAMLAttribute{ + { + Name: teamUserRoleSSOAttrNamePrefix + "3", + Values: []SAMLAttributeValue{ + {Value: "technician"}, + }, + }, + }, + shouldFail: false, + expectedSSORolesInfo: SSORolesInfo{ + Teams: []TeamRole{ + { + ID: 3, + Role: "technician", + }, + }, + }, + }, + { + name: "global-gitops-not-supported-for-jit", + attributes: []SAMLAttribute{ + { + Name: globalUserRoleSSOAttrName, + Values: []SAMLAttributeValue{ + {Value: "gitops"}, + }, + }, + }, + shouldFail: true, + }, + { + name: "team-gitops-not-supported-for-jit", + attributes: []SAMLAttribute{ + { + Name: teamUserRoleSSOAttrNamePrefix + "1", + Values: []SAMLAttributeValue{ + {Value: "gitops"}, + }, + }, + }, + shouldFail: true, + }, } { t.Run(tc.name, func(t *testing.T) { ssoRolesInfo, err := RolesFromSSOAttributes(tc.attributes) diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index ba1234412a..fb31f1ba76 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -4890,6 +4890,17 @@ func (s *integrationEnterpriseTestSuite) TestSSOJITProvisioning() { assert.Equal(t, "SSO User 6", user6.Name) require.NotNil(t, user6.GlobalRole) require.Equal(t, fleet.RoleObserver, *user6.GlobalRole) + + // A user with a global technician role can be created via JIT provisioning, + // see `tools/saml/users.php` for details. + body = s.LoginSSOUser("sso_user_8_global_technician", "user123#") + require.Contains(t, body, "Redirecting to Fleet at ...") + user8, err := s.ds.UserByEmail(context.Background(), "sso_user_8_global_technician@example.com") + require.NoError(t, err) + assert.Equal(t, "sso_user_8_global_technician@example.com", user8.Email) + assert.Equal(t, "SSO User 8", user8.Name) + require.NotNil(t, user8.GlobalRole) + require.Equal(t, fleet.RoleTechnician, *user8.GlobalRole) } func (s *integrationEnterpriseTestSuite) TestDistributedReadWithFeatures() { diff --git a/server/service/sessions_test.go b/server/service/sessions_test.go index 901b96e97b..777eaf17fe 100644 --- a/server/service/sessions_test.go +++ b/server/service/sessions_test.go @@ -449,6 +449,71 @@ func TestGetSSOUser(t *testing.T) { _, err = svc.GetSSOUser(ctx, auth) require.Error(t, err) + + // (5) Test JIT provisioning with global technician role. + + ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) { + return &fleet.AppConfig{ + SSOSettings: &fleet.SSOSettings{ + EnableSSO: true, + EnableSSOIdPLogin: true, + EnableJITProvisioning: true, + }, + }, nil + } + + newUser = nil + ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) { + return nil, newNotFoundError() + } + ds.NewUserFuncInvoked = false + + auth.assertionAttributes = []fleet.SAMLAttribute{ + { + Name: "FLEET_JIT_USER_ROLE_GLOBAL", + Values: []fleet.SAMLAttributeValue{ + {Value: "technician"}, + }, + }, + } + + _, err = svc.GetSSOUser(ctx, auth) + require.NoError(t, err) + + require.NotNil(t, newUser) + require.NotNil(t, newUser.GlobalRole) + require.Equal(t, fleet.RoleTechnician, *newUser.GlobalRole) + require.Empty(t, newUser.Teams) + + // (6) Test JIT provisioning with team technician role. + + newUser = nil + ds.UserByEmailFunc = func(ctx context.Context, email string) (*fleet.User, error) { + return nil, newNotFoundError() + } + ds.NewUserFuncInvoked = false + + ds.TeamWithExtrasFunc = func(ctx context.Context, tid uint) (*fleet.Team, error) { + return &fleet.Team{ID: tid}, nil + } + + auth.assertionAttributes = []fleet.SAMLAttribute{ + { + Name: "FLEET_JIT_USER_ROLE_TEAM_1", + Values: []fleet.SAMLAttributeValue{ + {Value: "technician"}, + }, + }, + } + + _, err = svc.GetSSOUser(ctx, auth) + require.NoError(t, err) + + require.NotNil(t, newUser) + require.Nil(t, newUser.GlobalRole) + require.Len(t, newUser.Teams, 1) + require.Equal(t, uint(1), newUser.Teams[0].ID) + require.Equal(t, fleet.RoleTechnician, newUser.Teams[0].Role) } func TestInitiateSSOWithSSOServerURL(t *testing.T) { diff --git a/tools/saml/users.php b/tools/saml/users.php index a77a513ba9..986bc078d1 100644 --- a/tools/saml/users.php +++ b/tools/saml/users.php @@ -68,6 +68,14 @@ $config = array( 'eduPersonAffiliation' => array('group1'), 'email' => 'sso_user_no_displayname@example.com', ), + // sso_user_8_global_technician has FLEET_JIT_USER_ROLE_GLOBAL attribute to be added as global technician. + 'sso_user_8_global_technician:user123#' => array( + 'uid' => array('8'), + 'eduPersonAffiliation' => array('group1'), + 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name' => array('SSO User 8'), + 'email' => 'sso_user_8_global_technician@example.com', + 'FLEET_JIT_USER_ROLE_GLOBAL' => 'technician', + ), ), );