mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 13:37:30 +00:00
Additions and fixes for app_sso_platform table (#41048)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #40630 # 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 ## fleetd/orbit/Fleet Desktop - [x] Verified compatibility with the latest released version of Fleet (see [Must rule](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/workflows/fleetd-development-and-release-strategy.md)) - [x] If the change applies to only one platform, confirmed that `runtime.GOOS` is used as needed to isolate changes - [x] Verified that fleetd runs on macOS, Linux and Windows (macOS only) - [ ] Verified auto-update works from the released version of component to the new version (see [tools/tuf/test](../tools/tuf/test/README.md)) (should not affect updates)
This commit is contained in:
parent
97a1abef82
commit
322895c787
6 changed files with 150 additions and 34 deletions
2
orbit/changes/40630-app-sso-platform
Normal file
2
orbit/changes/40630-app-sso-platform
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
- Fix a case where `app_sso_platform` does not return any data if Platform SSO is configured but the user has not yet completed registration.
|
||||
- Add `registration_completed` and `login_type` columns to `app_sso_platform` table.
|
||||
|
|
@ -32,6 +32,10 @@ func Columns() []table.ColumnDefinition {
|
|||
table.TextColumn("device_id"),
|
||||
// User principal name of the user that logged in via Platform SSO.
|
||||
table.TextColumn("user_principal_name"),
|
||||
// Whether Platform SSO registration is completed. Extracted from "Device Configuration" -> "registrationCompleted" (0 = false, 1 = true).
|
||||
table.IntegerColumn("registration_completed"),
|
||||
// Login type extracted from "Device Configuration" -> "loginType" (e.g. "POLoginTypeUserSecureEnclaveKey (2)").
|
||||
table.TextColumn("login_type"),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -104,11 +108,18 @@ func Generate(ctx context.Context, queryContext table.QueryContext) ([]map[strin
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
registrationCompleted := "0"
|
||||
if appSSOPlatform.registrationCompleted {
|
||||
registrationCompleted = "1"
|
||||
}
|
||||
|
||||
return []map[string]string{{
|
||||
"extension_identifier": appSSOPlatform.extensionIdentifier,
|
||||
"realm": appSSOPlatform.realm,
|
||||
"device_id": appSSOPlatform.deviceID,
|
||||
"user_principal_name": appSSOPlatform.userPrincipalName,
|
||||
"extension_identifier": appSSOPlatform.extensionIdentifier,
|
||||
"realm": appSSOPlatform.realm,
|
||||
"registration_completed": registrationCompleted,
|
||||
"device_id": appSSOPlatform.deviceID,
|
||||
"user_principal_name": appSSOPlatform.userPrincipalName,
|
||||
"login_type": appSSOPlatform.loginType,
|
||||
}}, nil
|
||||
}
|
||||
|
||||
|
|
@ -150,10 +161,12 @@ func extractJSONSections(s []byte) (deviceConfig string, userConfig string, err
|
|||
}
|
||||
|
||||
type appSSOPlatformData struct {
|
||||
extensionIdentifier string
|
||||
deviceID string
|
||||
realm string
|
||||
userPrincipalName string
|
||||
extensionIdentifier string
|
||||
deviceID string
|
||||
registrationCompleted bool
|
||||
loginType string
|
||||
realm string
|
||||
userPrincipalName string
|
||||
}
|
||||
|
||||
func parseAppSSOPlatformCommandOutput(output []byte, expectedExtensionIdentifier string, expectedRealm string) (*appSSOPlatformData, error) {
|
||||
|
|
@ -168,6 +181,8 @@ func parseAppSSOPlatformCommandOutput(output []byte, expectedExtensionIdentifier
|
|||
deviceConfig := struct {
|
||||
DeviceSigningCertificate string `json:"deviceSigningCertificate"`
|
||||
ExtensionIdentifier string `json:"extensionIdentifier"`
|
||||
RegistrationCompleted bool `json:"registrationCompleted"`
|
||||
LoginType string `json:"loginType"`
|
||||
}{}
|
||||
if err := json.Unmarshal([]byte(deviceConfigJSON), &deviceConfig); err != nil {
|
||||
return nil, fmt.Errorf("could not unmarshal \"Device Configuration\" JSON: %w", err)
|
||||
|
|
@ -176,19 +191,20 @@ func parseAppSSOPlatformCommandOutput(output []byte, expectedExtensionIdentifier
|
|||
log.Debug().Str("extensionIdentifier", deviceConfig.ExtensionIdentifier).Msg("device registered, but found unmatched extension")
|
||||
return nil, nil
|
||||
}
|
||||
dsc, err := base64.RawURLEncoding.DecodeString(deviceConfig.DeviceSigningCertificate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode \"deviceSigningCertificate\": %w", err)
|
||||
}
|
||||
deviceSigningCertificate, err := x509.ParseCertificate(dsc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse \"deviceSigningCertificate\": %w", err)
|
||||
}
|
||||
if deviceSigningCertificate.Subject.CommonName == "" {
|
||||
return nil, errors.New("empty subject common name in \"deviceSigningCertificate\"")
|
||||
var deviceID string
|
||||
if deviceConfig.DeviceSigningCertificate != "" {
|
||||
dsc, err := base64.RawURLEncoding.DecodeString(deviceConfig.DeviceSigningCertificate)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode \"deviceSigningCertificate\": %w", err)
|
||||
}
|
||||
deviceSigningCertificate, err := x509.ParseCertificate(dsc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse \"deviceSigningCertificate\": %w", err)
|
||||
}
|
||||
deviceID = deviceSigningCertificate.Subject.CommonName
|
||||
}
|
||||
log.Debug().Str(
|
||||
"\"Device Configuration\"", deviceSigningCertificate.Subject.CommonName,
|
||||
"\"Device Configuration\"", deviceID,
|
||||
).Msg("found device ID")
|
||||
userConfig := struct {
|
||||
KerberosStatus []map[string]any `json:"kerberosStatus"`
|
||||
|
|
@ -196,10 +212,12 @@ func parseAppSSOPlatformCommandOutput(output []byte, expectedExtensionIdentifier
|
|||
if userConfigJSON == "(null)" {
|
||||
log.Debug().Msg("user not registered")
|
||||
return &appSSOPlatformData{
|
||||
extensionIdentifier: deviceConfig.ExtensionIdentifier,
|
||||
deviceID: deviceSigningCertificate.Subject.CommonName,
|
||||
realm: expectedRealm,
|
||||
userPrincipalName: "",
|
||||
extensionIdentifier: deviceConfig.ExtensionIdentifier,
|
||||
deviceID: deviceID,
|
||||
registrationCompleted: deviceConfig.RegistrationCompleted,
|
||||
loginType: deviceConfig.LoginType,
|
||||
realm: expectedRealm,
|
||||
userPrincipalName: "",
|
||||
}, nil
|
||||
}
|
||||
if err := json.Unmarshal([]byte(userConfigJSON), &userConfig); err != nil {
|
||||
|
|
@ -230,10 +248,12 @@ func parseAppSSOPlatformCommandOutput(output []byte, expectedExtensionIdentifier
|
|||
if expectedRealm != realm {
|
||||
log.Debug().Str("realm", realm).Msg("user registered, but found unmatched realm")
|
||||
return &appSSOPlatformData{
|
||||
extensionIdentifier: deviceConfig.ExtensionIdentifier,
|
||||
deviceID: deviceSigningCertificate.Subject.CommonName,
|
||||
realm: expectedRealm,
|
||||
userPrincipalName: "",
|
||||
extensionIdentifier: deviceConfig.ExtensionIdentifier,
|
||||
deviceID: deviceID,
|
||||
registrationCompleted: deviceConfig.RegistrationCompleted,
|
||||
loginType: deviceConfig.LoginType,
|
||||
realm: expectedRealm,
|
||||
userPrincipalName: "",
|
||||
}, nil
|
||||
}
|
||||
suffix := fmt.Sprintf("@%s", realm)
|
||||
|
|
@ -242,16 +262,18 @@ func parseAppSSOPlatformCommandOutput(output []byte, expectedExtensionIdentifier
|
|||
log.Debug().Str(
|
||||
"extension_identifier", deviceConfig.ExtensionIdentifier,
|
||||
).Str(
|
||||
"device_id", deviceSigningCertificate.Subject.CommonName,
|
||||
"device_id", deviceID,
|
||||
).Str(
|
||||
"realm", realm,
|
||||
).Str(
|
||||
"user_principal_name", upn,
|
||||
).Msg("device and user found")
|
||||
return &appSSOPlatformData{
|
||||
extensionIdentifier: deviceConfig.ExtensionIdentifier,
|
||||
deviceID: deviceSigningCertificate.Subject.CommonName,
|
||||
realm: realm,
|
||||
userPrincipalName: upn,
|
||||
extensionIdentifier: deviceConfig.ExtensionIdentifier,
|
||||
deviceID: deviceID,
|
||||
registrationCompleted: deviceConfig.RegistrationCompleted,
|
||||
loginType: deviceConfig.LoginType,
|
||||
realm: realm,
|
||||
userPrincipalName: upn,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@ var (
|
|||
|
||||
//go:embed testdata/app_sso_platform_state_empty_kerberos_status.txt
|
||||
noKerberosStatus string
|
||||
|
||||
//go:embed testdata/app_sso_platform_state_configured_not_registered.txt
|
||||
configuredNotRegistered string
|
||||
)
|
||||
|
||||
func TestParseAppSSOPlatformCommandOutput(t *testing.T) {
|
||||
|
|
@ -35,13 +38,15 @@ func TestParseAppSSOPlatformCommandOutput(t *testing.T) {
|
|||
require.Equal(t, "com.microsoft.CompanyPortalMac.ssoextension", data.extensionIdentifier)
|
||||
require.Equal(t, "KERBEROS.MICROSOFTONLINE.COM", data.realm)
|
||||
require.Equal(t, "foobar@contoso.onmicrosoft.com", data.userPrincipalName)
|
||||
require.True(t, data.registrationCompleted)
|
||||
require.Equal(t, "POLoginTypeUserSecureEnclaveKey (2)", data.loginType)
|
||||
|
||||
// Empty, Platform SSO not set yet.
|
||||
// Platform SSO not set yet.
|
||||
data, err = parseAppSSOPlatformCommandOutput([]byte(empty), "com.microsoft.CompanyPortalMac.ssoextension", "KERBEROS.MICROSOFTONLINE.COM")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, data)
|
||||
|
||||
// No, kerberos status - this could happen if user removes the SSO account via Company portal
|
||||
// No kerberos status - this could happen if user removes the SSO account via Company portal
|
||||
data, err = parseAppSSOPlatformCommandOutput([]byte(noKerberosStatus), "com.microsoft.CompanyPortalMac.ssoextension", "KERBEROS.MICROSOFTONLINE.COM")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, data)
|
||||
|
|
@ -59,12 +64,25 @@ func TestParseAppSSOPlatformCommandOutput(t *testing.T) {
|
|||
require.Equal(t, "com.microsoft.CompanyPortalMac.ssoextension", data.extensionIdentifier)
|
||||
require.Equal(t, "FOOBAR.OTHER.COM", data.realm)
|
||||
require.Equal(t, "", data.userPrincipalName)
|
||||
require.Equal(t, "POLoginTypeUserSecureEnclaveKey (2)", data.loginType)
|
||||
|
||||
// None matches.
|
||||
data, err = parseAppSSOPlatformCommandOutput([]byte(sample1), "com.microsoft.Other.other", "FOOBAR.OTHER.COM")
|
||||
require.NoError(t, err)
|
||||
require.Nil(t, data)
|
||||
|
||||
// Platform SSO is configured (device configuration present) but registration not completed and user configuration is null.
|
||||
// This can happen when the configuration profile is deployed but the user hasn't registered with SSO yet.
|
||||
data, err = parseAppSSOPlatformCommandOutput([]byte(configuredNotRegistered), "com.microsoft.CompanyPortalMac.ssoextension", "KERBEROS.MICROSOFTONLINE.COM")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, data)
|
||||
require.Equal(t, "", data.deviceID)
|
||||
require.Equal(t, "com.microsoft.CompanyPortalMac.ssoextension", data.extensionIdentifier)
|
||||
require.Equal(t, "KERBEROS.MICROSOFTONLINE.COM", data.realm)
|
||||
require.Equal(t, "", data.userPrincipalName)
|
||||
require.False(t, data.registrationCompleted)
|
||||
require.Equal(t, "POLoginTypeUserSecureEnclaveKey (2)", data.loginType)
|
||||
|
||||
// Platform SSO extension identifier matches, but user is not registered yet (null).
|
||||
// Can happen if Platform SSO configuration profile was deployed and this is a workstation with two users,
|
||||
// and one user registered but not the other one.
|
||||
|
|
@ -75,6 +93,7 @@ func TestParseAppSSOPlatformCommandOutput(t *testing.T) {
|
|||
require.Equal(t, "com.microsoft.CompanyPortalMac.ssoextension", data.extensionIdentifier)
|
||||
require.Equal(t, "KERBEROS.MICROSOFTONLINE.COM", data.realm)
|
||||
require.Equal(t, "", data.userPrincipalName)
|
||||
require.Equal(t, "POLoginTypeUserSecureEnclaveKey (2)", data.loginType)
|
||||
}
|
||||
|
||||
func TestGenerateErrors(t *testing.T) {
|
||||
|
|
|
|||
53
orbit/pkg/table/app_sso_platform/testdata/app_sso_platform_state_configured_not_registered.txt
vendored
Normal file
53
orbit/pkg/table/app_sso_platform/testdata/app_sso_platform_state_configured_not_registered.txt
vendored
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
Time: 2026-03-05 16:36:57 +0000
|
||||
|
||||
Device Configuration:
|
||||
{
|
||||
"_accessTokenTerminalIdentityKeySEP" : false,
|
||||
"_accessTokenTerminalIdentityKeySystem" : false,
|
||||
"_deviceEncryptionKeyData" : "wI3a/2hAEPpukuFsD/Vi29mtPiay3Qgr+dMVW2ejoA8=",
|
||||
"_deviceSigningKeyData" : "Qt3aUwTLbX/bFehpl/DBthLhTgXfnuCKgnXF/nqNWNw=",
|
||||
"allowAccessTokenExpressMode" : false,
|
||||
"allowDeviceIdentifiersInAttestation" : false,
|
||||
"authGracePeriodStart" : "2026-03-04T21:01:56Z",
|
||||
"authorizationEnabled" : false,
|
||||
"created" : "2026-03-05T16:36:57Z",
|
||||
"createFirstUserDuringSetupEnabled" : true,
|
||||
"createUserLoginTypes" : [
|
||||
1,
|
||||
3
|
||||
],
|
||||
"createUsersEnabled" : false,
|
||||
"encryptionAlgorithm" : "ECDHE-A256GCM",
|
||||
"extensionIdentifier" : "com.microsoft.CompanyPortalMac.ssoextension",
|
||||
"fileVaultPolicy" : "None (0)",
|
||||
"lastEncryptionKeyChange" : "2026-03-04T21:01:56Z",
|
||||
"loginFrequency" : 64800,
|
||||
"loginPolicy" : "None (0)",
|
||||
"loginType" : "POLoginTypeUserSecureEnclaveKey (2)",
|
||||
"newUserAuthorizationMode" : "None",
|
||||
"offlineGracePeriod" : "0 hours",
|
||||
"pendingEncryptionAlgorithm" : "none",
|
||||
"pendingSigningAlgorithm" : "none",
|
||||
"protocolVersion" : 1,
|
||||
"registrationCompleted" : false,
|
||||
"requireAuthGracePeriod" : "0 hours",
|
||||
"sdkVersionString" : 15.199999999999999,
|
||||
"sharedDeviceKeys" : true,
|
||||
"signingAlgorithm" : "ES256",
|
||||
"synchronizeProfilePicture" : false,
|
||||
"temporarySessionQuickLogin" : false,
|
||||
"tokenToUserMapping" : {
|
||||
"AccountName" : "preferred_username",
|
||||
"FullName" : "name"
|
||||
},
|
||||
"unlockPolicy" : "None (0)",
|
||||
"userAuthorizationMode" : "None",
|
||||
"version" : 1
|
||||
}
|
||||
|
||||
Login Configuration:
|
||||
(null)
|
||||
|
||||
User Configuration:
|
||||
(null)
|
||||
|
||||
|
|
@ -592,6 +592,18 @@
|
|||
"type": "text",
|
||||
"required": false,
|
||||
"description": "User principal name of the user that logged in via Platform SSO."
|
||||
},
|
||||
{
|
||||
"name": "registration_completed",
|
||||
"type": "integer",
|
||||
"required": false,
|
||||
"description": "Whether Platform SSO registration is completed (0 = false, 1 = true)."
|
||||
},
|
||||
{
|
||||
"name": "login_type",
|
||||
"type": "text",
|
||||
"required": false,
|
||||
"description": "Login type extracted from device configuration (e.g. \"POLoginTypeUserSecureEnclaveKey (2)\")."
|
||||
}
|
||||
],
|
||||
"notes": "This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).",
|
||||
|
|
|
|||
|
|
@ -25,5 +25,13 @@ columns:
|
|||
type: text
|
||||
required: false
|
||||
description: User principal name of the user that logged in via Platform SSO.
|
||||
- name: registration_completed
|
||||
type: integer
|
||||
required: false
|
||||
description: Whether Platform SSO registration is completed (0 = false, 1 = true).
|
||||
- name: login_type
|
||||
type: text
|
||||
required: false
|
||||
description: Login type extracted from device configuration (e.g. "POLoginTypeUserSecureEnclaveKey (2)").
|
||||
notes: This table is not a core osquery table. It is included as part of Fleet's agent ([fleetd](https://fleetdm.com/docs/get-started/anatomy#fleetd)).
|
||||
evented: false
|
||||
|
|
|
|||
Loading…
Reference in a new issue