fleet/ee/server/service/scep/testing_utils.go
Magnus Jensen a8c9e261d7
speed up macOS profile delivery for initial enrollments (#41960)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #34433 

It speeds up the cron, meaning fleetd, bootstrap and now profiles should
be sent within 10 seconds of being known to fleet, compared to the
previous 1 minute.

It's heavily based on my last PR, so the structure and changes are close
to identical, with some small differences.
**I did not do the redis key part in this PR, as I think that should
come in it's own PR, to avoid overlooking logic bugs with that code, and
since this one is already quite sized since we're moving core pieces of
code around.**

# 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.


## Testing

- [x] Added/updated automated tests
- [x] QA'd all new/changed functionality manually


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Faster macOS onboarding: device profiles are delivered and installed
as part of DEP enrollment, shortening initial setup.
* Improved profile handling: per-host profile preprocessing, secret
detection, and clearer failure marking.

* **Improvements**
  * Consolidated SCEP/NDES error messaging for clearer diagnostics.
  * Cron/work scheduling tuned to prioritize Apple MDM profile delivery.

* **Tests**
* Expanded MDM unit and integration tests, including
DeclarativeManagement handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-03-19 14:58:10 -05:00

160 lines
4.3 KiB
Go

package scep
import (
"crypto/x509"
_ "embed"
"encoding/binary"
"fmt"
"log/slog"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"syscall"
"testing"
"unicode/utf16"
"github.com/fleetdm/fleet/v4/server/mdm/scep/depot"
filedepot "github.com/fleetdm/fleet/v4/server/mdm/scep/depot/file"
scepserver "github.com/fleetdm/fleet/v4/server/mdm/scep/server"
"github.com/gorilla/mux"
"github.com/stretchr/testify/require"
)
//go:embed testdata/testca/ca.key
var caKey []byte
//go:embed testdata/testca/ca.pem
var caPem []byte
// NewTestSCEPServer creates a new SCEP server for testing purposes. The depotPath should be the
// relative path to the directory where the test CA files are stored (e.g., "./testdata/testca")
func NewTestSCEPServer(t *testing.T) *httptest.Server {
t.Helper()
caDir := t.TempDir()
if err := os.WriteFile(filepath.Join(caDir, "ca.key"), caKey, 0o644); err != nil {
t.Fatalf("failed to write ca.key: %v", err)
}
if err := os.WriteFile(filepath.Join(caDir, "ca.pem"), caPem, 0o644); err != nil {
t.Fatalf("failed to write ca.pem: %v", err)
}
var err error
var certDepot depot.Depot // cert storage
t.Cleanup(func() {
_ = os.Remove(caDir)
})
certDepot, err = filedepot.NewFileDepot(caDir)
if err != nil {
t.Fatal(err)
}
certDepot = &noopDepot{certDepot}
crt, key, err := certDepot.CA([]byte{})
if err != nil {
t.Fatal(err)
}
var svc scepserver.Service // scep service
svc, err = scepserver.NewService(crt[0], key, scepserver.NopCSRSigner())
if err != nil {
t.Fatal(err)
}
logger := slog.New(slog.DiscardHandler)
e := scepserver.MakeServerEndpoints(svc)
scepHandler := scepserver.MakeHTTPHandler(e, svc, logger)
r := mux.NewRouter()
r.Handle("/scep", scepHandler)
server := httptest.NewServer(r)
t.Cleanup(server.Close)
return server
}
type noopDepot struct{ depot.Depot }
func (d *noopDepot) Put(_ string, _ *x509.Certificate) error {
return nil
}
//go:embed testdata/mscep_admin_cache_full.html
var mscepAdminCacheFull []byte
//go:embed testdata/mscep_admin_insufficient_permissions.html
var mscepAdminInsufficientPermissions []byte
//go:embed testdata/mscep_admin_password.html
var mscepAdminPassword []byte
func NewTestNDESAdminServer(t *testing.T, responseTemplate string, responseStatus int) *httptest.Server {
t.Helper()
var returnPage func() []byte
returnStatus := http.StatusOK
ndesAdminServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(returnStatus)
if returnStatus == http.StatusOK {
_, err := w.Write(returnPage())
require.NoError(t, err)
}
}))
t.Cleanup(ndesAdminServer.Close)
// We need to convert the HTML page to UTF-16 encoding, which is used by Windows servers
convertHTML := func(html []byte) []byte {
datUTF16, err := utf16FromString(string(html))
require.NoError(t, err)
byteData := make([]byte, len(datUTF16)*2)
for i, v := range datUTF16 {
binary.LittleEndian.PutUint16(byteData[i*2:], v)
}
return byteData
}
switch responseTemplate {
case "mscep_admin_cache_full":
// Catch ths issue when NDES password cache is full
returnPage = func() []byte {
return convertHTML(mscepAdminCacheFull)
}
case "mscep_admin_insufficient_permissions":
// Catch this issue when account has insufficient permissions
returnPage = func() []byte {
return convertHTML(mscepAdminInsufficientPermissions)
}
case "mscep_admin_password":
// All good, NDES admin page loads correctly
returnPage = func() []byte {
return convertHTML(mscepAdminPassword)
}
default:
returnPage = func() []byte {
return []byte{}
}
}
return ndesAdminServer
}
func NewTestDynamicChallengeServer(t *testing.T) *httptest.Server {
t.Helper()
dynamicChallengeServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
fmt.Println(r.URL.Path)
_, err := w.Write([]byte("dynamic challenge"))
require.NoError(t, err)
}))
t.Cleanup(dynamicChallengeServer.Close)
return dynamicChallengeServer
}
// utf16FromString returns the UTF-16 encoding of the UTF-8 string s, with a terminating NUL added.
// If s contains a NUL byte at any location, it returns (nil, syscall.EINVAL).
func utf16FromString(s string) ([]uint16, error) {
for i := 0; i < len(s); i++ {
if s[i] == 0 {
return nil, syscall.EINVAL
}
}
return utf16.Encode([]rune(s + "\x00")), nil
}