mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Gate relay mobile tokens behind Relay license
This commit is contained in:
@@ -213,6 +213,12 @@ profile and assignment columns, but embedded table framing must route through
|
||||
`policyPosture` aggregation as read-only data-governance context, but they
|
||||
must not reinterpret sensitivity, routing, or redaction counts as install
|
||||
capacity, registration eligibility, or agent assignment state.
|
||||
Relay mobile credential issuance is not an agent bootstrap or lifecycle
|
||||
repair path just because it lives under shared `internal/api/` routing.
|
||||
`POST /api/security/tokens/relay-mobile` must remain API/security owned,
|
||||
must require the paid `relay` entitlement before token minting, and must not
|
||||
be reused by installer, auto-registration, assignment, or repair flows as an
|
||||
alternate setup credential.
|
||||
When lifecycle surfaces also hydrate from `/api/state`, that first-session
|
||||
snapshot must carry the same canonical resource types and display names as
|
||||
`/api/resources` instead of briefly showing legacy host aliases before the
|
||||
|
||||
@@ -218,6 +218,13 @@ product API routes free of maintainer commercial analytics.
|
||||
session revocation must still delete the full session token set rather than
|
||||
leaving any retained replacement token valid.
|
||||
48. `internal/api/security_tokens.go` shared with `security-privacy`: the security token handlers are both a security/privacy control surface and a canonical API payload contract boundary.
|
||||
The dedicated Pulse Mobile relay token route is part of that same API
|
||||
contract even though its runtime capability is Relay-owned:
|
||||
`POST /api/security/tokens/relay-mobile` must pass normal admin and
|
||||
`settings:write` authorization, then require the paid `relay` feature
|
||||
before minting a `relay:mobile:access` credential. Community installs may
|
||||
receive the standard license-required response, but direct API calls must
|
||||
not bypass Relay entitlement by creating mobile runtime tokens.
|
||||
49. `internal/api/slo.go` shared with `performance-and-scalability`: the SLO endpoint is both an API contract surface and a protected performance hot-path boundary.
|
||||
50. `internal/api/system_settings.go` shared with `security-privacy`: the system settings telemetry and auth controls are both a security/privacy control surface and a canonical API payload contract boundary.
|
||||
51. `internal/api/unified_agent.go` shared with `agent-lifecycle`: unified agent download and installer handlers are both an agent lifecycle control surface and a canonical API payload contract boundary.
|
||||
|
||||
@@ -116,3 +116,8 @@ surface too. The dedicated `relay:mobile:access` credential may only reach the
|
||||
explicit runtime route inventory in `internal/api/relay_mobile_capability.go`,
|
||||
and expanding that inventory is governed L7 work rather than a router-local
|
||||
compatibility tweak.
|
||||
The route that mints that dedicated credential is also part of the paid Relay
|
||||
boundary. `POST /api/security/tokens/relay-mobile` lives in the shared
|
||||
auth/security router, but it must require the paid `relay` entitlement before
|
||||
creating a `relay:mobile:access` token so Community installs cannot bypass
|
||||
Relay/mobile gating through direct API calls.
|
||||
|
||||
@@ -92,6 +92,11 @@ controls as normal product settings.
|
||||
page-local `container runtime` labels.
|
||||
13. `internal/api/security.go` shared with `api-contracts`: the security handlers are both a security/privacy control surface and a canonical API payload contract boundary.
|
||||
14. `internal/api/security_tokens.go` shared with `api-contracts`: the security token handlers are both a security/privacy control surface and a canonical API payload contract boundary.
|
||||
Pulse Mobile relay token creation is a security token-management surface,
|
||||
but it is not a free API-token convenience. After admin and
|
||||
`settings:write` authorization, `POST /api/security/tokens/relay-mobile`
|
||||
must fail closed with the standard license-required response unless the
|
||||
active entitlement includes the paid `relay` feature.
|
||||
15. `internal/api/system_settings.go` shared with `api-contracts`: the system settings telemetry and auth controls are both a security/privacy control surface and a canonical API payload contract boundary.
|
||||
16. `internal/cloudcp/auth/magiclink.go` shared with `cloud-paid`: control-plane magic-link HMAC handling is both a Pulse Cloud account-access boundary and a security/privacy token-secrecy boundary.
|
||||
17. `internal/cloudcp/auth/magiclink_store.go` shared with `cloud-paid`: control-plane magic-link persistence is both a Pulse Cloud account-access boundary and a security/privacy storage-hardening boundary.
|
||||
|
||||
@@ -159,6 +159,11 @@ state.
|
||||
second generic auth body, and `/api/config/export` or `/api/config/import`
|
||||
bypass entries must still leave public-network and credential decisions to
|
||||
their route-local handlers.
|
||||
That same adjacent token boundary does not make Relay mobile credentials
|
||||
available to storage/recovery flows. `POST /api/security/tokens/relay-mobile`
|
||||
must stay API/security and Relay-entitlement owned, require the paid `relay`
|
||||
feature before minting, and must not be reused as a recovery session,
|
||||
export/import bypass, or storage-local credential transport.
|
||||
That same adjacent API boundary also owns monitored-system admission preview
|
||||
transport for provider-backed setup context. `/api/truenas/connections/preview`,
|
||||
`/api/truenas/connections/{id}/preview`, `/api/vmware/connections/preview`,
|
||||
|
||||
@@ -4144,6 +4144,7 @@ func TestContract_HostedOrgManagerSessionCanMintRelayMobileToken(t *testing.T) {
|
||||
}
|
||||
|
||||
router := newMultiTenantRouter(t, cfg)
|
||||
setLicenseTierForHandlersForTests(t, router.licenseHandlers, "org-a", pkglicensing.TierRelay)
|
||||
|
||||
sessionToken := "relay-owner-session-" + strings.ReplaceAll(time.Now().UTC().Format(time.RFC3339Nano), ":", "-")
|
||||
GetSessionStore().CreateSession(sessionToken, time.Hour, "agent", "127.0.0.1", "legacy-owner")
|
||||
|
||||
@@ -45,3 +45,28 @@ func setMaxMonitoredSystemsLicenseForTests(t *testing.T, maxMonitoredSystems int
|
||||
testLicenseProviderMu.Unlock()
|
||||
})
|
||||
}
|
||||
|
||||
func setLicenseTierForHandlersForTests(t *testing.T, handlers *LicenseHandlers, orgID string, tier pkglicensing.Tier) {
|
||||
t.Helper()
|
||||
if handlers == nil {
|
||||
t.Fatal("license handlers are required")
|
||||
}
|
||||
ctx := context.Background()
|
||||
if orgID != "" {
|
||||
ctx = context.WithValue(ctx, OrgIDContextKey, orgID)
|
||||
}
|
||||
svc := handlers.Service(ctx)
|
||||
if svc == nil {
|
||||
t.Fatal("license service is required")
|
||||
}
|
||||
svc.SetCurrentForTesting(&pkglicensing.License{
|
||||
Claims: pkglicensing.Claims{
|
||||
LicenseID: "api-route-license-tier-test",
|
||||
Email: "route-license@example.test",
|
||||
Tier: tier,
|
||||
IssuedAt: time.Now().Add(-time.Hour).Unix(),
|
||||
ExpiresAt: time.Now().Add(24 * time.Hour).Unix(),
|
||||
},
|
||||
ValidatedAt: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
|
||||
)
|
||||
|
||||
// TestRelayEndpointsRequireLicenseFeature verifies that relay settings endpoints
|
||||
@@ -89,6 +90,34 @@ func TestRelayOnboardingEndpointsRequireLicenseFeature(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelayMobileTokenEndpointRequiresLicenseFeature(t *testing.T) {
|
||||
rawToken := "relay-mobile-token-license-test.12345678"
|
||||
record := newTokenRecord(t, rawToken, []string{config.ScopeSettingsWrite, config.ScopeRelayMobileAccess}, nil)
|
||||
cfg := newTestConfigWithTokens(t, record)
|
||||
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
|
||||
handler := router.Handler()
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/security/tokens/relay-mobile", strings.NewReader(`{}`))
|
||||
req.Header.Set("X-API-Token", rawToken)
|
||||
rec := httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusPaymentRequired {
|
||||
t.Fatalf("expected 402 for missing relay license, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
|
||||
setLicenseTierForHandlersForTests(t, router.licenseHandlers, "", pkglicensing.TierRelay)
|
||||
|
||||
req = httptest.NewRequest(http.MethodPost, "/api/security/tokens/relay-mobile", strings.NewReader(`{}`))
|
||||
req.Header.Set("X-API-Token", rawToken)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("expected relay-licensed token minting to succeed, got %d: %s", rec.Code, rec.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// TestRelayLicenseGatingResponseFormat verifies that the 402 response from
|
||||
// relay endpoints includes a JSON body with upgrade information. This ensures
|
||||
// the frontend can show an appropriate upgrade prompt.
|
||||
|
||||
@@ -127,7 +127,11 @@ func (r *Router) registerAuthSecurityInstallRoutes() {
|
||||
if !ensureSettingsWriteScope(r.config, w, req) {
|
||||
return
|
||||
}
|
||||
r.handleCreateRelayMobileAccessToken(w, req)
|
||||
if req.Method != http.MethodPost {
|
||||
r.handleCreateRelayMobileAccessToken(w, req)
|
||||
return
|
||||
}
|
||||
RequireLicenseFeature(r.licenseHandlers, featureRelayKey, r.handleCreateRelayMobileAccessToken)(w, req)
|
||||
}))
|
||||
r.mux.HandleFunc("/api/security/tokens", RequirePermission(r.config, r.authorizer, auth.ActionAdmin, auth.ResourceUsers, func(w http.ResponseWriter, req *http.Request) {
|
||||
switch req.Method {
|
||||
|
||||
Reference in New Issue
Block a user