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):
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.
Within the new Okta app > Sign On tab, added this expression:
Within Okta, added a new user in Directory > People and assigned it to
the new Okta app.
Logged in to fleet with the new user via SSO and verified its role is
Technician:
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',
+ ),
),
);