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:
co-authored by
Claude Sonnet 4.6
parent
086f8154f9
commit
0a98ce5582
@@ -0,0 +1 @@
|
||||
* Added support for JIT provisioning of the Technician role via SSO SAML attributes.
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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',
|
||||
),
|
||||
),
|
||||
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user