Files
fleet/server/service/conditional_access_idp_test.go
T
Dante Catalfamo 49db931ffb Auto-clean duplicate Okta CA SCEP cert after profile install (#46172)
**Related issue:** Resolves #42757

## Summary

Resending or renewing the Okta conditional access profile leaves an
orphaned SCEP certificate in the per-user macOS keychain, accumulating
duplicates with every renewal. This PR auto-runs an existing
keychain-cleanup script after a successful `InstallProfile` ack for the
Okta CA profile, so admins no longer have to find and run the script
manually.

## Root cause

Investigation in the issue thread isolated the trigger:

- The Okta CA `.mobileconfig` bundles `com.apple.security.scep` with
`com.apple.security.identitypreference` in a single profile (macOS
rejects the alternative — `Identity payload not found in same profile as
identity preference payload`).
- The Identity Preference payload creates a keychain-resident preference
item that keeps the *old* cert pinned across profile replacement, even
though the rewritten Identity Preference now points to the fresh SCEP
enrollment.
- EAP-TLS Wi-Fi profiles renew cleanly because they reference the cert
via SystemConfiguration (`PayloadCertificateUUID`), not the keychain —
so this isn't a generic SCEP-bundling issue.

The team decision in the issue (`@sharon-fdm`) was to delete the
duplicate certificate rather than restructure the profile. A standalone
cleanup script already shipped at
`docs/solutions/macos/scripts/delete-duplicate-scep-certificates.sh` and
was linked from the Okta CA guide; admins had to find and run it.

## Approach

Hook the existing Apple MDM `InstallProfile` ack path in
`MDMAppleCheckinAndCommandService.CommandAndReportResults`, parallel to
the existing ACME `CertificateList` follow-up. When the ack is for the
Okta CA profile and status is `verifying`, enqueue an internal host
script run that executes the cleanup script targeting the host's
per-user MDM enrollment short name.

Key properties:

- **Single hook, three paths covered.** Admin "Resend" nulls the profile
status and the reconciliation cron re-enqueues an `InstallProfile`; the
SCEP renewal cron also re-issues `InstallProfile`. Both flow through the
same ack handler this hook attaches to.
- **Idempotent.** The cleanup script no-ops when only one matching cert
is present, so triggering on initial installs (not just renewals) is
safe and removes the need to distinguish "is this a renewal".
- **Tightly gated.** Single indexed lookup keyed on `(host_uuid,
command_uuid, profile_identifier, platform='darwin')`. Other
SCEP-bearing profiles do not trigger the script. No work happens for
hosts with no per-user enrollment.
- **Internal-script semantics** (matches lock/unlock/wipe prior art).
Runs even when scripts are globally disabled. Does not appear in the
user-facing host activity feed.
- **Failure-isolated.** Enqueue errors are logged but do not break the
ack path; the renewal itself is what matters.
- **Defense in depth on the shell call.** The macOS short name is
validated against a strict regex (`^[A-Za-z0-9_][A-Za-z0-9_.-]*$`, ≤31
chars) before being interpolated, and POSIX single-quote-escaped on the
way through.

## Files

**New**
- `server/service/conditional_access_cleanup.go` — `//go:embed` of the
cleanup script, the hook helper `maybeRunOktaCACleanupScript`, the
validated shell-wrapper builder, and the POSIX single-quote escape
helper.
- `server/service/conditional_access_cleanup_test.go` — unit coverage
for username validation, shell escaping, the routing decisions of the
hook helper (mock-based), and an embed-sync assertion against the docs
copy.
-
`server/service/embedded_scripts/delete-duplicate-scep-certificates.sh`
— embed source-of-truth copy, byte-for-byte equal to the public
`docs/solutions/macos/scripts/` script.
- `changes/42757-okta-conditional-access-duplicate-scep-cert-cleanup` —
user-visible changes note.

**Datastore**
- `server/datastore/mysql/mdm.go` —
`OktaCACleanupTargetForInstallCommand`: single SQL lookup that returns
`(host_id, user_short_name, ok)` for the new hook. Returns `ok=false`
for non-Okta profiles, non-darwin hosts, or hosts without a user-channel
enrollment.
- `server/datastore/mysql/scripts.go` —
`NewInternalHostScriptExecutionRequest`: thin wrapper that routes
through the existing internal-script codepath (`isInternal=true`) used
by lock/unlock/wipe. Refactored the existing public method to share an
internal helper.

**Interface / mocks**
- `server/fleet/conditional_access_idp.go` — exported
`ConditionalAccessOktaProfileIdentifier`,
`ConditionalAccessOktaCertificateCN`, and the new `OktaCACleanupTarget`
struct, so both the template-render path and the SQL lookup can
reference the same source of truth.
- `server/fleet/datastore.go` — `OktaCACleanupTargetForInstallCommand`
and `NewInternalHostScriptExecutionRequest` added to the `Datastore`
interface.
- `server/mock/datastore_mock.go` — regenerated (additions only).

**Wiring**
- `server/service/apple_mdm.go` — call into
`maybeRunOktaCACleanupScript` from the InstallProfile
`MDMDeliveryVerifying` branch, alongside the existing ACME
`maybeQueueCertificateListForACMEProfile` follow-up. Warns on error
rather than failing the ack.
- `server/service/conditional_access_idp.go` — use the new
`fleet.ConditionalAccessOktaCertificateCN` constant when rendering the
profile template, eliminating the magic string duplication.

**Tests touched**
- `server/datastore/mysql/mdm_test.go` — integration test
`testOktaCACleanupTargetForInstallCommand` covering the happy path,
non-Okta profile, device-only enrollment, and unknown command.
- `server/datastore/mysql/scripts_test.go` —
`testNewInternalHostScriptExecutionRequest` confirming the internal flag
is set correctly and the new entry only appears under the internal-only
listing filter.
- `server/service/apple_mdm_test.go` — added the new mock stub for
`OktaCACleanupTargetForInstallCommandFunc` to
`TestMDMCommandAndReportResultsProfileHandling` so the existing test
continues to pass with the new hook in the codepath.
- `server/service/conditional_access_idp_test.go` — the rendered-profile
assertion now also pins on the shared
`ConditionalAccessOktaProfileIdentifier` and
`ConditionalAccessOktaCertificateCN` constants so the template can't
drift from the SQL lookup.
2026-06-05 16:20:32 -04:00

483 lines
16 KiB
Go

package service
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"math/big"
"testing"
"time"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/config"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mock"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/jmoiron/sqlx"
"github.com/stretchr/testify/require"
)
func TestConditionalAccessGetIdPSigningCertAuth(t *testing.T) {
t.Parallel()
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "test-private-key"
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
// Mock the datastore to return a valid IdP certificate
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetConditionalAccessIDPCert: {
Name: fleet.MDMAssetConditionalAccessIDPCert,
Value: []byte("-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----"),
},
}, nil
}
testCases := []struct {
name string
user *fleet.User
shouldFail bool
}{
{"global admin", test.UserAdmin, false},
{"global maintainer", test.UserMaintainer, false},
{"global observer", test.UserObserver, false},
{"global observer+", test.UserObserverPlus, false},
{"global gitops", test.UserGitOps, false},
{"team admin", test.UserTeamAdminTeam1, true},
{"team maintainer", test.UserTeamMaintainerTeam1, true},
{"team observer", test.UserTeamObserverTeam1, true},
{"team observer+", test.UserTeamObserverPlusTeam1, true},
{"team gitops", test.UserTeamGitOpsTeam1, true},
{"user no roles", test.UserNoRoles, true},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := test.UserContext(ctx, tt.user)
certPEM, err := svc.ConditionalAccessGetIdPSigningCert(ctx)
if tt.shouldFail {
require.Error(t, err)
var forbiddenError *authz.Forbidden
require.ErrorAs(t, err, &forbiddenError)
require.Nil(t, certPEM)
} else {
require.NoError(t, err)
require.NotNil(t, certPEM)
}
})
}
}
func TestConditionalAccessGetIdPSigningCert(t *testing.T) {
t.Parallel()
t.Run("missing server private key", func(t *testing.T) {
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "" // Not configured
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
certPEM, err := svc.ConditionalAccessGetIdPSigningCert(ctx)
var badReqErr *fleet.BadRequestError
require.ErrorAs(t, err, &badReqErr)
require.Contains(t, err.Error(), "Fleet server private key is not configured")
require.Nil(t, certPEM)
})
}
func TestConditionalAccessGetIdPAppleProfileAuth(t *testing.T) {
t.Parallel()
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "test-private-key"
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
// Mock valid certificate
certPEM := generateTestCertPEM(t)
// Mock the datastore methods
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetConditionalAccessCACert: {
Name: fleet.MDMAssetConditionalAccessCACert,
Value: certPEM,
},
}, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
ServerURL: "https://fleet.example.com",
},
}, nil
}
ds.GetEnrollSecretsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.EnrollSecret, error) {
return []*fleet.EnrollSecret{
{Secret: "test-secret-123"},
}, nil
}
testCases := []struct {
name string
user *fleet.User
shouldFail bool
}{
{"global admin", test.UserAdmin, false},
{"global maintainer", test.UserMaintainer, false},
{"global observer", test.UserObserver, false},
{"global observer+", test.UserObserverPlus, false},
{"global gitops", test.UserGitOps, false},
{"team admin", test.UserTeamAdminTeam1, true},
{"team maintainer", test.UserTeamMaintainerTeam1, true},
{"team observer", test.UserTeamObserverTeam1, true},
{"team observer+", test.UserTeamObserverPlusTeam1, true},
{"team gitops", test.UserTeamGitOpsTeam1, true},
{"user no roles", test.UserNoRoles, true},
}
for _, tt := range testCases {
t.Run(tt.name, func(t *testing.T) {
ctx := test.UserContext(ctx, tt.user)
profileData, err := svc.ConditionalAccessGetIdPAppleProfile(ctx)
if tt.shouldFail {
require.Error(t, err)
var forbiddenError *authz.Forbidden
require.ErrorAs(t, err, &forbiddenError)
require.Nil(t, profileData)
} else {
require.NoError(t, err)
require.NotNil(t, profileData)
// Verify the profile contains expected content
profileStr := string(profileData)
require.Contains(t, profileStr, "com.fleetdm.conditional-access")
require.Contains(t, profileStr, "https://okta.fleet.example.com")
}
})
}
}
// generateTestCertPEM generates a test certificate in PEM format for testing
func generateTestCertPEM(t *testing.T) []byte {
// Create a simple self-signed certificate
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "Test CA",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
IsCA: true,
BasicConstraintsValid: true,
}
priv, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
require.NoError(t, err)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})
return certPEM
}
func TestConditionalAccessGetIdPAppleProfile(t *testing.T) {
certPEM := generateTestCertPEM(t)
t.Run("missing server private key", func(t *testing.T) {
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "" // Not configured
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
profileData, err := svc.ConditionalAccessGetIdPAppleProfile(ctx)
var badReqErr *fleet.BadRequestError
require.ErrorAs(t, err, &badReqErr)
require.Contains(t, err.Error(), "Fleet server private key is not configured")
require.Nil(t, profileData)
})
t.Run("success - generates valid profile", func(t *testing.T) {
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "test-private-key"
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetConditionalAccessCACert: {
Name: fleet.MDMAssetConditionalAccessCACert,
Value: certPEM,
},
}, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
ServerURL: "https://fleet.example.com:8080",
},
}, nil
}
ds.GetEnrollSecretsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.EnrollSecret, error) {
return []*fleet.EnrollSecret{
{Secret: "test-secret-456"},
}, nil
}
profileData, err := svc.ConditionalAccessGetIdPAppleProfile(ctx)
require.NoError(t, err)
require.NotEmpty(t, profileData)
profileStr := string(profileData)
// Verify XML structure
require.Contains(t, profileStr, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>")
require.Contains(t, profileStr, "<!DOCTYPE plist")
// Verify URLs with port preserved
require.Contains(t, profileStr, "https://fleet.example.com:8080/api/fleet/conditional_access/scep")
require.Contains(t, profileStr, "https://okta.fleet.example.com:8080")
// Verify challenge secret
require.Contains(t, profileStr, "test-secret-456")
// Verify payload identifiers
require.Contains(t, profileStr, "com.fleetdm.conditional-access-ca")
require.Contains(t, profileStr, "com.fleetdm.conditional-access-scep")
require.Contains(t, profileStr, "com.fleetdm.conditional-access-preference")
require.Contains(t, profileStr, "com.fleetdm.chrome.certs")
// Top-level PayloadIdentifier and certificate CN must match the
// shared constants so the InstallProfile-ack cleanup hook can
// reliably gate on them.
require.Contains(t, profileStr, fleet.ConditionalAccessOktaProfileIdentifier)
require.Contains(t, profileStr, fleet.ConditionalAccessOktaCertificateCN)
// Verify the renewal-ID marker is in the SCEP payload's Subject OU
// so auto-renewal activates by default. Substituted to
// fleet-<profile_uuid> at delivery time; Fleet's own SCEP CA
// preserves OU in the issued cert.
require.Contains(t, profileStr, "$FLEET_VAR_CERTIFICATE_RENEWAL_ID")
require.Regexp(t,
`(?s)<key>Subject</key>.*<string>OU</string>\s*<string>\$FLEET_VAR_CERTIFICATE_RENEWAL_ID</string>`,
profileStr,
"renewal-ID marker must be in Subject OU, not CN",
)
})
t.Run("missing CA certificate", func(t *testing.T) {
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "test-private-key"
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{}, nil
}
profileData, err := svc.ConditionalAccessGetIdPAppleProfile(ctx)
require.Error(t, err)
require.Contains(t, err.Error(), "conditional access CA certificate not configured")
require.Nil(t, profileData)
})
t.Run("invalid PEM certificate", func(t *testing.T) {
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "test-private-key"
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetConditionalAccessCACert: {
Name: fleet.MDMAssetConditionalAccessCACert,
Value: []byte("not a valid PEM"),
},
}, nil
}
profileData, err := svc.ConditionalAccessGetIdPAppleProfile(ctx)
require.Error(t, err)
require.Contains(t, err.Error(), "failed to decode CA certificate PEM")
require.Nil(t, profileData)
})
t.Run("invalid DER certificate", func(t *testing.T) {
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "test-private-key"
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
// Valid PEM structure but invalid DER content
invalidCertPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: []byte("invalid DER data"),
})
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetConditionalAccessCACert: {
Name: fleet.MDMAssetConditionalAccessCACert,
Value: invalidCertPEM,
},
}, nil
}
profileData, err := svc.ConditionalAccessGetIdPAppleProfile(ctx)
require.Error(t, err)
require.Contains(t, err.Error(), "failed to parse CA certificate")
require.Nil(t, profileData)
})
t.Run("no enroll secrets", func(t *testing.T) {
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "test-private-key"
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetConditionalAccessCACert: {
Name: fleet.MDMAssetConditionalAccessCACert,
Value: certPEM,
},
}, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
ServerURL: "https://fleet.example.com",
},
}, nil
}
ds.GetEnrollSecretsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.EnrollSecret, error) {
return []*fleet.EnrollSecret{}, nil
}
profileData, err := svc.ConditionalAccessGetIdPAppleProfile(ctx)
var badReqErr *fleet.BadRequestError
require.ErrorAs(t, err, &badReqErr)
require.Contains(t, err.Error(), "global enroll secret is not configured")
require.Nil(t, profileData)
})
t.Run("server URL not configured", func(t *testing.T) {
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "test-private-key"
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetConditionalAccessCACert: {
Name: fleet.MDMAssetConditionalAccessCACert,
Value: certPEM,
},
}, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
ServerURL: "",
},
}, nil
}
profileData, err := svc.ConditionalAccessGetIdPAppleProfile(ctx)
var badReqErr *fleet.BadRequestError
require.ErrorAs(t, err, &badReqErr)
require.Contains(t, err.Error(), "server URL is not configured")
require.Nil(t, profileData)
})
t.Run("invalid server URL", func(t *testing.T) {
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "test-private-key"
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetConditionalAccessCACert: {
Name: fleet.MDMAssetConditionalAccessCACert,
Value: certPEM,
},
}, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
ServerURL: "://invalid-url",
},
}, nil
}
profileData, err := svc.ConditionalAccessGetIdPAppleProfile(ctx)
require.Error(t, err)
require.Contains(t, err.Error(), "failed to parse server URL")
require.Nil(t, profileData)
})
t.Run("deterministic UUIDs based on server URL", func(t *testing.T) {
ds := new(mock.Store)
cfg := config.TestConfig()
cfg.Server.PrivateKey = "test-private-key"
svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil)
ctx = test.UserContext(ctx, test.UserAdmin)
ds.GetAllMDMConfigAssetsByNameFunc = func(ctx context.Context, assetNames []fleet.MDMAssetName, _ sqlx.QueryerContext) (map[fleet.MDMAssetName]fleet.MDMConfigAsset, error) {
return map[fleet.MDMAssetName]fleet.MDMConfigAsset{
fleet.MDMAssetConditionalAccessCACert: {
Name: fleet.MDMAssetConditionalAccessCACert,
Value: certPEM,
},
}, nil
}
ds.AppConfigFunc = func(ctx context.Context) (*fleet.AppConfig, error) {
return &fleet.AppConfig{
ServerSettings: fleet.ServerSettings{
ServerURL: "https://fleet.example.com",
},
}, nil
}
ds.GetEnrollSecretsFunc = func(ctx context.Context, teamID *uint) ([]*fleet.EnrollSecret, error) {
return []*fleet.EnrollSecret{
{Secret: "test-secret"},
}, nil
}
// Generate profile twice with same server URL
profileData1, err := svc.ConditionalAccessGetIdPAppleProfile(ctx)
require.NoError(t, err)
profileData2, err := svc.ConditionalAccessGetIdPAppleProfile(ctx)
require.NoError(t, err)
// UUIDs should be identical
require.Equal(t, profileData1, profileData2)
})
}