package mysql import ( "bytes" "encoding/base64" "encoding/json" "strings" "sync" "testing" "time" "git.yuntuo.hk/k2safe/orbit/internal/consoleauth" "git.yuntuo.hk/k2safe/orbit/internal/credentialvault" "git.yuntuo.hk/k2safe/orbit/internal/fault" "git.yuntuo.hk/k2safe/orbit/platform/auth" ) func TestIntegrationCredentialVault(t *testing.T) { ctx, db := overlapDB(t) ring, err := credentialvault.Read(strings.NewReader(`{"active":"test","keys":{"test":"` + base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{42}, 32)) + `"}}`)) if err != nil { t.Fatal(err) } name := "vault-" + string(testID(t)) var initial BootstrapResult if err := Bootstrap(ctx, db, BootstrapInput{Application: name, Namespace: "default", Environment: "test", Queue: "work", ConsoleUsername: name, ConsolePassword: "vault-test-password-12345"}, func(v BootstrapResult) error { initial = v; return nil }); err != nil { t.Fatal(err) } login, err := NewConsoleAuthStore(db).Login(ctx, name, "vault-test-password-12345", "test", "127.0.0.1") if err != nil { t.Fatal(err) } admin, _ := auth.Parse(login.Token) sdk, _ := auth.Parse(initial.Token) store := NewConsoleApplicationStore(db, name, ring) in := consoleauth.CredentialTokenInput{ExpectedRevision: 1} access := func(input consoleauth.CredentialTokenInput, importCurrent bool) (consoleauth.CredentialToken, error) { return store.AccessCredentialToken(ctx, admin, initial.NamespaceID, initial.CredentialID, input, importCurrent) } for _, unauthorized := range []auth.Token{sdk, {}} { if _, err := store.AccessCredentialToken(ctx, unauthorized, initial.NamespaceID, initial.CredentialID, in, false); fault.Code(err) != "PERMISSION_DENIED" { t.Fatal("SDK or missing session accepted") } } if _, err := NewConsoleApplicationStore(db, "other", ring).AccessCredentialToken(ctx, admin, initial.NamespaceID, initial.CredentialID, in, false); fault.Code(err) != "PERMISSION_DENIED" { t.Fatal("wrong administrator accepted") } if _, err := access(in, false); fault.Code(err) != "STATE_CONFLICT" { t.Fatal("legacy token unexpectedly revealed") } in.Token = initial.Token // 审计失败时导入事务回滚,查看也不能把秘密交付给调用方。 trigger := "vault_audit_" + string(testID(t)) if _, err := db.ExecContext(ctx, "CREATE TRIGGER "+trigger+" BEFORE INSERT ON orbit_audit_events FOR EACH ROW BEGIN IF NEW.namespace_id=UNHEX('"+string(initial.NamespaceID)+"') AND NEW.action LIKE 'console.credential.token.%' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='vault audit failure test'; END IF; END"); err != nil { t.Fatal(err) } defer db.ExecContext(ctx, "DROP TRIGGER IF EXISTS "+trigger) if got, err := access(in, true); err == nil || got.Token != "" { t.Fatal("audit failure returned secret") } var imported bool if err := db.QueryRowContext(ctx, `SELECT secret_ciphertext IS NOT NULL FROM orbit_credentials WHERE namespace_id=? AND id=?`, initial.NamespaceID, initial.CredentialID).Scan(&imported); err != nil || imported { t.Fatal("failed import did not roll back") } if _, err := db.ExecContext(ctx, "DROP TRIGGER "+trigger); err != nil { t.Fatal(err) } if _, err := access(in, true); err != nil { t.Fatal(err) } if _, err := access(in, true); err != nil { t.Fatal("repeat import failed", err) } in.Token = "" if _, err := db.ExecContext(ctx, "CREATE TRIGGER "+trigger+" BEFORE INSERT ON orbit_audit_events FOR EACH ROW BEGIN IF NEW.namespace_id=UNHEX('"+string(initial.NamespaceID)+"') AND NEW.action='console.credential.token.view' THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT='vault audit failure test'; END IF; END"); err != nil { t.Fatal(err) } if got, err := access(in, false); err == nil || got.Token != "" { t.Fatal("read without audit returned secret") } if _, err := db.ExecContext(ctx, "DROP TRIGGER "+trigger); err != nil { t.Fatal(err) } if got, err := access(in, false); err != nil || got.Token != initial.Token || got.Revision != 1 { t.Fatal("import changed identity or cannot reveal") } var cipher []byte if err := db.QueryRowContext(ctx, `SELECT secret_ciphertext FROM orbit_credentials WHERE namespace_id=? AND id=?`, initial.NamespaceID, initial.CredentialID).Scan(&cipher); err != nil || bytes.Contains(cipher, []byte(initial.Token)) { t.Fatal("plaintext persisted") } detail, err := store.GetApplication(ctx, admin, initial.NamespaceID) raw, _ := json.Marshal(detail) if err != nil || !detail.Credentials[0].TokenStored || bytes.Contains(raw, []byte(initial.Token)) { t.Fatal("metadata leaked or failed") } // 已启用密文保管的 CREATE、ROTATE、过渡期读取以及旧操作重放。 create, err := store.PrepareApplication(ctx, admin, consoleauth.ApplicationInput{Kind: "CREATE", Application: "vault-new-" + string(testID(t)), Namespace: "default", Environment: "test", Queue: "work", Reason: "vault integration"}) if err != nil { t.Fatal(err) } if _, err := store.ApplyApplication(ctx, admin, create); err != nil { t.Fatal(err) } if got, err := store.AccessCredentialToken(ctx, admin, create.NamespaceID, create.CredentialID, consoleauth.CredentialTokenInput{ExpectedRevision: 1}, false); err != nil || got.Token != create.Token { t.Fatal("new app not encrypted") } rotate, err := PrepareCredentialOperation(ctx, db, CredentialChangeInput{NamespaceID: initial.NamespaceID, CredentialID: initial.CredentialID, ExpectedRevision: 1, Action: "ROTATE", GraceFor: time.Hour, Reason: "vault rotate"}) if err != nil { t.Fatal(err) } var wg sync.WaitGroup wg.Add(2) var readErr, rotateErr error var read consoleauth.CredentialToken go func() { defer wg.Done(); read, readErr = access(in, false) }() go func() { defer wg.Done(); _, rotateErr = ApplyCredentialOperation(ctx, db, rotate, ring) }() wg.Wait() if rotateErr != nil || (readErr != nil && fault.Code(readErr) != "STATE_CONFLICT") || (readErr == nil && read.Token != initial.Token) { t.Fatal("concurrent read/rotation inconsistent", rotateErr, readErr) } in.ExpectedRevision = 2 if got, err := access(in, false); err != nil || got.Token != rotate.Token { t.Fatal("current token unavailable during grace") } in.Token = initial.Token if _, err := access(in, true); fault.Code(err) != "INVALID_ARGUMENT" { t.Fatal("old generation imported") } in.Token = "" status, err := GetCredentialStatus(ctx, db, initial.NamespaceID, initial.CredentialID) if err != nil || status.PreviousValidUntil == nil { t.Fatal("grace lost") } for _, action := range []string{"SET_EXPIRY", "FINALIZE"} { op, err := PrepareCredentialOperation(ctx, db, CredentialChangeInput{NamespaceID: initial.NamespaceID, CredentialID: initial.CredentialID, ExpectedRevision: in.ExpectedRevision, Action: action, Reason: "preserve vault"}) if err != nil { t.Fatal(err) } if _, err = ApplyCredentialOperation(ctx, db, op, ring); err != nil { t.Fatal(err) } in.ExpectedRevision++ if got, err := access(in, false); err != nil || got.Token != rotate.Token { t.Fatal("non-secret change invalidated encryption") } } if _, err := ApplyCredentialOperation(ctx, db, rotate, ring); err != nil { t.Fatal(err) } if got, err := access(in, false); err != nil || got.Token != rotate.Token { t.Fatal("replay changed ciphertext") } // 撤销会清除密文;历史 CLI 不配置密钥时轮换也不能遗留过期密文。 op, err := PrepareCredentialOperation(ctx, db, CredentialChangeInput{NamespaceID: initial.NamespaceID, CredentialID: initial.CredentialID, ExpectedRevision: in.ExpectedRevision, Action: "ROTATE", Reason: "legacy CLI"}) if err != nil { t.Fatal(err) } if _, err = ApplyCredentialOperation(ctx, db, op); err != nil { t.Fatal(err) } in.ExpectedRevision++ if _, err := access(in, false); fault.Code(err) != "STATE_CONFLICT" { t.Fatal("stale ciphertext remained after CLI rotation") } in.Token = op.Token if _, err := access(in, true); err != nil { t.Fatal(err) } in.Token = "" op, err = PrepareCredentialOperation(ctx, db, CredentialChangeInput{NamespaceID: initial.NamespaceID, CredentialID: initial.CredentialID, ExpectedRevision: in.ExpectedRevision, Action: "REVOKE", Reason: "revoke vault"}) if err != nil { t.Fatal(err) } if _, err = ApplyCredentialOperation(ctx, db, op, ring); err != nil { t.Fatal(err) } in.ExpectedRevision++ if _, err := access(in, false); err == nil { t.Fatal("revoked credential revealed") } var stored bool if err := db.QueryRowContext(ctx, `SELECT secret_ciphertext IS NOT NULL FROM orbit_credentials WHERE namespace_id=? AND id=?`, initial.NamespaceID, initial.CredentialID).Scan(&stored); err != nil || stored { t.Fatal("revoked ciphertext remains") } // 审计只包含版本与操作身份,不包含 Token 或密文。 rows, err := db.QueryContext(ctx, "SELECT CONCAT_WS(' ',action,`before`,`after`) FROM orbit_audit_events WHERE namespace_id=?", initial.NamespaceID) if err != nil { t.Fatal(err) } defer rows.Close() views := 0 for rows.Next() { var text string if err := rows.Scan(&text); err != nil { t.Fatal(err) } if strings.Contains(text, initial.Token) || strings.Contains(text, rotate.Token) { t.Fatal("secret leaked to audit") } if strings.Contains(text, "console.credential.token.view") { views++ } } if rows.Err() != nil || views < 3 { t.Fatal("view audit missing", rows.Err()) } }