<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #45524 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] Timeouts are implemented and retries are limited to avoid infinite loops - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [x] QA'd all new/changed functionality manually For unreleased bug fixes in a release candidate, one of: - [ ] Confirmed that the fix is not expected to adversely impact load test results - [ ] Alerted the release DRI if additional load testing is needed ## Database migrations - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). ## New Fleet configuration settings - [ ] Setting(s) is/are explicitly excluded from GitOps If you didn't check the box above, follow this checklist for GitOps-enabled settings: - [x] Verified that the setting is exported via `fleetctl generate-gitops` - [x] Verified the setting is documented in a separate PR to [the GitOps documentation](https://github.com/fleetdm/fleet/blob/main/docs/Configuration/yaml-files.md#L485) - [x] Verified that the setting is cleared on the server if it is not supplied in a YAML file (or that it is documented as being optional) - [x] Verified that any relevant UI is disabled when GitOps mode is enabled <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added Apple Platform SSO (PSSO) for macOS with device registration, sign-in, and public discovery (JWKS + Apple app-site association) protected by single-use nonces. * Added Apple account provisioning (Platform SSO password sync) configuration with masked client-secret handling and GitOps support. * Added a host-scoped PSSO device registration token variable for Apple MDM profile generation. * **Bug Fixes** * Fixed macOS packaging to correctly build, embed, and sign the Platform SSO extension. * Resetting device Apple MDM data now also clears stored PSSO enrollment records. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Magnus Jensen <magnus@fleetdm.com>
179 lines
5.7 KiB
Go
179 lines
5.7 KiB
Go
package mysql
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestApplePSSO(t *testing.T) {
|
|
ds := CreateMySQLDS(t)
|
|
|
|
cases := []struct {
|
|
name string
|
|
fn func(t *testing.T, ds *Datastore)
|
|
}{
|
|
{"SetOrUpdateAndGet", testPSSOSetOrUpdateAndGet},
|
|
{"ReRegistrationKeepsOldKeys", testPSSOReRegistrationKeepsOldKeys},
|
|
{"RejectsKIDOwnedByAnotherHost", testPSSORejectsKIDOwnedByAnotherHost},
|
|
{"DeleteDevice", testPSSODeleteDevice},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
defer TruncateTables(t, ds)
|
|
c.fn(t, ds)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testPSSOSetOrUpdateAndGet(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
const hostUUID = "ABCDEFGH-0000-0000-0000-111111111111"
|
|
|
|
keys := []fleet.PSSOKey{
|
|
{KID: "kid-sign-1", KeyType: fleet.PSSOKeyTypeSigning, PEM: "sign-pem-1"},
|
|
{KID: "kid-enc-1", KeyType: fleet.PSSOKeyTypeEncryption, PEM: "enc-pem-1"},
|
|
}
|
|
require.NoError(t, ds.SetOrUpdatePSSODevice(ctx, hostUUID, keys))
|
|
|
|
device, err := ds.GetPSSODevice(ctx, hostUUID)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, hostUUID, device.HostUUID)
|
|
assert.False(t, device.CreatedAt.IsZero())
|
|
assert.False(t, device.UpdatedAt.IsZero())
|
|
|
|
_, err = ds.GetPSSODevice(ctx, "unregistered-uuid")
|
|
require.Error(t, err)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
|
|
signKey, err := ds.GetPSSOKey(ctx, "kid-sign-1")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, hostUUID, signKey.HostUUID)
|
|
assert.Equal(t, fleet.PSSOKeyTypeSigning, signKey.KeyType)
|
|
assert.Equal(t, "sign-pem-1", signKey.PEM)
|
|
|
|
encKey, err := ds.GetPSSOKey(ctx, "kid-enc-1")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, fleet.PSSOKeyTypeEncryption, encKey.KeyType)
|
|
assert.Equal(t, "enc-pem-1", encKey.PEM)
|
|
|
|
_, err = ds.GetPSSOKey(ctx, "no-such-kid")
|
|
require.Error(t, err)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
|
|
listed, err := ds.ListPSSOKeys(ctx, hostUUID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, listed, 2)
|
|
|
|
listed, err = ds.ListPSSOKeys(ctx, "unregistered-uuid")
|
|
require.NoError(t, err)
|
|
assert.Empty(t, listed)
|
|
|
|
// Upserting the same kid updates the row in place.
|
|
require.NoError(t, ds.SetOrUpdatePSSODevice(ctx, hostUUID, []fleet.PSSOKey{
|
|
{KID: "kid-sign-1", KeyType: fleet.PSSOKeyTypeSigning, PEM: "sign-pem-1-rotated"},
|
|
}))
|
|
signKey, err = ds.GetPSSOKey(ctx, "kid-sign-1")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "sign-pem-1-rotated", signKey.PEM)
|
|
|
|
listed, err = ds.ListPSSOKeys(ctx, hostUUID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, listed, 2)
|
|
}
|
|
|
|
func testPSSOReRegistrationKeepsOldKeys(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
const hostUUID = "ABCDEFGH-0000-0000-0000-222222222222"
|
|
|
|
require.NoError(t, ds.SetOrUpdatePSSODevice(ctx, hostUUID, []fleet.PSSOKey{
|
|
{KID: "kid-sign-old", KeyType: fleet.PSSOKeyTypeSigning, PEM: "sign-pem-old"},
|
|
{KID: "kid-enc-old", KeyType: fleet.PSSOKeyTypeEncryption, PEM: "enc-pem-old"},
|
|
}))
|
|
|
|
// Re-register with fresh keys: old keys must remain resolvable.
|
|
require.NoError(t, ds.SetOrUpdatePSSODevice(ctx, hostUUID, []fleet.PSSOKey{
|
|
{KID: "kid-sign-new", KeyType: fleet.PSSOKeyTypeSigning, PEM: "sign-pem-new"},
|
|
{KID: "kid-enc-new", KeyType: fleet.PSSOKeyTypeEncryption, PEM: "enc-pem-new"},
|
|
}))
|
|
|
|
for _, kid := range []string{"kid-sign-old", "kid-enc-old", "kid-sign-new", "kid-enc-new"} {
|
|
key, err := ds.GetPSSOKey(ctx, kid)
|
|
require.NoError(t, err, "kid %s", kid)
|
|
assert.Equal(t, hostUUID, key.HostUUID)
|
|
}
|
|
|
|
listed, err := ds.ListPSSOKeys(ctx, hostUUID)
|
|
require.NoError(t, err)
|
|
assert.Len(t, listed, 4)
|
|
}
|
|
|
|
func testPSSORejectsKIDOwnedByAnotherHost(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
const (
|
|
hostUUID1 = "ABCDEFGH-0000-0000-0000-555555555555"
|
|
hostUUID2 = "ABCDEFGH-0000-0000-0000-666666666666"
|
|
)
|
|
|
|
require.NoError(t, ds.SetOrUpdatePSSODevice(ctx, hostUUID1, []fleet.PSSOKey{
|
|
{KID: "kid-shared", KeyType: fleet.PSSOKeyTypeSigning, PEM: "host1-key"},
|
|
}))
|
|
|
|
// A second host must not be able to claim (and overwrite) a kid host1 owns.
|
|
err := ds.SetOrUpdatePSSODevice(ctx, hostUUID2, []fleet.PSSOKey{
|
|
{KID: "kid-shared", KeyType: fleet.PSSOKeyTypeSigning, PEM: "host2-key"},
|
|
})
|
|
require.Error(t, err)
|
|
var conflict *fleet.ConflictError
|
|
require.ErrorAs(t, err, &conflict)
|
|
|
|
// host1's key row is untouched.
|
|
key, err := ds.GetPSSOKey(ctx, "kid-shared")
|
|
require.NoError(t, err)
|
|
assert.Equal(t, hostUUID1, key.HostUUID)
|
|
assert.Equal(t, "host1-key", key.PEM)
|
|
|
|
// The whole registration rolled back: host2 got no device row.
|
|
_, err = ds.GetPSSODevice(ctx, hostUUID2)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
}
|
|
|
|
func testPSSODeleteDevice(t *testing.T, ds *Datastore) {
|
|
ctx := t.Context()
|
|
const (
|
|
hostUUID1 = "ABCDEFGH-0000-0000-0000-333333333333"
|
|
hostUUID2 = "ABCDEFGH-0000-0000-0000-444444444444"
|
|
)
|
|
|
|
require.NoError(t, ds.SetOrUpdatePSSODevice(ctx, hostUUID1, []fleet.PSSOKey{
|
|
{KID: "kid-sign-h1", KeyType: fleet.PSSOKeyTypeSigning, PEM: "p"},
|
|
{KID: "kid-enc-h1", KeyType: fleet.PSSOKeyTypeEncryption, PEM: "p"},
|
|
}))
|
|
require.NoError(t, ds.SetOrUpdatePSSODevice(ctx, hostUUID2, []fleet.PSSOKey{
|
|
{KID: "kid-sign-h2", KeyType: fleet.PSSOKeyTypeSigning, PEM: "p"},
|
|
}))
|
|
|
|
require.NoError(t, ds.DeletePSSODevice(ctx, hostUUID1))
|
|
|
|
_, err := ds.GetPSSODevice(ctx, hostUUID1)
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
|
|
// Keys cascade with the device row.
|
|
_, err = ds.GetPSSOKey(ctx, "kid-sign-h1")
|
|
assert.True(t, fleet.IsNotFound(err))
|
|
listed, err := ds.ListPSSOKeys(ctx, hostUUID1)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, listed)
|
|
|
|
// Other hosts are untouched.
|
|
_, err = ds.GetPSSODevice(ctx, hostUUID2)
|
|
require.NoError(t, err)
|
|
_, err = ds.GetPSSOKey(ctx, "kid-sign-h2")
|
|
require.NoError(t, err)
|
|
|
|
// Deleting an unregistered host is a no-op.
|
|
require.NoError(t, ds.DeletePSSODevice(ctx, "never-registered"))
|
|
}
|