Initial project snapshot

This commit is contained in:
2026-04-28 22:29:50 +03:00
commit 8ba0561f4f
365 changed files with 91832 additions and 0 deletions
@@ -0,0 +1,37 @@
package secrets
import (
"encoding/json"
"fmt"
)
type AssignmentSecretMergeResult struct {
Metadata map[string]any
Keys []string
}
func MergeResourceSecretIntoAssignmentMetadata(metadata map[string]any, payload json.RawMessage) (AssignmentSecretMergeResult, error) {
if metadata == nil {
metadata = map[string]any{}
}
var secretPayload map[string]any
if err := json.Unmarshal(payload, &secretPayload); err != nil {
return AssignmentSecretMergeResult{}, fmt.Errorf("decode resolved resource secret: %w", err)
}
resource, _ := metadata["resource"].(map[string]any)
if resource == nil {
resource = map[string]any{}
metadata["resource"] = resource
}
resourceMetadata, _ := resource["metadata"].(map[string]any)
if resourceMetadata == nil {
resourceMetadata = map[string]any{}
resource["metadata"] = resourceMetadata
}
keys := make([]string, 0, len(secretPayload))
for key, value := range secretPayload {
resourceMetadata[key] = value
keys = append(keys, key)
}
return AssignmentSecretMergeResult{Metadata: metadata, Keys: keys}, nil
}
@@ -0,0 +1,113 @@
package secrets
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"io"
"strings"
)
const AlgorithmAES256GCM = "AES-256-GCM"
var (
ErrSecretEncryptionKeyMissing = errors.New("secret encryption key is not configured")
ErrSecretPayloadInvalid = errors.New("secret payload must be a json object")
)
type Encryptor struct {
aead cipher.AEAD
keyID string
}
type EncryptedPayload struct {
Algorithm string
KeyID string
Nonce []byte
Ciphertext []byte
PayloadSHA256 string
}
func NewEncryptor(masterKeyBase64, keyID string) (*Encryptor, error) {
masterKeyBase64 = strings.TrimSpace(masterKeyBase64)
if masterKeyBase64 == "" {
return nil, ErrSecretEncryptionKeyMissing
}
key, err := base64.StdEncoding.DecodeString(masterKeyBase64)
if err != nil {
if rawKey, rawErr := base64.RawStdEncoding.DecodeString(masterKeyBase64); rawErr == nil {
key = rawKey
} else {
return nil, fmt.Errorf("decode secret encryption key: %w", err)
}
}
if len(key) != 32 {
return nil, fmt.Errorf("secret encryption key must decode to 32 bytes")
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, fmt.Errorf("create secret cipher: %w", err)
}
aead, err := cipher.NewGCM(block)
if err != nil {
return nil, fmt.Errorf("create secret gcm: %w", err)
}
if strings.TrimSpace(keyID) == "" {
keyID = "local-v1"
}
return &Encryptor{aead: aead, keyID: keyID}, nil
}
func (e *Encryptor) KeyID() string {
if e == nil {
return ""
}
return e.keyID
}
func (e *Encryptor) Encrypt(plaintext, aad []byte) (EncryptedPayload, error) {
if e == nil {
return EncryptedPayload{}, ErrSecretEncryptionKeyMissing
}
nonce := make([]byte, e.aead.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return EncryptedPayload{}, fmt.Errorf("generate secret nonce: %w", err)
}
hash := sha256.Sum256(plaintext)
return EncryptedPayload{
Algorithm: AlgorithmAES256GCM,
KeyID: e.keyID,
Nonce: nonce,
Ciphertext: e.aead.Seal(nil, nonce, plaintext, aad),
PayloadSHA256: hex.EncodeToString(hash[:]),
}, nil
}
func (e *Encryptor) Decrypt(payload EncryptedPayload, aad []byte) ([]byte, error) {
if e == nil {
return nil, ErrSecretEncryptionKeyMissing
}
if payload.Algorithm != "" && payload.Algorithm != AlgorithmAES256GCM {
return nil, fmt.Errorf("unsupported secret algorithm %q", payload.Algorithm)
}
plaintext, err := e.aead.Open(nil, payload.Nonce, payload.Ciphertext, aad)
if err != nil {
return nil, fmt.Errorf("decrypt secret payload: %w", err)
}
return plaintext, nil
}
func ResourceSecretAAD(organizationID, resourceID, secretRef, protocol string) []byte {
return []byte(strings.Join([]string{
"rap-resource-secret-v1",
strings.TrimSpace(organizationID),
strings.TrimSpace(resourceID),
strings.TrimSpace(secretRef),
strings.ToLower(strings.TrimSpace(protocol)),
}, "|"))
}
@@ -0,0 +1,65 @@
package secrets
import (
"encoding/base64"
"encoding/json"
"testing"
)
func TestEncryptorRoundTrip(t *testing.T) {
key := base64.StdEncoding.EncodeToString([]byte("0123456789abcdef0123456789abcdef"))
encryptor, err := NewEncryptor(key, "test-key")
if err != nil {
t.Fatalf("NewEncryptor returned error: %v", err)
}
aad := ResourceSecretAAD("org-1", "resource-1", "rap-secret://test", "rdp")
encrypted, err := encryptor.Encrypt([]byte(`{"username":"user","password":"secret"}`), aad)
if err != nil {
t.Fatalf("Encrypt returned error: %v", err)
}
plaintext, err := encryptor.Decrypt(encrypted, aad)
if err != nil {
t.Fatalf("Decrypt returned error: %v", err)
}
if string(plaintext) != `{"username":"user","password":"secret"}` {
t.Fatalf("unexpected plaintext: %s", plaintext)
}
}
func TestEncryptorRejectsWrongAAD(t *testing.T) {
key := base64.StdEncoding.EncodeToString([]byte("0123456789abcdef0123456789abcdef"))
encryptor, err := NewEncryptor(key, "test-key")
if err != nil {
t.Fatalf("NewEncryptor returned error: %v", err)
}
encrypted, err := encryptor.Encrypt([]byte(`{"password":"secret"}`), ResourceSecretAAD("org-1", "resource-1", "ref", "rdp"))
if err != nil {
t.Fatalf("Encrypt returned error: %v", err)
}
if _, err := encryptor.Decrypt(encrypted, ResourceSecretAAD("org-2", "resource-1", "ref", "rdp")); err == nil {
t.Fatalf("expected decrypt with wrong aad to fail")
}
}
func TestMergeResourceSecretIntoAssignmentMetadata(t *testing.T) {
metadata := map[string]any{
"resource": map[string]any{
"id": "resource-1",
"metadata": map[string]any{
"rdp_host": "host",
},
},
}
merged, err := MergeResourceSecretIntoAssignmentMetadata(metadata, json.RawMessage(`{"username":"user","password":"secret","domain":"corp"}`))
if err != nil {
t.Fatalf("MergeResourceSecretIntoAssignmentMetadata returned error: %v", err)
}
resource := merged.Metadata["resource"].(map[string]any)
resourceMetadata := resource["metadata"].(map[string]any)
if resourceMetadata["rdp_host"] != "host" {
t.Fatalf("existing metadata was not preserved")
}
if resourceMetadata["username"] != "user" || resourceMetadata["password"] != "secret" || resourceMetadata["domain"] != "corp" {
t.Fatalf("secret payload was not merged: %#v", resourceMetadata)
}
}
@@ -0,0 +1,132 @@
package secrets
import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
)
var (
ErrPlaintextResourceCredentials = errors.New("plaintext resource credentials are not allowed in metadata in production")
ErrMissingResourceSecretRef = errors.New("secret_ref is required for this resource protocol in production")
)
var credentialKeyFragments = []string{
"accesstoken",
"clientsecret",
"credential",
"credentials",
"domain",
"password",
"privatekey",
"refreshtoken",
"secret",
"secrets",
"token",
"user",
"username",
}
var safeReferenceKeys = []string{
"certificateverificationmode",
"renderqualityprofile",
"secretref",
"secretreference",
"vaultref",
}
func ValidateResourceSecretReadiness(protocol string, secretRef *string, metadata json.RawMessage, appEnv string) error {
if !IsProductionEnv(appEnv) {
return nil
}
paths, err := PlaintextCredentialMetadataPaths(metadata)
if err != nil {
return err
}
if len(paths) > 0 {
return fmt.Errorf("%w: %s", ErrPlaintextResourceCredentials, strings.Join(paths, ", "))
}
if ResourceProtocolRequiresSecretRef(protocol) && (secretRef == nil || strings.TrimSpace(*secretRef) == "") {
return ErrMissingResourceSecretRef
}
return nil
}
func IsProductionEnv(appEnv string) bool {
switch strings.ToLower(strings.TrimSpace(appEnv)) {
case "prod", "production":
return true
default:
return false
}
}
func ResourceProtocolRequiresSecretRef(protocol string) bool {
switch strings.ToLower(strings.TrimSpace(protocol)) {
case "rdp", "vnc", "ssh":
return true
default:
return false
}
}
func PlaintextCredentialMetadataPaths(raw json.RawMessage) ([]string, error) {
if len(raw) == 0 {
return nil, nil
}
var value any
if err := json.Unmarshal(raw, &value); err != nil {
return nil, errors.New("metadata must be valid json")
}
metadata, ok := value.(map[string]any)
if !ok {
return nil, errors.New("metadata must be a json object")
}
var paths []string
collectCredentialPaths(metadata, "", &paths)
sort.Strings(paths)
return slices.Compact(paths), nil
}
func collectCredentialPaths(value any, prefix string, paths *[]string) {
switch typed := value.(type) {
case map[string]any:
for key, child := range typed {
path := key
if prefix != "" {
path = prefix + "." + key
}
if isCredentialMetadataKey(key) {
*paths = append(*paths, path)
}
collectCredentialPaths(child, path, paths)
}
case []any:
for index, child := range typed {
collectCredentialPaths(child, fmt.Sprintf("%s[%d]", prefix, index), paths)
}
}
}
func isCredentialMetadataKey(key string) bool {
normalized := normalizeMetadataKey(key)
if slices.Contains(safeReferenceKeys, normalized) {
return false
}
for _, fragment := range credentialKeyFragments {
if normalized == fragment || strings.HasSuffix(normalized, fragment) {
return true
}
}
return false
}
func normalizeMetadataKey(key string) string {
key = strings.ToLower(strings.TrimSpace(key))
replacer := strings.NewReplacer("_", "", "-", "", " ", "", ".", "")
return replacer.Replace(key)
}
@@ -0,0 +1,52 @@
package secrets
import (
"encoding/json"
"errors"
"slices"
"testing"
)
func TestValidateResourceSecretReadinessAllowsPlaintextInDevelopment(t *testing.T) {
metadata := json.RawMessage(`{"username":"m","password":"secret"}`)
if err := ValidateResourceSecretReadiness("rdp", nil, metadata, "development"); err != nil {
t.Fatalf("development metadata should remain allowed for smoke/dev: %v", err)
}
}
func TestValidateResourceSecretReadinessRejectsPlaintextCredentialsInProduction(t *testing.T) {
metadata := json.RawMessage(`{"rdp_host":"host","credentials":{"username":"m","password":"secret"}}`)
err := ValidateResourceSecretReadiness("rdp", stringPtr("vault://org/resource"), metadata, "production")
if !errors.Is(err, ErrPlaintextResourceCredentials) {
t.Fatalf("expected plaintext credential rejection, got %v", err)
}
paths, err := PlaintextCredentialMetadataPaths(metadata)
if err != nil {
t.Fatalf("metadata paths: %v", err)
}
for _, expected := range []string{"credentials", "credentials.password", "credentials.username"} {
if !slices.Contains(paths, expected) {
t.Fatalf("expected sensitive path %q in %v", expected, paths)
}
}
}
func TestValidateResourceSecretReadinessRequiresSecretRefForProductionRDP(t *testing.T) {
metadata := json.RawMessage(`{"rdp_host":"host","rdp_port":3389}`)
err := ValidateResourceSecretReadiness("rdp", nil, metadata, "production")
if !errors.Is(err, ErrMissingResourceSecretRef) {
t.Fatalf("expected missing secret_ref rejection, got %v", err)
}
}
func TestValidateResourceSecretReadinessAllowsProductionSecretRef(t *testing.T) {
metadata := json.RawMessage(`{"rdp_host":"host","rdp_port":3389,"secret_ref":"vault://org/resource"}`)
if err := ValidateResourceSecretReadiness("rdp", stringPtr("vault://org/resource"), metadata, "production"); err != nil {
t.Fatalf("production secret_ref metadata should be accepted: %v", err)
}
}
func stringPtr(value string) *string {
return &value
}
@@ -0,0 +1,259 @@
package secrets
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
postgresplatform "github.com/example/remote-access-platform/backend/internal/platform/postgres"
)
var (
ErrResourceSecretNotFound = errors.New("resource secret not found")
ErrSecretAccessDenied = errors.New("resource secret access denied")
ErrSecretLeaseRequired = errors.New("resource secret resolution requires lease proof")
)
type ResourceSecretStore struct {
db postgresplatform.DBTX
encryptor *Encryptor
now func() time.Time
}
type ResourceSecretResolver interface {
ResolveForSession(ctx context.Context, req ResolveResourceSecretRequest) (*ResolvedResourceSecret, error)
}
type ResourceSecretDescriptor struct {
ID string `json:"id"`
OrganizationID string `json:"organization_id"`
ResourceID string `json:"resource_id"`
SecretRef string `json:"secret_ref"`
Protocol string `json:"protocol"`
Version int `json:"version"`
KeyID string `json:"key_id"`
Algorithm string `json:"algorithm"`
Metadata json.RawMessage `json:"metadata"`
CreatedAt time.Time `json:"created_at"`
RotatedAt *time.Time `json:"rotated_at,omitempty"`
}
type UpsertResourceSecretCommand struct {
OrganizationID string
ResourceID string
Protocol string
SecretRef string
Payload json.RawMessage
Metadata json.RawMessage
ActorUserID string
}
type ResolveResourceSecretRequest struct {
SecretRef string
OrganizationID string
ResourceID string
SessionID string
WorkerID string
LeaseID string
}
type ResolvedResourceSecret struct {
Descriptor ResourceSecretDescriptor
Payload json.RawMessage
}
func NewResourceSecretStore(db postgresplatform.DBTX, encryptor *Encryptor) *ResourceSecretStore {
return &ResourceSecretStore{db: db, encryptor: encryptor, now: time.Now}
}
func (s *ResourceSecretStore) WithDB(db postgresplatform.DBTX) *ResourceSecretStore {
if s == nil {
return nil
}
return &ResourceSecretStore{db: db, encryptor: s.encryptor, now: s.now}
}
func DefaultResourceSecretRef(organizationID, resourceID string) string {
return "rap-secret://org/" + strings.TrimSpace(organizationID) + "/resources/" + strings.TrimSpace(resourceID) + "/primary"
}
func (s *ResourceSecretStore) Upsert(ctx context.Context, cmd UpsertResourceSecretCommand) (*ResourceSecretDescriptor, error) {
if s == nil || s.encryptor == nil {
return nil, ErrSecretEncryptionKeyMissing
}
payload, err := normalizeJSONObject(cmd.Payload)
if err != nil {
return nil, err
}
metadata, err := normalizeJSONObjectAllowEmpty(cmd.Metadata)
if err != nil {
return nil, err
}
secretRef := strings.TrimSpace(cmd.SecretRef)
if secretRef == "" {
secretRef = DefaultResourceSecretRef(cmd.OrganizationID, cmd.ResourceID)
}
protocol := strings.ToLower(strings.TrimSpace(cmd.Protocol))
encrypted, err := s.encryptor.Encrypt(payload, ResourceSecretAAD(cmd.OrganizationID, cmd.ResourceID, secretRef, protocol))
if err != nil {
return nil, err
}
now := s.now().UTC()
const query = `
INSERT INTO resource_secrets (
organization_id, resource_id, secret_ref, protocol, version, key_id,
algorithm, nonce, ciphertext, payload_sha256, metadata, created_by_user_id,
created_at, rotated_at
) VALUES (
$1::uuid, $2::uuid, $3, $4, 1, $5,
$6, $7, $8, $9, $10::jsonb, NULLIF($11, '')::uuid,
$12, NULL
)
ON CONFLICT (resource_id) DO UPDATE SET
secret_ref = EXCLUDED.secret_ref,
protocol = EXCLUDED.protocol,
version = resource_secrets.version + 1,
key_id = EXCLUDED.key_id,
algorithm = EXCLUDED.algorithm,
nonce = EXCLUDED.nonce,
ciphertext = EXCLUDED.ciphertext,
payload_sha256 = EXCLUDED.payload_sha256,
metadata = EXCLUDED.metadata,
created_by_user_id = EXCLUDED.created_by_user_id,
rotated_at = EXCLUDED.created_at
RETURNING id::text, organization_id::text, resource_id::text, secret_ref,
protocol, version, key_id, algorithm, metadata, created_at, rotated_at
`
var descriptor ResourceSecretDescriptor
if err := s.db.QueryRow(ctx, query,
cmd.OrganizationID,
cmd.ResourceID,
secretRef,
protocol,
encrypted.KeyID,
encrypted.Algorithm,
encrypted.Nonce,
encrypted.Ciphertext,
encrypted.PayloadSHA256,
metadata,
cmd.ActorUserID,
now,
).Scan(
&descriptor.ID,
&descriptor.OrganizationID,
&descriptor.ResourceID,
&descriptor.SecretRef,
&descriptor.Protocol,
&descriptor.Version,
&descriptor.KeyID,
&descriptor.Algorithm,
&descriptor.Metadata,
&descriptor.CreatedAt,
&descriptor.RotatedAt,
); err != nil {
return nil, fmt.Errorf("upsert resource secret: %w", err)
}
return &descriptor, nil
}
func (s *ResourceSecretStore) ResolveForSession(ctx context.Context, req ResolveResourceSecretRequest) (*ResolvedResourceSecret, error) {
if s == nil || s.encryptor == nil {
return nil, ErrSecretEncryptionKeyMissing
}
if strings.TrimSpace(req.LeaseID) == "" {
return nil, ErrSecretLeaseRequired
}
const query = `
SELECT sec.id::text, sec.organization_id::text, sec.resource_id::text, sec.secret_ref,
sec.protocol, sec.version, sec.key_id, sec.algorithm, sec.metadata,
sec.created_at, sec.rotated_at, sec.nonce, sec.ciphertext,
rs.organization_id::text, rs.resource_id::text, COALESCE(rs.worker_id, ''), rs.state
FROM resource_secrets sec
JOIN remote_sessions rs ON rs.resource_id = sec.resource_id
WHERE sec.secret_ref = $1 AND rs.id = $2::uuid
`
var descriptor ResourceSecretDescriptor
var nonce, ciphertext []byte
var sessionOrganizationID, sessionResourceID, sessionWorkerID, sessionState string
if err := s.db.QueryRow(ctx, query, req.SecretRef, req.SessionID).Scan(
&descriptor.ID,
&descriptor.OrganizationID,
&descriptor.ResourceID,
&descriptor.SecretRef,
&descriptor.Protocol,
&descriptor.Version,
&descriptor.KeyID,
&descriptor.Algorithm,
&descriptor.Metadata,
&descriptor.CreatedAt,
&descriptor.RotatedAt,
&nonce,
&ciphertext,
&sessionOrganizationID,
&sessionResourceID,
&sessionWorkerID,
&sessionState,
); err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrResourceSecretNotFound
}
return nil, fmt.Errorf("resolve resource secret: %w", err)
}
if descriptor.OrganizationID != req.OrganizationID ||
descriptor.ResourceID != req.ResourceID ||
sessionOrganizationID != req.OrganizationID ||
sessionResourceID != req.ResourceID ||
sessionWorkerID != req.WorkerID ||
!secretResolvableSessionState(sessionState) {
return nil, ErrSecretAccessDenied
}
plaintext, err := s.encryptor.Decrypt(EncryptedPayload{
Algorithm: descriptor.Algorithm,
KeyID: descriptor.KeyID,
Nonce: nonce,
Ciphertext: ciphertext,
}, ResourceSecretAAD(descriptor.OrganizationID, descriptor.ResourceID, descriptor.SecretRef, descriptor.Protocol))
if err != nil {
return nil, err
}
return &ResolvedResourceSecret{
Descriptor: descriptor,
Payload: json.RawMessage(plaintext),
}, nil
}
func normalizeJSONObject(raw json.RawMessage) (json.RawMessage, error) {
if len(raw) == 0 || !json.Valid(raw) {
return nil, ErrSecretPayloadInvalid
}
var decoded map[string]any
if err := json.Unmarshal(raw, &decoded); err != nil {
return nil, ErrSecretPayloadInvalid
}
encoded, err := json.Marshal(decoded)
if err != nil {
return nil, err
}
return json.RawMessage(encoded), nil
}
func normalizeJSONObjectAllowEmpty(raw json.RawMessage) (json.RawMessage, error) {
if len(raw) == 0 {
return json.RawMessage(`{}`), nil
}
return normalizeJSONObject(raw)
}
func secretResolvableSessionState(state string) bool {
switch state {
case "starting", "active", "reconnecting":
return true
default:
return false
}
}