feat(vNext.4): implement trusted-subject security chain for portal user key self-service
- Add portal_auth.go: Portal user session auth with HMAC-signed cookies
- Add /api/portal/session/{login,logout,state} endpoints
- Update nginx config template: cookie-to-header trusted proxy pattern
- Update frontend: sync CRM session on login/logout
- Add TRUSTED_SUBJECT_DEPLOY_GUIDE.md with remote43 deployment steps
- Update EXECUTION_BOARD.md: mark trusted-subject blocking issue as resolved
This implements the secure chain:
Browser → Portal → nginx (cookie→header) → CRM (verify proxy secret)
Required remote43 actions:
1. Generate 64-char hex secret
2. Update .env.crm with TRUSTED_* config
3. Update nginx with cookie map and header injection
4. Restart services
Fixes EXECUTION_BOARD.md 2026-06-08 blocking issue
This commit is contained in:
@@ -85,15 +85,15 @@ func TestUserKeysRepoUpdateSecret(t *testing.T) {
|
||||
t.Fatalf("Create() error = %v", err)
|
||||
}
|
||||
|
||||
if err := store.UserKeys().UpdateSecret(ctx, "key_rotate_001", "sha256:new", "sk-****new1", "active"); err != nil {
|
||||
if err := store.UserKeys().UpdateSecret(ctx, "key_rotate_001", "subject|key:key_rotate_001|rot:key_nonce", "sha256:new", "sk-****new1", "active"); err != nil {
|
||||
t.Fatalf("UpdateSecret() error = %v", err)
|
||||
}
|
||||
key, err := store.UserKeys().GetByID(ctx, "key_rotate_001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetByID() error = %v", err)
|
||||
}
|
||||
if key.KeyFingerprint != "sha256:new" || key.MaskedPreview != "sk-****new1" || key.AdminStatus != "active" {
|
||||
t.Fatalf("updated key = %+v, want new fingerprint/mask/status", key)
|
||||
if key.ManagedIdentitySelector != "subject|key:key_rotate_001|rot:key_nonce" || key.KeyFingerprint != "sha256:new" || key.MaskedPreview != "sk-****new1" || key.AdminStatus != "active" {
|
||||
t.Fatalf("updated key = %+v, want new selector/fingerprint/mask/status", key)
|
||||
}
|
||||
if strings.TrimSpace(key.UpdatedAt) == "" {
|
||||
t.Fatalf("UpdatedAt = %q, want non-empty", key.UpdatedAt)
|
||||
|
||||
@@ -9,20 +9,21 @@ import (
|
||||
)
|
||||
|
||||
type UserKeyRecord struct {
|
||||
ID int64 `json:"-"`
|
||||
KeyID string `json:"key_id"`
|
||||
OwnerSubjectID string `json:"owner_subject_id"`
|
||||
KeyFingerprint string `json:"key_fingerprint"`
|
||||
MaskedPreview string `json:"masked_preview"`
|
||||
DisplayName string `json:"display_name"`
|
||||
LogicalGroupID string `json:"logical_group_id"`
|
||||
AllowedModels []string `json:"allowed_models"`
|
||||
AdminStatus string `json:"admin_status"`
|
||||
QuotaStatus string `json:"quota_status"`
|
||||
LastUsedAt string `json:"last_used_at,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ExpiresAt string `json:"expires_at,omitempty"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
ID int64 `json:"-"`
|
||||
KeyID string `json:"key_id"`
|
||||
OwnerSubjectID string `json:"owner_subject_id"`
|
||||
ManagedIdentitySelector string `json:"-"`
|
||||
KeyFingerprint string `json:"key_fingerprint"`
|
||||
MaskedPreview string `json:"masked_preview"`
|
||||
DisplayName string `json:"display_name"`
|
||||
LogicalGroupID string `json:"logical_group_id"`
|
||||
AllowedModels []string `json:"allowed_models"`
|
||||
AdminStatus string `json:"admin_status"`
|
||||
QuotaStatus string `json:"quota_status"`
|
||||
LastUsedAt string `json:"last_used_at,omitempty"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ExpiresAt string `json:"expires_at,omitempty"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type UserKeysRepo struct {
|
||||
@@ -40,11 +41,11 @@ func (r *UserKeysRepo) Create(ctx context.Context, key UserKeyRecord) (int64, er
|
||||
}
|
||||
result, err := r.db.ExecContext(ctx, `
|
||||
INSERT INTO user_keys (
|
||||
key_id, owner_subject_id, key_fingerprint, masked_preview,
|
||||
key_id, owner_subject_id, managed_identity_selector, key_fingerprint, masked_preview,
|
||||
display_name, logical_group_id, allowed_models,
|
||||
admin_status, quota_status
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
key.KeyID, key.OwnerSubjectID, key.KeyFingerprint, key.MaskedPreview,
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
key.KeyID, key.OwnerSubjectID, key.ManagedIdentitySelector, key.KeyFingerprint, key.MaskedPreview,
|
||||
key.DisplayName, key.LogicalGroupID, string(modelsJSON),
|
||||
key.AdminStatus, key.QuotaStatus,
|
||||
)
|
||||
@@ -60,7 +61,7 @@ func scanUserKeys(rows *sql.Rows) ([]UserKeyRecord, error) {
|
||||
var k UserKeyRecord
|
||||
var modelsJSON, lastUsedAt, expiresAt sql.NullString
|
||||
err := rows.Scan(
|
||||
&k.ID, &k.KeyID, &k.OwnerSubjectID, &k.KeyFingerprint, &k.MaskedPreview,
|
||||
&k.ID, &k.KeyID, &k.OwnerSubjectID, &k.ManagedIdentitySelector, &k.KeyFingerprint, &k.MaskedPreview,
|
||||
&k.DisplayName, &k.LogicalGroupID, &modelsJSON,
|
||||
&k.AdminStatus, &k.QuotaStatus, &lastUsedAt, &k.CreatedAt, &expiresAt, &k.UpdatedAt,
|
||||
)
|
||||
@@ -81,7 +82,7 @@ func scanOneUserKey(row *sql.Row) (*UserKeyRecord, error) {
|
||||
var k UserKeyRecord
|
||||
var modelsJSON, lastUsedAt, expiresAt sql.NullString
|
||||
err := row.Scan(
|
||||
&k.ID, &k.KeyID, &k.OwnerSubjectID, &k.KeyFingerprint, &k.MaskedPreview,
|
||||
&k.ID, &k.KeyID, &k.OwnerSubjectID, &k.ManagedIdentitySelector, &k.KeyFingerprint, &k.MaskedPreview,
|
||||
&k.DisplayName, &k.LogicalGroupID, &modelsJSON,
|
||||
&k.AdminStatus, &k.QuotaStatus, &lastUsedAt, &k.CreatedAt, &expiresAt, &k.UpdatedAt,
|
||||
)
|
||||
@@ -98,7 +99,7 @@ func scanOneUserKey(row *sql.Row) (*UserKeyRecord, error) {
|
||||
|
||||
func (r *UserKeysRepo) ListByOwner(ctx context.Context, subjectID string) ([]UserKeyRecord, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, key_id, owner_subject_id, key_fingerprint, masked_preview,
|
||||
SELECT id, key_id, owner_subject_id, managed_identity_selector, key_fingerprint, masked_preview,
|
||||
display_name, logical_group_id, allowed_models,
|
||||
admin_status, quota_status, last_used_at, created_at, expires_at, updated_at
|
||||
FROM user_keys WHERE owner_subject_id = ? ORDER BY created_at DESC`, subjectID)
|
||||
@@ -111,7 +112,7 @@ func (r *UserKeysRepo) ListByOwner(ctx context.Context, subjectID string) ([]Use
|
||||
|
||||
func (r *UserKeysRepo) ListByFingerprint(ctx context.Context, fingerprint string) ([]UserKeyRecord, error) {
|
||||
rows, err := r.db.QueryContext(ctx, `
|
||||
SELECT id, key_id, owner_subject_id, key_fingerprint, masked_preview,
|
||||
SELECT id, key_id, owner_subject_id, managed_identity_selector, key_fingerprint, masked_preview,
|
||||
display_name, logical_group_id, allowed_models,
|
||||
admin_status, quota_status, last_used_at, created_at, expires_at, updated_at
|
||||
FROM user_keys WHERE key_fingerprint = ? ORDER BY created_at DESC`, fingerprint)
|
||||
@@ -124,7 +125,7 @@ func (r *UserKeysRepo) ListByFingerprint(ctx context.Context, fingerprint string
|
||||
|
||||
func (r *UserKeysRepo) GetByID(ctx context.Context, keyID string) (*UserKeyRecord, error) {
|
||||
row := r.db.QueryRowContext(ctx, `
|
||||
SELECT id, key_id, owner_subject_id, key_fingerprint, masked_preview,
|
||||
SELECT id, key_id, owner_subject_id, managed_identity_selector, key_fingerprint, masked_preview,
|
||||
display_name, logical_group_id, allowed_models,
|
||||
admin_status, quota_status, last_used_at, created_at, expires_at, updated_at
|
||||
FROM user_keys WHERE key_id = ?`, keyID)
|
||||
@@ -154,8 +155,9 @@ func (r *UserKeysRepo) UpdateStatus(ctx context.Context, keyID string, adminStat
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *UserKeysRepo) UpdateSecret(ctx context.Context, keyID, fingerprint, maskedPreview, adminStatus string) error {
|
||||
func (r *UserKeysRepo) UpdateSecret(ctx context.Context, keyID, managedIdentitySelector, fingerprint, maskedPreview, adminStatus string) error {
|
||||
keyID = strings.TrimSpace(keyID)
|
||||
managedIdentitySelector = strings.TrimSpace(managedIdentitySelector)
|
||||
fingerprint = strings.TrimSpace(fingerprint)
|
||||
maskedPreview = strings.TrimSpace(maskedPreview)
|
||||
adminStatus = strings.ToLower(strings.TrimSpace(adminStatus))
|
||||
@@ -174,9 +176,9 @@ func (r *UserKeysRepo) UpdateSecret(ctx context.Context, keyID, fingerprint, mas
|
||||
}
|
||||
result, err := r.db.ExecContext(ctx,
|
||||
`UPDATE user_keys
|
||||
SET key_fingerprint = ?, masked_preview = ?, admin_status = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
SET managed_identity_selector = ?, key_fingerprint = ?, masked_preview = ?, admin_status = ?, updated_at = strftime('%Y-%m-%dT%H:%M:%SZ','now')
|
||||
WHERE key_id = ?`,
|
||||
fingerprint, maskedPreview, adminStatus, keyID,
|
||||
managedIdentitySelector, fingerprint, maskedPreview, adminStatus, keyID,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update user_key secret: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user