Enable JIT provisioning for Technician role (#41286)

<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**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):

<img width="1069" height="790" alt="Screenshot 2026-03-10 at 9 10 05 AM"
src="https://github.com/user-attachments/assets/7a160599-524e-4118-922b-5f9b601129eb"
/>

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.

<img width="2536" height="1299" alt="Screenshot 2026-03-10 at 9 22
03 AM"
src="https://github.com/user-attachments/assets/68193815-4abd-4a3b-9e95-147b1b3105d3"
/>

Within the new Okta app > Sign On tab, added this expression:

<img width="765" height="444" alt="Screenshot 2026-03-10 at 9 35 41 AM"
src="https://github.com/user-attachments/assets/40073cfc-931c-492e-bd5f-e8e89434b107"
/>

Within Okta, added a new user in Directory > People and assigned it to
the new Okta app.

<img width="1050" height="515" alt="Screenshot 2026-03-10 at 9 36 14 AM"
src="https://github.com/user-attachments/assets/1b0a2847-208a-4251-8d9c-6bd0cba33d13"
/>

Logged in to fleet with the new user via SSO and verified its role is
Technician:

<img width="714" height="507" alt="Screenshot 2026-03-10 at 9 32 15 AM"
src="https://github.com/user-attachments/assets/cf44d99c-78bc-4d7c-9f46-5c25fc745778"
/>

<img width="1356" height="339" alt="Screenshot 2026-03-10 at 9 37 11 AM"
src="https://github.com/user-attachments/assets/baa028cb-6b3b-4c9b-b02e-ac2e16ec9262"
/>



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 <noreply@anthropic.com>
This commit is contained in:
Nico
2026-03-10 10:15:01 -03:00
committed by GitHub
co-authored by Claude Sonnet 4.6
parent 086f8154f9
commit 0a98ce5582
6 changed files with 146 additions and 1 deletions
+1
View File
@@ -0,0 +1 @@
* Added support for JIT provisioning of the Technician role via SSO SAML attributes.
+2 -1
View File
@@ -122,7 +122,7 @@ const (
// for setting role for a team with ID <TEAM_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)
}
+59
View File
@@ -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)
@@ -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() {
+65
View File
@@ -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) {
+8
View File
@@ -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',
),
),
);