mirror of
https://github.com/UNITRONIX/BetterDesk.git
synced 2026-09-11 01:55:43 +00:00
f354e83a14
Expose pending LAN and managed enrollment requests in the UX 3.5 notification center, synchronize badges through authenticated panel events with polling fallback, and refresh the registrations view automatically. Also include the pending relay transport fixes already present in the working tree. Thanks: INSOLVE (Honorary); Marco Jakobs (@jacotec); MyNameisStitch (@MyNameisStitch); Redspin (@playerumpknow)
594 lines
21 KiB
Go
594 lines
21 KiB
Go
package api
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ed25519"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/unitronix/betterdesk-server/config"
|
|
"github.com/unitronix/betterdesk-server/db"
|
|
"github.com/unitronix/betterdesk-server/events"
|
|
"github.com/unitronix/betterdesk-server/peer"
|
|
)
|
|
|
|
func signedEnrollmentHeaders(t *testing.T, privateKey ed25519.PrivateKey, method, path, deviceID string, publicKey ed25519.PublicKey) http.Header {
|
|
t.Helper()
|
|
timestamp := time.Now().UTC().Format(time.RFC3339)
|
|
nonce := fmt.Sprintf("%s-%d", t.Name(), time.Now().UnixNano())
|
|
canonicalKey := base64.StdEncoding.EncodeToString(publicKey)
|
|
signature := ed25519.Sign(privateKey, enrollmentProofPayload(method, path, deviceID, canonicalKey, timestamp, nonce))
|
|
|
|
headers := make(http.Header)
|
|
headers.Set("X-BD-Enrollment-Timestamp", timestamp)
|
|
headers.Set("X-BD-Enrollment-Nonce", nonce)
|
|
headers.Set("X-BD-Enrollment-Signature", base64.StdEncoding.EncodeToString(signature))
|
|
return headers
|
|
}
|
|
|
|
func TestManagedEnrollmentPublishesPendingEvent(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.EnrollmentMode = config.EnrollmentModeManaged
|
|
srv := New(cfg, database, peer.NewMap(), nil, "test")
|
|
bus := events.NewBus()
|
|
srv.SetEventBus(bus)
|
|
sub := bus.Subscribe(events.EventEnrollmentPending)
|
|
defer bus.Unsubscribe(sub)
|
|
|
|
body, _ := json.Marshal(map[string]any{
|
|
"device_id": "BD-PENDING-EVENT",
|
|
"uuid": "pending-event-machine",
|
|
"hostname": "pending-host",
|
|
"platform": "linux",
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
srv.handleDeviceRegister(rec, req)
|
|
|
|
if rec.Code != http.StatusAccepted {
|
|
t.Fatalf("expected 202, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
|
|
select {
|
|
case event := <-sub.Ch:
|
|
if event.Type != events.EventEnrollmentPending {
|
|
t.Fatalf("event type = %q, want %q", event.Type, events.EventEnrollmentPending)
|
|
}
|
|
if event.Data["device_id"] != "BD-PENDING-EVENT" {
|
|
t.Fatalf("event device_id = %q", event.Data["device_id"])
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for pending enrollment event")
|
|
}
|
|
}
|
|
|
|
func TestDeviceRegisterIdentityConflict(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
database.UpsertPeer(&db.Peer{
|
|
ID: "BD-TEST1",
|
|
UUID: "original-machine-uuid",
|
|
})
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.EnrollmentMode = "open"
|
|
srv := New(cfg, database, peer.NewMap(), nil, "test")
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("POST /api/devices/register", srv.handleDeviceRegister)
|
|
|
|
body, _ := json.Marshal(map[string]any{
|
|
"device_id": "BD-TEST1",
|
|
"uuid": "different-machine-uuid",
|
|
"hostname": "host-b",
|
|
"platform": "linux amd64",
|
|
"device_type": "desktop",
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusConflict {
|
|
t.Fatalf("expected 409, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var resp EnrollmentResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Error != "identity_conflict" {
|
|
t.Fatalf("expected identity_conflict, got %q", resp.Error)
|
|
}
|
|
if resp.SuggestedDeviceID != "BD-TEST1-2" {
|
|
t.Fatalf("expected suggested ID BD-TEST1-2, got %q", resp.SuggestedDeviceID)
|
|
}
|
|
}
|
|
|
|
func TestDeviceRegisterSameUUIDReissues(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
database.UpsertPeer(&db.Peer{
|
|
ID: "BD-TEST2",
|
|
UUID: "same-machine-uuid",
|
|
})
|
|
|
|
cfg := config.DefaultConfig()
|
|
cfg.EnrollmentMode = "open"
|
|
srv := New(cfg, database, peer.NewMap(), nil, "test")
|
|
if err := srv.storeBdMgmtPublicKey("BD-TEST2", base64.StdEncoding.EncodeToString(publicKey)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("POST /api/devices/register", srv.handleDeviceRegister)
|
|
|
|
body, _ := json.Marshal(map[string]any{
|
|
"device_id": "BD-TEST2",
|
|
"uuid": "same-machine-uuid",
|
|
"hostname": "host-a",
|
|
"platform": "linux amd64",
|
|
"device_type": "os_agent",
|
|
"bundle_id": "support-bundle-test2",
|
|
"public_key": base64.StdEncoding.EncodeToString(publicKey),
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
applyHeaders(req, signedEnrollmentHeaders(t, privateKey, http.MethodPost, "/api/devices/register", "BD-TEST2", publicKey))
|
|
rec := httptest.NewRecorder()
|
|
mux.ServeHTTP(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d body=%s", rec.Code, rec.Body.String())
|
|
}
|
|
var resp EnrollmentResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.Status != "approved" {
|
|
t.Fatalf("expected approved, got %q", resp.Status)
|
|
}
|
|
if resp.DeviceToken == "" {
|
|
t.Fatal("expected device token on re-registration")
|
|
}
|
|
}
|
|
|
|
func TestSupportAgentEnrollmentCannotChangeBoundBundle(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const deviceID = "BD-BUNDLE-BOUND"
|
|
if err := database.UpsertPeer(&db.Peer{ID: deviceID, UUID: "bundle-machine"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := New(config.DefaultConfig(), database, peer.NewMap(), nil, "test")
|
|
if err := srv.storeBdMgmtPublicKey(deviceID, base64.StdEncoding.EncodeToString(publicKey)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := database.SetConfig(deviceBundleIDPrefix+deviceID, "support-bundle-a"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
body, _ := json.Marshal(map[string]any{
|
|
"device_id": deviceID,
|
|
"uuid": "bundle-machine",
|
|
"device_type": "os_agent",
|
|
"bundle_id": "support-bundle-b",
|
|
"public_key": base64.StdEncoding.EncodeToString(publicKey),
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body))
|
|
applyHeaders(req, signedEnrollmentHeaders(t, privateKey, http.MethodPost, "/api/devices/register", deviceID, publicKey))
|
|
rec := httptest.NewRecorder()
|
|
srv.handleDeviceRegister(rec, req)
|
|
if rec.Code != http.StatusForbidden {
|
|
t.Fatalf("bundle change status = %d, want 403: %s", rec.Code, rec.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestDeviceRegisterDoesNotReissueTokenWithoutIdentityProof(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
publicKey, _, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := database.UpsertPeer(&db.Peer{
|
|
ID: "BD-NOPROOF",
|
|
UUID: "machine-uuid-no-proof",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
srv := New(config.DefaultConfig(), database, peer.NewMap(), nil, "test")
|
|
if err := srv.storeBdMgmtPublicKey("BD-NOPROOF", base64.StdEncoding.EncodeToString(publicKey)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
body, _ := json.Marshal(map[string]any{
|
|
"device_id": "BD-NOPROOF",
|
|
"uuid": "machine-uuid-no-proof",
|
|
"public_key": base64.StdEncoding.EncodeToString(publicKey),
|
|
})
|
|
req := httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body))
|
|
rec := httptest.NewRecorder()
|
|
srv.handleDeviceRegister(rec, req)
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status = %d, want 200: %s", rec.Code, rec.Body.String())
|
|
}
|
|
var resp EnrollmentResponse
|
|
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp.DeviceToken != "" {
|
|
t.Fatal("unauthenticated re-registration must not receive a device token")
|
|
}
|
|
}
|
|
|
|
func TestDeviceRegisterStatusIssuesTokenOnlyAfterIdentityProof(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := database.UpsertPeer(&db.Peer{
|
|
ID: "BD-STATUS",
|
|
UUID: "machine-uuid-status",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
srv := New(config.DefaultConfig(), database, peer.NewMap(), nil, "test")
|
|
if err := srv.storeBdMgmtPublicKey("BD-STATUS", base64.StdEncoding.EncodeToString(publicKey)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
noProof := httptest.NewRequest(http.MethodGet, "/api/devices/register/status?device_id=BD-STATUS", nil)
|
|
noProofRec := httptest.NewRecorder()
|
|
srv.handleDeviceRegisterStatus(noProofRec, noProof)
|
|
var noProofResp EnrollmentResponse
|
|
if err := json.Unmarshal(noProofRec.Body.Bytes(), &noProofResp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if noProofResp.DeviceToken != "" {
|
|
t.Fatal("unauthenticated status poll must not receive a device token")
|
|
}
|
|
|
|
proof := httptest.NewRequest(http.MethodGet, "/api/devices/register/status?device_id=BD-STATUS", nil)
|
|
applyHeaders(proof, signedEnrollmentHeaders(t, privateKey, http.MethodGet, "/api/devices/register/status", "BD-STATUS", publicKey))
|
|
proofRec := httptest.NewRecorder()
|
|
srv.handleDeviceRegisterStatus(proofRec, proof)
|
|
if proofRec.Code != http.StatusOK {
|
|
t.Fatalf("signed status = %d, want 200: %s", proofRec.Code, proofRec.Body.String())
|
|
}
|
|
var proofResp EnrollmentResponse
|
|
if err := json.Unmarshal(proofRec.Body.Bytes(), &proofResp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if proofResp.DeviceToken == "" {
|
|
t.Fatal("signed status poll must receive a device token")
|
|
}
|
|
|
|
refresh := httptest.NewRequest(http.MethodGet, "/api/devices/register/status?device_id=BD-STATUS", nil)
|
|
applyHeaders(refresh, signedEnrollmentHeaders(t, privateKey, http.MethodGet, "/api/devices/register/status", "BD-STATUS", publicKey))
|
|
refresh.Header.Set("Authorization", "Bearer "+proofResp.DeviceToken)
|
|
refreshRec := httptest.NewRecorder()
|
|
srv.handleDeviceRegisterStatus(refreshRec, refresh)
|
|
if refreshRec.Code != http.StatusOK {
|
|
t.Fatalf("authenticated refresh = %d, want 200: %s", refreshRec.Code, refreshRec.Body.String())
|
|
}
|
|
var refreshResp EnrollmentResponse
|
|
if err := json.Unmarshal(refreshRec.Body.Bytes(), &refreshResp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if refreshResp.DeviceToken != "" {
|
|
t.Fatal("ordinary authenticated refresh must not re-issue a device token")
|
|
}
|
|
}
|
|
|
|
func TestEnrollmentProofCannotBeReplayed(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := database.UpsertPeer(&db.Peer{ID: "BD-REPLAY", UUID: "machine-uuid-replay"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := New(config.DefaultConfig(), database, peer.NewMap(), nil, "test")
|
|
if err := srv.storeBdMgmtPublicKey("BD-REPLAY", base64.StdEncoding.EncodeToString(publicKey)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
req := httptest.NewRequest(http.MethodGet, "/api/devices/register/status?device_id=BD-REPLAY", nil)
|
|
applyHeaders(req, signedEnrollmentHeaders(t, privateKey, http.MethodGet, "/api/devices/register/status", "BD-REPLAY", publicKey))
|
|
if err := srv.verifyEnrollmentDeviceProof(req, "BD-REPLAY", ""); err != nil {
|
|
t.Fatalf("first proof verification: %v", err)
|
|
}
|
|
if err := srv.verifyEnrollmentDeviceProof(req, "BD-REPLAY", ""); err == nil {
|
|
t.Fatal("replayed enrollment proof was accepted")
|
|
}
|
|
}
|
|
|
|
func TestOpenEnrollmentRequiresProofBeforeIssuingDeviceToken(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := New(config.DefaultConfig(), database, peer.NewMap(), nil, "test")
|
|
|
|
body, _ := json.Marshal(map[string]any{
|
|
"device_id": "BD-OPEN01",
|
|
"uuid": "machine-uuid-open",
|
|
"device_type": "os_agent",
|
|
"bundle_id": "support-bundle-open",
|
|
"public_key": base64.StdEncoding.EncodeToString(publicKey),
|
|
})
|
|
unauthenticated := httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body))
|
|
unauthenticatedRec := httptest.NewRecorder()
|
|
srv.handleDeviceRegister(unauthenticatedRec, unauthenticated)
|
|
if unauthenticatedRec.Code != http.StatusForbidden {
|
|
t.Fatalf("unproven support-agent enrollment status = %d, want 403: %s", unauthenticatedRec.Code, unauthenticatedRec.Body.String())
|
|
}
|
|
var unauthenticatedResp EnrollmentResponse
|
|
if err := json.Unmarshal(unauthenticatedRec.Body.Bytes(), &unauthenticatedResp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if unauthenticatedResp.DeviceToken != "" {
|
|
t.Fatal("open enrollment without proof must not issue a device token")
|
|
}
|
|
if peerInfo, err := database.GetPeer("BD-OPEN01"); err != nil || peerInfo != nil {
|
|
t.Fatalf("unproven support-agent enrollment created a peer: peer=%+v err=%v", peerInfo, err)
|
|
}
|
|
|
|
proof := httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body))
|
|
applyHeaders(proof, signedEnrollmentHeaders(t, privateKey, http.MethodPost, "/api/devices/register", "BD-OPEN01", publicKey))
|
|
proofRec := httptest.NewRecorder()
|
|
srv.handleDeviceRegister(proofRec, proof)
|
|
var proofResp EnrollmentResponse
|
|
if err := json.Unmarshal(proofRec.Body.Bytes(), &proofResp); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if proofResp.DeviceToken == "" {
|
|
t.Fatal("open enrollment with proof must issue a device token")
|
|
}
|
|
}
|
|
|
|
func TestManagedApprovalPreservesEnrollmentMetadata(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := config.DefaultConfig()
|
|
cfg.EnrollmentMode = config.EnrollmentModeManaged
|
|
srv := New(cfg, database, peer.NewMap(), nil, "test")
|
|
|
|
body, _ := json.Marshal(map[string]any{
|
|
"device_id": "BD-META1",
|
|
"uuid": "machine-uuid-metadata",
|
|
"hostname": "agent-host",
|
|
"platform": "linux",
|
|
"version": "1.2.3",
|
|
"device_type": "os_agent",
|
|
"bundle_id": "support-bundle-a",
|
|
"tags": "support, linux, support",
|
|
"public_key": base64.StdEncoding.EncodeToString(publicKey),
|
|
})
|
|
register := httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body))
|
|
applyHeaders(register, signedEnrollmentHeaders(t, privateKey, http.MethodPost, "/api/devices/register", "BD-META1", publicKey))
|
|
registerRec := httptest.NewRecorder()
|
|
srv.handleDeviceRegister(registerRec, register)
|
|
if registerRec.Code != http.StatusAccepted {
|
|
t.Fatalf("managed registration status = %d, want 202: %s", registerRec.Code, registerRec.Body.String())
|
|
}
|
|
|
|
pendingRaw, err := database.GetConfig(pendingDevicePrefix + "BD-META1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pending := parsePendingEnrollmentMeta(pendingRaw)
|
|
if pending.UUID != "machine-uuid-metadata" || pending.DeviceType != "os_agent" ||
|
|
pending.BundleID != "support-bundle-a" || pending.Tags != "support,linux" ||
|
|
pending.PublicKey != base64.StdEncoding.EncodeToString(publicKey) {
|
|
t.Fatalf("pending metadata was not preserved: %+v", pending)
|
|
}
|
|
|
|
approve := httptest.NewRequest(http.MethodPost, "/api/enrollment/approve/BD-META1", bytes.NewBufferString(`{"sync_mode":"standard"}`))
|
|
approve.SetPathValue("id", "BD-META1")
|
|
approveRec := httptest.NewRecorder()
|
|
srv.handleApproveDevice(approveRec, approve)
|
|
if approveRec.Code != http.StatusOK {
|
|
t.Fatalf("approval status = %d, want 200: %s", approveRec.Code, approveRec.Body.String())
|
|
}
|
|
|
|
approved, err := database.GetPeer("BD-META1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if approved == nil {
|
|
t.Fatal("approved peer missing")
|
|
}
|
|
if approved.UUID != "machine-uuid-metadata" || approved.DeviceType != "os_agent" || approved.Tags != "support,linux" {
|
|
t.Fatalf("approved peer metadata = %+v", approved)
|
|
}
|
|
if bundleID, err := database.GetConfig(deviceBundleIDPrefix + "BD-META1"); err != nil || bundleID != "support-bundle-a" {
|
|
t.Fatalf("bundle ID = %q, err=%v", bundleID, err)
|
|
}
|
|
}
|
|
|
|
func TestSuggestAlternateDeviceID(t *testing.T) {
|
|
if got := suggestAlternateDeviceID("BD-ABC"); got != "BD-ABC-2" {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
if got := suggestAlternateDeviceID("BD-ABC-2"); got != "BD-ABC-3" {
|
|
t.Fatalf("got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestEnrollmentProofReplayCannotUseBoundTokenFallback(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
const deviceID = "BD-PROOF-REPLAY"
|
|
if err := database.UpsertPeer(&db.Peer{ID: deviceID, UUID: "proof-replay-machine"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
srv := New(config.DefaultConfig(), database, peer.NewMap(), nil, "test")
|
|
if err := srv.storeBdMgmtPublicKey(deviceID, base64.StdEncoding.EncodeToString(publicKey)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
boundToken, err := srv.issueEnrollmentDeviceToken(deviceID)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
headers := signedEnrollmentHeaders(t, privateKey, http.MethodPost, "/api/devices/register", deviceID, publicKey)
|
|
headers.Set("Authorization", "Bearer "+boundToken)
|
|
newRequest := func() *http.Request {
|
|
req := httptest.NewRequest(http.MethodPost, "/api/devices/register", nil)
|
|
applyHeaders(req, headers)
|
|
return req
|
|
}
|
|
|
|
if !srv.authorizeEnrollmentTokenIssue(newRequest(), deviceID, base64.StdEncoding.EncodeToString(publicKey), "", true) {
|
|
t.Fatal("first proof should authorize token issuance")
|
|
}
|
|
if srv.authorizeEnrollmentTokenIssue(newRequest(), deviceID, base64.StdEncoding.EncodeToString(publicKey), "", true) {
|
|
t.Fatal("replayed proof must not bypass nonce protection with a bound token")
|
|
}
|
|
}
|
|
|
|
func TestEnrollmentStateBlocksRegistrationAndStatus(t *testing.T) {
|
|
t.Run("rejected", func(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
const deviceID = "BD-REJECTED-STATE"
|
|
if err := database.SetConfig(rejectedDevicePrefix+deviceID, `{"rejected":true}`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := New(config.DefaultConfig(), database, peer.NewMap(), nil, "test")
|
|
body, err := json.Marshal(map[string]any{
|
|
"device_id": deviceID,
|
|
"uuid": "rejected-machine",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
register := httptest.NewRecorder()
|
|
srv.handleDeviceRegister(register, httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body)))
|
|
if register.Code != http.StatusForbidden {
|
|
t.Fatalf("rejected registration status = %d, want 403: %s", register.Code, register.Body.String())
|
|
}
|
|
if peerInfo, err := database.GetPeer(deviceID); err != nil || peerInfo != nil {
|
|
t.Fatalf("rejected registration unexpectedly created peer: peer=%+v err=%v", peerInfo, err)
|
|
}
|
|
|
|
status := httptest.NewRecorder()
|
|
srv.handleDeviceRegisterStatus(status, httptest.NewRequest(http.MethodGet, "/api/devices/register/status?device_id="+deviceID, nil))
|
|
if status.Code != http.StatusForbidden {
|
|
t.Fatalf("rejected status poll = %d, want 403: %s", status.Code, status.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("banned", func(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
const deviceID = "BD-BANNED-STATE"
|
|
if err := database.UpsertPeer(&db.Peer{ID: deviceID, UUID: "banned-machine"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := database.BanPeer(deviceID, "test"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := New(config.DefaultConfig(), database, peer.NewMap(), nil, "test")
|
|
body, err := json.Marshal(map[string]any{
|
|
"device_id": deviceID,
|
|
"uuid": "banned-machine",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
register := httptest.NewRecorder()
|
|
srv.handleDeviceRegister(register, httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body)))
|
|
if register.Code != http.StatusForbidden {
|
|
t.Fatalf("banned registration status = %d, want 403: %s", register.Code, register.Body.String())
|
|
}
|
|
|
|
status := httptest.NewRecorder()
|
|
srv.handleDeviceRegisterStatus(status, httptest.NewRequest(http.MethodGet, "/api/devices/register/status?device_id="+deviceID, nil))
|
|
if status.Code != http.StatusForbidden {
|
|
t.Fatalf("banned status poll = %d, want 403: %s", status.Code, status.Body.String())
|
|
}
|
|
})
|
|
|
|
t.Run("removed", func(t *testing.T) {
|
|
database := testSetupDB(t)
|
|
defer database.Close()
|
|
|
|
const deviceID = "BD-REMOVED-STATE"
|
|
if err := database.UpsertPeer(&db.Peer{ID: deviceID, UUID: "removed-machine"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := database.DeletePeer(deviceID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
srv := New(config.DefaultConfig(), database, peer.NewMap(), nil, "test")
|
|
body, err := json.Marshal(map[string]any{
|
|
"device_id": deviceID,
|
|
"uuid": "removed-machine",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
register := httptest.NewRecorder()
|
|
srv.handleDeviceRegister(register, httptest.NewRequest(http.MethodPost, "/api/devices/register", bytes.NewReader(body)))
|
|
if register.Code != http.StatusForbidden {
|
|
t.Fatalf("removed registration status = %d, want 403: %s", register.Code, register.Body.String())
|
|
}
|
|
|
|
status := httptest.NewRecorder()
|
|
srv.handleDeviceRegisterStatus(status, httptest.NewRequest(http.MethodGet, "/api/devices/register/status?device_id="+deviceID, nil))
|
|
if status.Code != http.StatusForbidden {
|
|
t.Fatalf("removed status poll = %d, want 403: %s", status.Code, status.Body.String())
|
|
}
|
|
})
|
|
}
|