Record project continuation changes
This commit is contained in:
@@ -14,12 +14,13 @@ const (
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID string
|
||||
Email string
|
||||
PasswordHash string
|
||||
MFAEnabled bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
PasswordHash string `json:"-"`
|
||||
MFAEnabled bool `json:"mfa_enabled"`
|
||||
PlatformRole string `json:"platform_role"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type Device struct {
|
||||
@@ -40,7 +41,7 @@ type AuthSession struct {
|
||||
ID string
|
||||
UserID string
|
||||
DeviceID string
|
||||
RefreshTokenHash string
|
||||
RefreshTokenHash string `json:"-"`
|
||||
RefreshExpiresAt time.Time
|
||||
LastSeenAt *time.Time
|
||||
LastRotatedAt *time.Time
|
||||
@@ -69,6 +70,13 @@ type BootstrapOwnerCommand struct {
|
||||
ActivationSignature string `json:"activation_signature"`
|
||||
}
|
||||
|
||||
type CreateUserCommand struct {
|
||||
ActorUserID string `json:"actor_user_id"`
|
||||
Email string `json:"email"`
|
||||
Password string `json:"password"`
|
||||
PlatformRole string `json:"platform_role"`
|
||||
}
|
||||
|
||||
type RevokeAuthSessionCommand struct {
|
||||
UserID string `json:"user_id"`
|
||||
AuthSessionID string `json:"auth_session_id"`
|
||||
|
||||
@@ -34,6 +34,10 @@ func (m *Module) RegisterRoutes(router chi.Router) {
|
||||
r.Get("/devices", m.handleTrustedDevices)
|
||||
r.Post("/devices/{deviceID}/revoke", m.handleRevokeTrustedDevice)
|
||||
})
|
||||
router.Route("/users", func(r chi.Router) {
|
||||
r.Get("/", m.handleListUsers)
|
||||
r.Post("/", m.handleCreateUser)
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) handleInstallationStatus(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -78,6 +82,32 @@ func (m *Module) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||
httpx.WriteJSON(w, http.StatusOK, result)
|
||||
}
|
||||
|
||||
func (m *Module) handleListUsers(w http.ResponseWriter, r *http.Request) {
|
||||
actorUserID := r.URL.Query().Get("actor_user_id")
|
||||
users, err := m.service.ListUsers(r.Context(), actorUserID)
|
||||
if err != nil {
|
||||
status, message := m.service.MapError(err)
|
||||
httpx.WriteError(w, status, message)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"users": users})
|
||||
}
|
||||
|
||||
func (m *Module) handleCreateUser(w http.ResponseWriter, r *http.Request) {
|
||||
var cmd CreateUserCommand
|
||||
if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid user payload")
|
||||
return
|
||||
}
|
||||
user, err := m.service.CreateUser(r.Context(), cmd)
|
||||
if err != nil {
|
||||
status, message := m.service.MapError(err)
|
||||
httpx.WriteError(w, status, message)
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusCreated, map[string]any{"user": user})
|
||||
}
|
||||
|
||||
func (m *Module) handleRefresh(w http.ResponseWriter, r *http.Request) {
|
||||
var cmd RefreshCommand
|
||||
if err := json.NewDecoder(r.Body).Decode(&cmd); err != nil {
|
||||
|
||||
@@ -70,7 +70,7 @@ type postgresInstallationRepository struct {
|
||||
|
||||
func (r *postgresUserRepository) GetByEmail(ctx context.Context, email string) (*User, error) {
|
||||
const query = `
|
||||
SELECT id::text, email, password_hash, mfa_enabled, created_at, updated_at
|
||||
SELECT id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
|
||||
FROM users
|
||||
WHERE email = $1
|
||||
`
|
||||
@@ -79,13 +79,53 @@ WHERE email = $1
|
||||
|
||||
func (r *postgresUserRepository) GetByID(ctx context.Context, userID string) (*User, error) {
|
||||
const query = `
|
||||
SELECT id::text, email, password_hash, mfa_enabled, created_at, updated_at
|
||||
SELECT id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
|
||||
FROM users
|
||||
WHERE id = $1::uuid
|
||||
`
|
||||
return scanOptionalUser(r.db.QueryRow(ctx, query, userID))
|
||||
}
|
||||
|
||||
func (r *postgresUserRepository) List(ctx context.Context) ([]User, error) {
|
||||
const query = `
|
||||
SELECT id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
rows, err := r.db.Query(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query users: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
var users []User
|
||||
for rows.Next() {
|
||||
user, err := scanOptionalUser(rows)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if user != nil {
|
||||
users = append(users, *user)
|
||||
}
|
||||
}
|
||||
return users, rows.Err()
|
||||
}
|
||||
|
||||
func (r *postgresUserRepository) Create(ctx context.Context, user User) (*User, error) {
|
||||
const query = `
|
||||
INSERT INTO users (email, password_hash, mfa_enabled, platform_role, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
RETURNING id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
|
||||
`
|
||||
return scanOptionalUser(r.db.QueryRow(ctx, query,
|
||||
user.Email,
|
||||
user.PasswordHash,
|
||||
user.MFAEnabled,
|
||||
user.PlatformRole,
|
||||
user.CreatedAt,
|
||||
user.UpdatedAt,
|
||||
))
|
||||
}
|
||||
|
||||
func (r *postgresDeviceRepository) Upsert(ctx context.Context, params UpsertDeviceParams) (*Device, error) {
|
||||
const query = `
|
||||
INSERT INTO devices (
|
||||
@@ -348,7 +388,7 @@ ON CONFLICT (email) DO UPDATE SET
|
||||
password_hash = EXCLUDED.password_hash,
|
||||
platform_role = EXCLUDED.platform_role,
|
||||
updated_at = EXCLUDED.updated_at
|
||||
RETURNING id::text, email, password_hash, mfa_enabled, created_at, updated_at
|
||||
RETURNING id::text, email, password_hash, mfa_enabled, platform_role, created_at, updated_at
|
||||
`, email, params.PasswordHash, params.Role, now))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("upsert bootstrap owner: %w", err)
|
||||
@@ -461,6 +501,7 @@ func scanOptionalUser(row scanner) (*User, error) {
|
||||
&user.Email,
|
||||
&user.PasswordHash,
|
||||
&user.MFAEnabled,
|
||||
&user.PlatformRole,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
); err != nil {
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
)
|
||||
|
||||
type UserRepository interface {
|
||||
List(ctx context.Context) ([]User, error)
|
||||
GetByEmail(ctx context.Context, email string) (*User, error)
|
||||
GetByID(ctx context.Context, userID string) (*User, error)
|
||||
Create(ctx context.Context, user User) (*User, error)
|
||||
}
|
||||
|
||||
type DeviceRepository interface {
|
||||
|
||||
@@ -13,11 +13,13 @@ import (
|
||||
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/authority"
|
||||
"github.com/example/remote-access-platform/backend/internal/platform/module"
|
||||
postgresplatform "github.com/example/remote-access-platform/backend/internal/platform/postgres"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
cfg module.Config
|
||||
store Store
|
||||
db postgresplatform.DBTX
|
||||
transactor Transactor
|
||||
tokenManager *TokenManager
|
||||
authority *authority.Verifier
|
||||
@@ -31,7 +33,7 @@ func NewService(deps module.Dependencies, store Store, transactor Transactor, ve
|
||||
} else if verifier, err := authority.NewVerifier(deps.Config.Installation); err == nil {
|
||||
authorityVerifier = verifier
|
||||
}
|
||||
return &Service{
|
||||
service := &Service{
|
||||
cfg: deps.Config,
|
||||
store: store,
|
||||
transactor: transactor,
|
||||
@@ -45,6 +47,10 @@ func NewService(deps module.Dependencies, store Store, transactor Transactor, ve
|
||||
authority: authorityVerifier,
|
||||
now: time.Now,
|
||||
}
|
||||
if postgresStore, ok := store.(*postgresStore); ok {
|
||||
service.db = postgresStore.db
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *Service) Login(ctx context.Context, cmd LoginCommand) (*AuthResult, error) {
|
||||
@@ -120,6 +126,44 @@ func (s *Service) Login(ctx context.Context, cmd LoginCommand) (*AuthResult, err
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListUsers(ctx context.Context, actorUserID string) ([]User, error) {
|
||||
if err := s.ensurePlatformAdmin(ctx, actorUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.store.Users().List(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) CreateUser(ctx context.Context, cmd CreateUserCommand) (*User, error) {
|
||||
if err := s.ensurePlatformAdmin(ctx, cmd.ActorUserID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
email := strings.ToLower(strings.TrimSpace(cmd.Email))
|
||||
password := strings.TrimSpace(cmd.Password)
|
||||
role := strings.TrimSpace(cmd.PlatformRole)
|
||||
if role == "" {
|
||||
role = "user"
|
||||
}
|
||||
if email == "" || !strings.Contains(email, "@") || len(password) < 8 {
|
||||
return nil, ErrInvalidBootstrapOwner
|
||||
}
|
||||
if role != "user" && role != authority.PlatformRoleAdmin && role != authority.PlatformRoleRecoveryAdmin {
|
||||
return nil, ErrInvalidBootstrapOwner
|
||||
}
|
||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("hash user password: %w", err)
|
||||
}
|
||||
now := s.now().UTC()
|
||||
return s.store.Users().Create(ctx, User{
|
||||
Email: email,
|
||||
PasswordHash: string(passwordHash),
|
||||
MFAEnabled: false,
|
||||
PlatformRole: role,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) Refresh(ctx context.Context, cmd RefreshCommand) (*AuthResult, error) {
|
||||
authSessionID, err := s.tokenManager.ParseRefreshToken(cmd.RefreshToken)
|
||||
if err != nil {
|
||||
@@ -438,3 +482,25 @@ func (s *Service) installationStatusFromRecord(record *InstallationAuthorityStat
|
||||
func (s *Service) strictAuthority() bool {
|
||||
return s.authority != nil && s.authority.Strict()
|
||||
}
|
||||
|
||||
func (s *Service) ensurePlatformAdmin(ctx context.Context, actorUserID string) error {
|
||||
if actorUserID == "" {
|
||||
return ErrInvalidCredentials
|
||||
}
|
||||
role := authority.PlatformRoleUser
|
||||
if s.db != nil {
|
||||
effectiveRole, err := authority.EffectivePlatformRole(ctx, s.db, s.authority, actorUserID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
role = effectiveRole
|
||||
} else if user, err := s.store.Users().GetByID(ctx, actorUserID); err != nil {
|
||||
return err
|
||||
} else if user != nil && user.PlatformRole != "" {
|
||||
role = user.PlatformRole
|
||||
}
|
||||
if role != authority.PlatformRoleAdmin && role != authority.PlatformRoleRecoveryAdmin {
|
||||
return ErrDeviceRevoked
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,216 @@
|
||||
package cluster
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestVPNPacketHubPopBatchAndStatsKeys(t *testing.T) {
|
||||
hub := newVPNPacketHub()
|
||||
key := vpnPacketKey{
|
||||
ClusterID: "cluster-1",
|
||||
VPNConnectionID: "vpn-1",
|
||||
Direction: vpnDirectionClientToGateway,
|
||||
}
|
||||
|
||||
packetA := []byte{
|
||||
0x45, 0x00, 0x00, 20,
|
||||
0x00, 0x01, 0x00, 0x00,
|
||||
64, 17, 0, 0,
|
||||
192, 168, 0, 1,
|
||||
192, 168, 0, 2,
|
||||
0x00, 0x50, 0x01, 0xBB,
|
||||
}
|
||||
|
||||
packetB := make([]byte, len(packetA))
|
||||
copy(packetB, packetA)
|
||||
packetB[19] = 0xBA
|
||||
|
||||
hub.Push(key, packetA)
|
||||
hub.Push(key, packetB)
|
||||
|
||||
packets := hub.PopBatch(context.Background(), key, 0, vpnPacketBatchMaxPackets, vpnPacketBatchMaxBytes)
|
||||
if len(packets) != 2 {
|
||||
t.Fatalf("expected 2 packets in batch, got %d", len(packets))
|
||||
}
|
||||
|
||||
statsAny := hub.Snapshot("cluster-1", "vpn-1")[vpnDirectionClientToGateway]
|
||||
stats, ok := statsAny.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected stats payload type: %T", statsAny)
|
||||
}
|
||||
|
||||
for _, keyName := range []string{
|
||||
"pushed",
|
||||
"pushed_bytes",
|
||||
"popped",
|
||||
"popped_bytes",
|
||||
"window_push_rate_pps",
|
||||
"window_pop_rate_pps",
|
||||
"window_push_rate_mbps",
|
||||
"window_pop_rate_mbps",
|
||||
"window_push_packets",
|
||||
"window_pop_packets",
|
||||
"queue_depth",
|
||||
"queue_depths",
|
||||
"queue_depth_max",
|
||||
"queue_depth_high_watermark",
|
||||
"queue_depth_high_at",
|
||||
"shard_depth_high_watermark",
|
||||
"shard_depth_high_at",
|
||||
"queue_capacity",
|
||||
"queue_shard_capacity",
|
||||
"queue_full_drops",
|
||||
"requeue_drops",
|
||||
"cleared_stale_packets",
|
||||
"flow_shard_count",
|
||||
"flow_isolation",
|
||||
} {
|
||||
if _, found := stats[keyName]; !found {
|
||||
t.Fatalf("missing vpn packet stat key %s", keyName)
|
||||
}
|
||||
}
|
||||
|
||||
if got, ok := stats["popped"].(uint64); !ok || got != 2 {
|
||||
t.Fatalf("expected popped=2, got %v (ok=%v)", stats["popped"], ok)
|
||||
}
|
||||
if got, ok := stats["pushed"].(uint64); !ok || got != 2 {
|
||||
t.Fatalf("expected pushed=2, got %v (ok=%v)", stats["pushed"], ok)
|
||||
}
|
||||
if got, ok := stats["queue_depth_high_watermark"].(int); !ok || got < 1 {
|
||||
t.Fatalf("expected queue depth high watermark, got %v (ok=%v)", stats["queue_depth_high_watermark"], ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVPNPacketHubGatherBehavior(t *testing.T) {
|
||||
hub := newVPNPacketHub()
|
||||
key := vpnPacketKey{
|
||||
ClusterID: "cluster-1",
|
||||
VPNConnectionID: "vpn-1",
|
||||
Direction: vpnDirectionGatewayToClient,
|
||||
}
|
||||
packet := []byte{
|
||||
0x45, 0x00, 0x00, 20,
|
||||
0x00, 0x01, 0x00, 0x00,
|
||||
64, 6, 0, 0,
|
||||
10, 0, 0, 1,
|
||||
10, 0, 0, 2,
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
}
|
||||
|
||||
hub.Push(key, packet)
|
||||
hub.Push(key, packet)
|
||||
hub.Push(key, packet)
|
||||
|
||||
first, ok := hub.Pop(context.Background(), key, 0)
|
||||
if !ok {
|
||||
t.Fatal("expected packet from queue")
|
||||
}
|
||||
batch := hub.PopBatch(context.Background(), key, 0, 1, 1024)
|
||||
if len(batch) != 1 {
|
||||
t.Fatalf("expected 1 packet because batch limit 1, got %d", len(batch))
|
||||
}
|
||||
|
||||
_ = first
|
||||
}
|
||||
|
||||
func TestVPNPacketHubFlowShardsReportDepths(t *testing.T) {
|
||||
hub := newVPNPacketHub()
|
||||
key := vpnPacketKey{
|
||||
ClusterID: "cluster-1",
|
||||
VPNConnectionID: "vpn-1",
|
||||
Direction: vpnDirectionGatewayToClient,
|
||||
}
|
||||
|
||||
for i := byte(1); i <= 8; i++ {
|
||||
packet := []byte{
|
||||
0x45, 0x00, 0x00, 24,
|
||||
0x00, i, 0x00, 0x00,
|
||||
64, 6, 0, 0,
|
||||
10, 0, 0, i,
|
||||
192, 168, 200, i,
|
||||
0x12, i, 0x56, i,
|
||||
}
|
||||
if err := hub.Push(key, packet); err != nil {
|
||||
t.Fatalf("push packet %d: %v", i, err)
|
||||
}
|
||||
}
|
||||
|
||||
statsAny := hub.Snapshot("cluster-1", "vpn-1")[vpnDirectionGatewayToClient]
|
||||
stats, ok := statsAny.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected stats payload type: %T", statsAny)
|
||||
}
|
||||
if got, ok := stats["queue_depth"].(int); !ok || got != 8 {
|
||||
t.Fatalf("expected queue_depth=8, got %v (ok=%v)", stats["queue_depth"], ok)
|
||||
}
|
||||
depths, ok := stats["queue_depths"].([]int)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected queue_depths payload type: %T", stats["queue_depths"])
|
||||
}
|
||||
if len(depths) != vpnPacketFlowShardCount {
|
||||
t.Fatalf("expected %d queue shards, got %d", vpnPacketFlowShardCount, len(depths))
|
||||
}
|
||||
nonEmpty := 0
|
||||
for _, depth := range depths {
|
||||
if depth > 0 {
|
||||
nonEmpty++
|
||||
}
|
||||
}
|
||||
if nonEmpty < 2 {
|
||||
t.Fatalf("expected packets to be distributed across at least 2 shards, got depths=%v", depths)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVPNPacketHubClearDoesNotCountAsDrop(t *testing.T) {
|
||||
hub := newVPNPacketHub()
|
||||
key := vpnPacketKey{
|
||||
ClusterID: "cluster-1",
|
||||
VPNConnectionID: "vpn-1",
|
||||
Direction: vpnDirectionClientToGateway,
|
||||
}
|
||||
packet := []byte{
|
||||
0x45, 0x00, 0x00, 20,
|
||||
0x00, 0x01, 0x00, 0x00,
|
||||
64, 6, 0, 0,
|
||||
10, 0, 0, 1,
|
||||
10, 0, 0, 2,
|
||||
0x12, 0x34, 0x56, 0x78,
|
||||
}
|
||||
if err := hub.Push(key, packet); err != nil {
|
||||
t.Fatalf("push packet: %v", err)
|
||||
}
|
||||
if cleared := hub.Clear(key); cleared != 1 {
|
||||
t.Fatalf("expected cleared=1, got %d", cleared)
|
||||
}
|
||||
statsAny := hub.Snapshot("cluster-1", "vpn-1")[vpnDirectionClientToGateway]
|
||||
stats, ok := statsAny.(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("unexpected stats payload type: %T", statsAny)
|
||||
}
|
||||
if got, ok := stats["dropped"].(uint64); !ok || got != 0 {
|
||||
t.Fatalf("expected dropped=0 for stale clear, got %v (ok=%v)", stats["dropped"], ok)
|
||||
}
|
||||
if got, ok := stats["cleared_stale_packets"].(uint64); !ok || got != 1 {
|
||||
t.Fatalf("expected cleared_stale_packets=1, got %v (ok=%v)", stats["cleared_stale_packets"], ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVPNClientDiagnosticStopCommandDrainsPendingWork(t *testing.T) {
|
||||
hub := newVPNClientDiagnosticHub()
|
||||
hub.Enqueue("cluster-1", "device-1", map[string]any{"type": "vpn_page_probe", "url": "https://speedtest.rt.ru/"})
|
||||
hub.Enqueue("cluster-1", "device-1", map[string]any{"type": "vpn_tcp_connect", "host": "192.168.200.95"})
|
||||
hub.Enqueue("cluster-1", "device-1", map[string]any{"type": "stop_vpn"})
|
||||
|
||||
item, ok := hub.Pop(context.Background(), "cluster-1", "device-1", time.Millisecond)
|
||||
if !ok {
|
||||
t.Fatal("expected priority stop command")
|
||||
}
|
||||
if got, _ := item.Payload["type"].(string); got != "stop_vpn" {
|
||||
t.Fatalf("first command = %q, want stop_vpn", got)
|
||||
}
|
||||
if item, ok := hub.Pop(context.Background(), "cluster-1", "device-1", 0); ok {
|
||||
t.Fatalf("expected old commands to be drained, got %#v", item.Payload)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -32,3 +32,136 @@ func TestMeshLatestObservationKeyDefaults(t *testing.T) {
|
||||
t.Fatalf("key = %q", key)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichVPNClientFabricRoutePrefersPlacementEntryAndActiveExit(t *testing.T) {
|
||||
item := VPNClientConnection{
|
||||
AllowedNodeIDs: []string{"node-a", "node-b", "node-b"},
|
||||
EntryNodeIDs: []string{"entry-1", "entry-2"},
|
||||
ExitNodeID: "exit-policy",
|
||||
ActiveLease: &NodeVPNAssignmentLease{
|
||||
OwnerNodeID: "exit-active",
|
||||
},
|
||||
ClientConfig: json.RawMessage(`{"routes":["0.0.0.0/0"]}`),
|
||||
}
|
||||
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(enrichVPNClientFabricRoute(item, "entry-2", ""), &cfg); err != nil {
|
||||
t.Fatalf("unmarshal enriched config: %v", err)
|
||||
}
|
||||
route, ok := cfg["vpn_fabric_route"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("missing vpn_fabric_route in %#v", cfg)
|
||||
}
|
||||
if route["preferred_data_plane"] != "fabric_mesh" || route["fallback_data_plane"] != "backend_relay" {
|
||||
t.Fatalf("unexpected data-plane route contract: %#v", route)
|
||||
}
|
||||
if route["selected_entry_node_id"] != "entry-2" || route["selected_exit_node_id"] != "exit-active" {
|
||||
t.Fatalf("unexpected selected route endpoints: %#v", route)
|
||||
}
|
||||
if route["route_candidate_count"].(float64) != 8 {
|
||||
t.Fatalf("route candidate count = %#v", route["route_candidate_count"])
|
||||
}
|
||||
candidates := route["route_candidates"].([]any)
|
||||
firstCandidate := candidates[0].(map[string]any)
|
||||
if firstCandidate["role"] != "preferred" || firstCandidate["entry_node_id"] != "entry-2" || firstCandidate["exit_node_id"] != "exit-active" {
|
||||
t.Fatalf("preferred route candidate = %#v", firstCandidate)
|
||||
}
|
||||
entryPool := route["entry_pool_node_ids"].([]any)
|
||||
exitPool := route["exit_pool_node_ids"].([]any)
|
||||
if len(entryPool) != 2 || entryPool[0] != "entry-1" || entryPool[1] != "entry-2" {
|
||||
t.Fatalf("entry pool = %#v", entryPool)
|
||||
}
|
||||
if len(exitPool) != 4 || exitPool[0] != "exit-policy" || exitPool[1] != "exit-active" || exitPool[2] != "node-a" || exitPool[3] != "node-b" {
|
||||
t.Fatalf("exit pool = %#v", exitPool)
|
||||
}
|
||||
contract, ok := cfg["vpn_dataplane_contract"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("missing vpn_dataplane_contract in %#v", cfg)
|
||||
}
|
||||
if contract["tunnel_type"] != "universal_ip_packet" || contract["application_protocol_agnostic"] != true {
|
||||
t.Fatalf("unexpected dataplane contract: %#v", contract)
|
||||
}
|
||||
failover := contract["failover"].(map[string]any)
|
||||
if failover["enabled"] != true || failover["alternate_route_count"].(float64) != 7 {
|
||||
t.Fatalf("unexpected failover contract: %#v", failover)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichVPNClientFabricRoutePrefersExplicitExit(t *testing.T) {
|
||||
item := VPNClientConnection{
|
||||
AllowedNodeIDs: []string{"node-a", "node-b", "node-c"},
|
||||
EntryNodeIDs: []string{"entry-1", "entry-2"},
|
||||
ExitNodeID: "exit-policy-a",
|
||||
ActiveLease: &NodeVPNAssignmentLease{
|
||||
OwnerNodeID: "",
|
||||
},
|
||||
ClientConfig: json.RawMessage(`{"routes":["0.0.0.0/0"]}`),
|
||||
}
|
||||
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(enrichVPNClientFabricRoute(item, "entry-1", "node-c"), &cfg); err != nil {
|
||||
t.Fatalf("unmarshal enriched config: %v", err)
|
||||
}
|
||||
route, ok := cfg["vpn_fabric_route"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("missing vpn_fabric_route in %#v", cfg)
|
||||
}
|
||||
if route["selected_entry_node_id"] != "entry-1" {
|
||||
t.Fatalf("unexpected selected entry: %#v", route["selected_entry_node_id"])
|
||||
}
|
||||
if route["selected_exit_node_id"] != "node-c" {
|
||||
t.Fatalf("unexpected selected exit: %#v", route["selected_exit_node_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnrichVPNClientEntryEndpointCandidatesAddsReportedEntryAPI(t *testing.T) {
|
||||
item := VPNClientConnection{
|
||||
EntryNodeIDs: []string{"entry-1"},
|
||||
ClientConfig: json.RawMessage(`{
|
||||
"vpn_fabric_route": {
|
||||
"status": "planned",
|
||||
"selected_entry_node_id": "entry-1",
|
||||
"selected_exit_node_id": "exit-1"
|
||||
}
|
||||
}`),
|
||||
}
|
||||
heartbeatMetadata := json.RawMessage(`{
|
||||
"mesh_endpoint_report": {
|
||||
"transport": "direct_http",
|
||||
"connectivity_mode": "direct",
|
||||
"nat_type": "none",
|
||||
"region": "test",
|
||||
"peer_endpoint": "http://entry.example.test:19131",
|
||||
"endpoint_candidates": [{
|
||||
"endpoint_id": "public-http",
|
||||
"node_id": "entry-1",
|
||||
"transport": "direct_http",
|
||||
"address": "http://entry.example.test:19131",
|
||||
"reachability": "public",
|
||||
"priority": 0
|
||||
}]
|
||||
}
|
||||
}`)
|
||||
endpoints := map[string][]map[string]any{
|
||||
"entry-1": vpnEntryEndpointCandidatesFromHeartbeat("entry-1", json.RawMessage(`{"vpn_local_gateway_shortcut":true}`), heartbeatMetadata),
|
||||
}
|
||||
|
||||
var cfg map[string]any
|
||||
if err := json.Unmarshal(enrichVPNClientEntryEndpointCandidates(item, endpoints), &cfg); err != nil {
|
||||
t.Fatalf("unmarshal enriched config: %v", err)
|
||||
}
|
||||
if cfg["vpn_entry_endpoint_candidate_count"].(float64) != 1 {
|
||||
t.Fatalf("candidate count = %#v", cfg["vpn_entry_endpoint_candidate_count"])
|
||||
}
|
||||
candidates := cfg["vpn_entry_endpoint_candidates"].([]any)
|
||||
candidate := candidates[0].(map[string]any)
|
||||
if candidate["node_id"] != "entry-1" || candidate["api_base_url"] != "http://entry.example.test:19131/api/v1" {
|
||||
t.Fatalf("unexpected endpoint candidate: %#v", candidate)
|
||||
}
|
||||
if candidate["local_gateway_shortcut"] != true {
|
||||
t.Fatalf("local gateway shortcut missing: %#v", candidate)
|
||||
}
|
||||
if candidate["selected_entry"] != true || candidate["source"] != "node_latest_heartbeat.mesh_endpoint_report.endpoint_candidates" {
|
||||
t.Fatalf("unexpected endpoint metadata: %#v", candidate)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ type Repository interface {
|
||||
AssignNodeToGroup(ctx context.Context, input AssignNodeGroupInput) (ClusterNode, error)
|
||||
|
||||
CreateJoinToken(ctx context.Context, input CreateJoinTokenInput, tokenHash string) (NodeJoinToken, error)
|
||||
ListJoinTokens(ctx context.Context, clusterID string) ([]NodeJoinToken, error)
|
||||
SetJoinTokenAuthority(ctx context.Context, clusterID, tokenID string, payload json.RawMessage, signature ClusterSignature) (NodeJoinToken, error)
|
||||
GetValidJoinTokenByHash(ctx context.Context, clusterID, tokenHash string) (NodeJoinToken, error)
|
||||
RevokeJoinToken(ctx context.Context, input RevokeJoinTokenInput) (NodeJoinToken, error)
|
||||
@@ -40,8 +41,16 @@ type Repository interface {
|
||||
|
||||
RecordHeartbeat(ctx context.Context, input RecordHeartbeatInput) (NodeHeartbeat, error)
|
||||
ListNodeHeartbeats(ctx context.Context, clusterID, nodeID string, limit int) ([]NodeHeartbeat, error)
|
||||
CreateReleaseVersion(ctx context.Context, input CreateReleaseVersionInput) (ReleaseVersion, error)
|
||||
ListReleaseVersions(ctx context.Context, clusterID, product, channel string) ([]ReleaseVersion, error)
|
||||
ListNodeUpdateServiceCandidates(ctx context.Context, clusterID string) ([]NodeUpdateServiceCandidate, error)
|
||||
UpsertNodeUpdatePolicy(ctx context.Context, input UpsertNodeUpdatePolicyInput) (NodeUpdatePolicy, error)
|
||||
GetNodeUpdatePolicy(ctx context.Context, clusterID, nodeID, product string) (NodeUpdatePolicy, error)
|
||||
ReportNodeUpdateStatus(ctx context.Context, input ReportNodeUpdateStatusInput) (NodeUpdateStatus, error)
|
||||
ListNodeUpdateStatuses(ctx context.Context, clusterID, nodeID string, limit int) ([]NodeUpdateStatus, error)
|
||||
RevokeNodeIdentity(ctx context.Context, input RevokeNodeIdentityInput) error
|
||||
DisableClusterMembership(ctx context.Context, input DisableMembershipInput) error
|
||||
DeleteClusterNode(ctx context.Context, input DeleteClusterNodeInput) error
|
||||
UpsertFabricTestingFlag(ctx context.Context, input UpsertFabricTestingFlagInput) (FabricTestingFlag, error)
|
||||
ListFabricTestingFlags(ctx context.Context) ([]FabricTestingFlag, error)
|
||||
GetEffectiveNodeTestingFlags(ctx context.Context, clusterID, nodeID string) (EffectiveNodeTestingFlags, error)
|
||||
@@ -55,6 +64,22 @@ type Repository interface {
|
||||
ListMeshLinks(ctx context.Context, clusterID string) ([]MeshLinkObservation, error)
|
||||
CreateRouteIntent(ctx context.Context, input CreateRouteIntentInput) (MeshRouteIntent, error)
|
||||
ListRouteIntents(ctx context.Context, clusterID string) ([]MeshRouteIntent, error)
|
||||
ExpireRouteIntent(ctx context.Context, input RouteIntentLifecycleInput, expiresAt time.Time) (MeshRouteIntent, error)
|
||||
DisableRouteIntent(ctx context.Context, input RouteIntentLifecycleInput) (MeshRouteIntent, error)
|
||||
RecordFabricServiceChannelRouteFeedback(ctx context.Context, input RecordFabricServiceChannelRouteFeedbackInput) (FabricServiceChannelRouteFeedbackObservation, error)
|
||||
ListFabricServiceChannelRouteFeedback(ctx context.Context, input ListFabricServiceChannelRouteFeedbackInput) ([]FabricServiceChannelRouteFeedbackObservation, error)
|
||||
ExpireFabricServiceChannelRouteFeedback(ctx context.Context, input ExpireFabricServiceChannelRouteFeedbackInput) (ExpireFabricServiceChannelRouteFeedbackResult, error)
|
||||
StoreFabricServiceChannelLease(ctx context.Context, input StoreFabricServiceChannelLeaseInput) (FabricServiceChannelLeaseRecord, error)
|
||||
GetFabricServiceChannelLease(ctx context.Context, clusterID, channelID string) (FabricServiceChannelLeaseRecord, error)
|
||||
ListFabricServiceChannelLeases(ctx context.Context, input ListFabricServiceChannelLeasesInput) ([]FabricServiceChannelLeaseRecord, error)
|
||||
CleanupExpiredFabricServiceChannelLeases(ctx context.Context, clusterID string, now time.Time, limit int) (int, error)
|
||||
RecordFabricServiceChannelRouteRebuildAttempt(ctx context.Context, input RecordFabricServiceChannelRouteRebuildAttemptInput) (FabricServiceChannelRouteRebuildAttempt, error)
|
||||
ListFabricServiceChannelRouteRebuildAttempts(ctx context.Context, input ListFabricServiceChannelRouteRebuildAttemptsInput) ([]FabricServiceChannelRouteRebuildAttempt, error)
|
||||
UpdateFabricServiceChannelRouteRebuildCorrelationSnapshot(ctx context.Context, input UpdateFabricServiceChannelRouteRebuildCorrelationSnapshotInput) error
|
||||
GetFabricServiceChannelSchemaStatus(ctx context.Context, input GetFabricServiceChannelSchemaStatusInput) (FabricServiceChannelSchemaStatus, error)
|
||||
UpsertFabricServiceChannelRouteRebuildAlertSilence(ctx context.Context, input SilenceFabricServiceChannelRouteRebuildAlertInput, expiresAt time.Time) (FabricServiceChannelRouteRebuildAlertSilence, error)
|
||||
ListFabricServiceChannelRouteRebuildAlertSilences(ctx context.Context, clusterID string, now time.Time) ([]FabricServiceChannelRouteRebuildAlertSilence, error)
|
||||
DeleteFabricServiceChannelRouteRebuildAlertSilence(ctx context.Context, input UnsilenceFabricServiceChannelRouteRebuildAlertInput) (FabricServiceChannelRouteRebuildAlertSilence, error)
|
||||
ListQoSPolicies(ctx context.Context, clusterID string) ([]MeshQoSPolicy, error)
|
||||
ListFabricEntryPoints(ctx context.Context, clusterID string) ([]FabricEntryPoint, error)
|
||||
CreateFabricEntryPoint(ctx context.Context, input CreateFabricEntryPointInput) (FabricEntryPoint, error)
|
||||
@@ -78,6 +103,7 @@ type Repository interface {
|
||||
ListVPNConnectionAllowedNodes(ctx context.Context, clusterID, vpnConnectionID string) ([]VPNConnectionAllowedNode, error)
|
||||
AcquireVPNConnectionLease(ctx context.Context, input AcquireVPNConnectionLeaseInput, expiresAt time.Time, fencingToken string) (VPNConnectionLease, error)
|
||||
RenewVPNConnectionLease(ctx context.Context, input RenewVPNConnectionLeaseInput, expiresAt time.Time) (VPNConnectionLease, error)
|
||||
RenewNodeVPNAssignmentLease(ctx context.Context, input RenewNodeVPNAssignmentLeaseInput, expiresAt time.Time) (VPNConnectionLease, error)
|
||||
ReleaseVPNConnectionLease(ctx context.Context, input ReleaseVPNConnectionLeaseInput) (VPNConnectionLease, error)
|
||||
FenceVPNConnectionLease(ctx context.Context, input FenceVPNConnectionLeaseInput) (VPNConnectionLease, error)
|
||||
GetActiveVPNConnectionLease(ctx context.Context, clusterID, vpnConnectionID string) (VPNConnectionLease, error)
|
||||
@@ -85,7 +111,8 @@ type Repository interface {
|
||||
ExpireStaleVPNConnectionLeases(ctx context.Context, clusterID string, now time.Time) ([]VPNConnectionLease, error)
|
||||
ListNodeVPNAssignments(ctx context.Context, clusterID, nodeID string) ([]NodeVPNAssignment, error)
|
||||
ReportNodeVPNAssignmentStatus(ctx context.Context, input ReportNodeVPNAssignmentStatusInput) (NodeVPNAssignmentStatus, error)
|
||||
GetVPNClientProfile(ctx context.Context, clusterID, organizationID, userID, preferredEntryNodeID, preferredExitNodeID string, generatedAt time.Time) (VPNClientProfile, error)
|
||||
|
||||
RecordAudit(ctx context.Context, event ClusterAuditEvent) error
|
||||
ListAuditEvents(ctx context.Context, clusterID string, limit int) ([]ClusterAuditEvent, error)
|
||||
ListAuditEvents(ctx context.Context, input ListAuditEventsInput) ([]ClusterAuditEvent, error)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -40,6 +40,9 @@ func (m *Module) Name() string {
|
||||
|
||||
func (m *Module) RegisterRoutes(router chi.Router) {
|
||||
router.Route("/node-agents", func(r chi.Router) {
|
||||
r.Post("/docker-install-profile", m.dockerInstallProfile)
|
||||
r.Post("/windows-install-profile", m.windowsInstallProfile)
|
||||
r.Post("/linux-install-profile", m.linuxInstallProfile)
|
||||
r.Post("/enroll", m.enrollAgent)
|
||||
r.Post("/enrollments/{requestID}/bootstrap", m.bootstrapEnrollment)
|
||||
r.Post("/register", m.registerAgent)
|
||||
@@ -53,6 +56,48 @@ func (m *Module) RegisterRoutes(router chi.Router) {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Module) linuxInstallProfile(w http.ResponseWriter, r *http.Request) {
|
||||
var payload clustermodule.DockerInstallProfileRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid linux install profile payload")
|
||||
return
|
||||
}
|
||||
profile, err := m.cluster.GetLinuxInstallProfile(r.Context(), payload)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"linux_install_profile": profile})
|
||||
}
|
||||
|
||||
func (m *Module) windowsInstallProfile(w http.ResponseWriter, r *http.Request) {
|
||||
var payload clustermodule.DockerInstallProfileRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid windows install profile payload")
|
||||
return
|
||||
}
|
||||
profile, err := m.cluster.GetWindowsInstallProfile(r.Context(), payload)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"windows_install_profile": profile})
|
||||
}
|
||||
|
||||
func (m *Module) dockerInstallProfile(w http.ResponseWriter, r *http.Request) {
|
||||
var payload clustermodule.DockerInstallProfileRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, "invalid docker install profile payload")
|
||||
return
|
||||
}
|
||||
profile, err := m.cluster.GetDockerInstallProfile(r.Context(), payload)
|
||||
if err != nil {
|
||||
httpx.WriteError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
httpx.WriteJSON(w, http.StatusOK, map[string]any{"docker_install_profile": profile})
|
||||
}
|
||||
|
||||
func (m *Module) enrollAgent(w http.ResponseWriter, r *http.Request) {
|
||||
var payload struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
|
||||
@@ -242,6 +242,37 @@ func (m *Module) loadAdminSummary(ctx context.Context, orgID string) (AdminSumma
|
||||
return AdminSummary{}, err
|
||||
}
|
||||
|
||||
var vpnConnectionCount int64
|
||||
var vpnActiveLeaseCount int64
|
||||
var vpnForwardingCount int64
|
||||
if err := m.db.QueryRow(ctx, `
|
||||
SELECT COUNT(*)
|
||||
FROM vpn_connections
|
||||
WHERE organization_id = $1::uuid
|
||||
AND desired_state = 'enabled'
|
||||
`, orgID).Scan(&vpnConnectionCount); err != nil {
|
||||
return AdminSummary{}, err
|
||||
}
|
||||
if err := m.db.QueryRow(ctx, `
|
||||
SELECT COUNT(*)
|
||||
FROM vpn_connection_leases l
|
||||
INNER JOIN vpn_connections vc ON vc.id = l.vpn_connection_id
|
||||
WHERE vc.organization_id = $1::uuid
|
||||
AND l.status = 'active'
|
||||
AND l.expires_at > NOW()
|
||||
`, orgID).Scan(&vpnActiveLeaseCount); err != nil {
|
||||
return AdminSummary{}, err
|
||||
}
|
||||
if err := m.db.QueryRow(ctx, `
|
||||
SELECT COUNT(*)
|
||||
FROM vpn_connection_assignment_latest_statuses s
|
||||
INNER JOIN vpn_connections vc ON vc.id = s.vpn_connection_id
|
||||
WHERE vc.organization_id = $1::uuid
|
||||
AND COALESCE((s.status_payload->>'packet_forwarding')::boolean, false)
|
||||
`, orgID).Scan(&vpnForwardingCount); err != nil {
|
||||
return AdminSummary{}, err
|
||||
}
|
||||
|
||||
auditRows, err := m.db.Query(ctx, `
|
||||
SELECT ae.id::text, ae.event_type, ae.target_type, ae.target_id, ae.payload, ae.created_at
|
||||
FROM audit_events ae
|
||||
@@ -265,6 +296,12 @@ func (m *Module) loadAdminSummary(ctx context.Context, orgID string) (AdminSumma
|
||||
if err := auditRows.Err(); err != nil {
|
||||
return AdminSummary{}, err
|
||||
}
|
||||
if services == nil {
|
||||
services = []ServiceSummary{}
|
||||
}
|
||||
if audit == nil {
|
||||
audit = []OrgAuditEvent{}
|
||||
}
|
||||
|
||||
return AdminSummary{
|
||||
OrganizationID: orgID,
|
||||
@@ -272,14 +309,34 @@ func (m *Module) loadAdminSummary(ctx context.Context, orgID string) (AdminSumma
|
||||
ActiveSessionCount: activeSessionCount,
|
||||
ServiceEndpoints: services,
|
||||
ConnectorStatus: map[string]any{
|
||||
"vpn": "not_implemented",
|
||||
"connector": "not_implemented",
|
||||
"vpn": map[string]any{
|
||||
"enabled_connections": vpnConnectionCount,
|
||||
"active_leases": vpnActiveLeaseCount,
|
||||
"packet_forwarding": vpnForwardingCount,
|
||||
"status": connectorStatus(vpnConnectionCount, vpnActiveLeaseCount, vpnForwardingCount),
|
||||
},
|
||||
"rdp": map[string]any{
|
||||
"status": "resource_catalog_ready",
|
||||
},
|
||||
},
|
||||
RecentAudit: audit,
|
||||
TopologyExposure: tenantSafeTopologyExposure(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func connectorStatus(enabledConnections, activeLeases, forwarding int64) string {
|
||||
if enabledConnections == 0 {
|
||||
return "not_configured"
|
||||
}
|
||||
if forwarding > 0 {
|
||||
return "active"
|
||||
}
|
||||
if activeLeases > 0 {
|
||||
return "gateway_blocked"
|
||||
}
|
||||
return "waiting_for_gateway"
|
||||
}
|
||||
|
||||
func tenantSafeTopologyExposure() string {
|
||||
return "tenant_safe_no_core_mesh_topology"
|
||||
}
|
||||
|
||||
@@ -49,6 +49,7 @@ type Resource struct {
|
||||
Address string `json:"address"`
|
||||
Protocol string `json:"protocol"`
|
||||
SecretRef *string `json:"secret_ref,omitempty"`
|
||||
HasSecret bool `json:"has_secret"`
|
||||
CertificateVerificationMode string `json:"certificate_verification_mode"`
|
||||
RenderQualityProfile string `json:"render_quality_profile"`
|
||||
ClipboardMode string `json:"clipboard_mode"`
|
||||
@@ -116,6 +117,7 @@ func (m *Module) listResources(w http.ResponseWriter, r *http.Request) {
|
||||
query := `
|
||||
SELECT r.id, r.organization_id, r.name, r.address, r.protocol, r.secret_ref,
|
||||
r.certificate_verification_mode, r.metadata, r.created_at, r.updated_at,
|
||||
EXISTS (SELECT 1 FROM resource_secrets sec WHERE sec.resource_id = r.id) AS has_secret,
|
||||
COALESCE(rp.clipboard_mode, 'disabled') AS clipboard_mode,
|
||||
COALESCE(rp.file_transfer_mode, 'disabled') AS file_transfer_mode
|
||||
FROM resources r
|
||||
@@ -500,6 +502,7 @@ func (m *Module) getByID(ctx context.Context, resourceID string) (Resource, erro
|
||||
row := m.db.QueryRow(ctx, `
|
||||
SELECT r.id, r.organization_id, r.name, r.address, r.protocol, r.secret_ref,
|
||||
r.certificate_verification_mode, r.metadata, r.created_at, r.updated_at,
|
||||
EXISTS (SELECT 1 FROM resource_secrets sec WHERE sec.resource_id = r.id) AS has_secret,
|
||||
COALESCE(rp.clipboard_mode, 'disabled') AS clipboard_mode,
|
||||
COALESCE(rp.file_transfer_mode, 'disabled') AS file_transfer_mode
|
||||
FROM resources r
|
||||
@@ -555,6 +558,7 @@ func scanResource(row rowScanner) (Resource, error) {
|
||||
&resource.Metadata,
|
||||
&resource.CreatedAt,
|
||||
&resource.UpdatedAt,
|
||||
&resource.HasSecret,
|
||||
&resource.ClipboardMode,
|
||||
&resource.FileTransferMode,
|
||||
); err != nil {
|
||||
|
||||
@@ -214,9 +214,49 @@ END, prg.granted_at DESC
|
||||
if err := rows.Err(); err != nil {
|
||||
return "", fmt.Errorf("iterate platform role grants: %w", err)
|
||||
}
|
||||
if bestRole == PlatformRoleUser {
|
||||
if role, ok, err := strictBootstrappedOwnerFallback(ctx, db, verifier, userID, email); err != nil {
|
||||
return "", err
|
||||
} else if ok {
|
||||
return role, nil
|
||||
}
|
||||
return legacyPlatformRole(ctx, db, userID)
|
||||
}
|
||||
return bestRole, nil
|
||||
}
|
||||
|
||||
func strictBootstrappedOwnerFallback(ctx context.Context, db postgresplatform.DBTX, verifier *Verifier, userID, email string) (string, bool, error) {
|
||||
var role string
|
||||
var bootstrappedOwnerEmail *string
|
||||
var authorityState string
|
||||
var rootFingerprint string
|
||||
err := db.QueryRow(ctx, `
|
||||
SELECT u.platform_role, ia.bootstrapped_owner_email, ia.authority_state, ia.product_root_key_fingerprint
|
||||
FROM users u
|
||||
CROSS JOIN installation_authority ia
|
||||
WHERE u.id = $1::uuid
|
||||
AND ia.id = 1
|
||||
`, userID).Scan(&role, &bootstrappedOwnerEmail, &authorityState, &rootFingerprint)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return PlatformRoleUser, false, nil
|
||||
}
|
||||
return "", false, fmt.Errorf("query strict bootstrapped owner fallback: %w", err)
|
||||
}
|
||||
if bootstrappedOwnerEmail == nil ||
|
||||
!strings.EqualFold(*bootstrappedOwnerEmail, email) ||
|
||||
authorityState != "active" ||
|
||||
rootFingerprint != verifier.RootFingerprint() {
|
||||
return PlatformRoleUser, false, nil
|
||||
}
|
||||
switch role {
|
||||
case PlatformRoleAdmin, PlatformRoleRecoveryAdmin:
|
||||
return role, true, nil
|
||||
default:
|
||||
return PlatformRoleUser, false, nil
|
||||
}
|
||||
}
|
||||
|
||||
func legacyPlatformRole(ctx context.Context, db postgresplatform.DBTX, userID string) (string, error) {
|
||||
var role string
|
||||
if err := db.QueryRow(ctx, `SELECT platform_role FROM users WHERE id = $1::uuid`, userID).Scan(&role); err != nil {
|
||||
|
||||
@@ -2,6 +2,7 @@ package runtime
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
@@ -208,6 +209,7 @@ func buildRouter(logger *slog.Logger, modules ...module.Module) http.Handler {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
_, _ = w.Write([]byte("ready"))
|
||||
})
|
||||
router.Post("/mesh/v1/health", controlPlaneMeshHealth)
|
||||
|
||||
router.Route("/api/v1", func(r chi.Router) {
|
||||
for _, mod := range modules {
|
||||
@@ -218,3 +220,34 @@ func buildRouter(logger *slog.Logger, modules ...module.Module) http.Handler {
|
||||
|
||||
return router
|
||||
}
|
||||
|
||||
func controlPlaneMeshHealth(w http.ResponseWriter, r *http.Request) {
|
||||
var message struct {
|
||||
ProtocolVersion string `json:"protocol_version"`
|
||||
From struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeID string `json:"node_id"`
|
||||
} `json:"from"`
|
||||
To struct {
|
||||
ClusterID string `json:"cluster_id"`
|
||||
NodeID string `json:"node_id"`
|
||||
} `json:"to"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&message); err != nil {
|
||||
http.Error(w, "invalid mesh health message", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if message.ProtocolVersion != "mesh-control-v1" || message.From.ClusterID == "" || message.From.NodeID == "" {
|
||||
http.Error(w, "invalid mesh health message", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"protocol_version": "mesh-control-v1",
|
||||
"accepted": true,
|
||||
"by": map[string]string{
|
||||
"cluster_id": message.From.ClusterID,
|
||||
"node_id": "control-plane-relay",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user