**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.
483 lines
16 KiB
Go
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)
|
|
})
|
|
}
|