feat: add single-admin application onboarding and token rotation
This commit is contained in:
165
internal/storage/mysql/console_applications_integration_test.go
Normal file
165
internal/storage/mysql/console_applications_integration_test.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package mysql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/consoleauth"
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/fault"
|
||||
"git.yuntuo.hk/k2safe/orbit/platform/auth"
|
||||
)
|
||||
|
||||
func TestIntegrationConsoleApplications(t *testing.T) {
|
||||
dsn := os.Getenv("ORBIT_TEST_MYSQL_DSN")
|
||||
if dsn == "" {
|
||||
t.Skip("requires disposable MySQL")
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
db, err := Open(ctx, dsn, 10, 3)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
if err := Migrate(ctx, db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
username := "access-" + string(testID(t))
|
||||
password := "local-test-password-12345"
|
||||
var initial BootstrapResult
|
||||
if err := Bootstrap(ctx, db, BootstrapInput{Application: username, Namespace: "default", Environment: "test", Queue: "initial", ConsoleUsername: username, ConsolePassword: password}, func(v BootstrapResult) error { initial = v; return nil }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
login, err := NewConsoleAuthStore(db).Login(ctx, username, password, "test", "127.0.0.1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
admin, _ := auth.Parse(login.Token)
|
||||
sdk, _ := auth.Parse(initial.Token)
|
||||
store := NewConsoleApplicationStore(db, username)
|
||||
if _, err := store.ListApplications(ctx, sdk, ""); fault.Code(err) != "PERMISSION_DENIED" {
|
||||
t.Fatalf("SDK reached admin: %v", err)
|
||||
}
|
||||
if _, err := NewConsoleApplicationStore(db, "").ListApplications(ctx, admin, ""); fault.Code(err) != "PERMISSION_DENIED" {
|
||||
t.Fatal("disabled admin accepted")
|
||||
}
|
||||
if _, err := NewConsoleApplicationStore(db, "different-admin").ListApplications(ctx, admin, ""); fault.Code(err) != "PERMISSION_DENIED" {
|
||||
t.Fatal("other user accepted")
|
||||
}
|
||||
in := consoleauth.ApplicationInput{Kind: "CREATE", Application: "created-" + string(testID(t)), Namespace: "default", Environment: "test", Queue: "work", Capabilities: []string{"worker", "events", "schedules", "workflows"}, ValidDays: 30, Reason: "integration onboarding"}
|
||||
op, err := store.PrepareApplication(ctx, admin, in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 准备只生成秘密,尚未允许业务访问。
|
||||
candidate, _ := auth.Parse(op.Token)
|
||||
if err := NewTaskStore(db).AuthorizeSubmit(ctx, candidate); err == nil {
|
||||
t.Fatal("prepared token accepted before commit")
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
receipts := make([]consoleauth.ApplicationReceipt, 2)
|
||||
errs := make([]error, 2)
|
||||
for i := range receipts {
|
||||
wg.Add(1)
|
||||
go func(i int) { defer wg.Done(); receipts[i], errs[i] = store.ApplyApplication(ctx, admin, op) }(i)
|
||||
}
|
||||
wg.Wait()
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if receipts[0].Deduplicated == receipts[1].Deduplicated {
|
||||
t.Fatal("concurrent original operation did not deduplicate")
|
||||
}
|
||||
if err := NewTaskStore(db).AuthorizeSubmit(ctx, candidate); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// 同一操作身份不可改变请求,重名不能留下半套资源。
|
||||
changed := op
|
||||
changed.Input.Reason = "modified"
|
||||
if _, err := store.ApplyApplication(ctx, admin, changed); fault.Code(err) != "IDEMPOTENCY_CONFLICT" {
|
||||
t.Fatalf("changed operation accepted: %v", err)
|
||||
}
|
||||
duplicate, err := store.PrepareApplication(ctx, admin, in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := store.ApplyApplication(ctx, admin, duplicate); fault.Code(err) != "STATE_CONFLICT" {
|
||||
t.Fatalf("duplicate name accepted: %v", err)
|
||||
}
|
||||
var n int
|
||||
if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM orbit_namespaces WHERE id=?`, duplicate.NamespaceID).Scan(&n); err != nil || n != 0 {
|
||||
t.Fatal("duplicate left namespace")
|
||||
}
|
||||
detail, err := store.GetApplication(ctx, admin, op.NamespaceID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if detail.Entry.ApplicationID != op.ApplicationID || len(detail.Queues) != 1 || len(detail.Credentials) != 1 || !detail.Credentials[0].Permissions.Allows("events:consume") {
|
||||
t.Fatal("incomplete onboarding")
|
||||
}
|
||||
raw, _ := json.Marshal(detail)
|
||||
if strings.Contains(string(raw), op.Token) || strings.Contains(string(raw), "secret_digest") {
|
||||
t.Fatal("detail leaked credential")
|
||||
}
|
||||
rotation := consoleauth.ApplicationInput{Kind: "ROTATE", NamespaceID: op.NamespaceID, CredentialID: op.CredentialID, ExpectedRevision: 1, ValidDays: 90, Reason: "scheduled rotation"}
|
||||
rotated, err := store.PrepareApplication(ctx, admin, rotation)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
receipt, err := store.ApplyApplication(ctx, admin, rotated)
|
||||
if err != nil || receipt.Revision != 2 {
|
||||
t.Fatalf("rotate: %v", err)
|
||||
}
|
||||
newToken, _ := auth.Parse(rotated.Token)
|
||||
if NewTaskStore(db).AuthorizeSubmit(ctx, candidate) == nil || NewTaskStore(db).AuthorizeSubmit(ctx, newToken) != nil {
|
||||
t.Fatal("rotation auth boundary failed")
|
||||
}
|
||||
replay, err := store.ApplyApplication(ctx, admin, rotated)
|
||||
if err != nil || !replay.Deduplicated {
|
||||
t.Fatalf("rotation replay: %v", err)
|
||||
}
|
||||
if _, err := store.PrepareApplication(ctx, admin, rotation); fault.Code(err) != "STATE_CONFLICT" {
|
||||
t.Fatalf("stale revision accepted: %v", err)
|
||||
}
|
||||
detail, err = store.GetApplication(ctx, admin, op.NamespaceID)
|
||||
if err != nil || len(detail.Credentials) != 1 || detail.Credentials[0].ID != op.CredentialID || detail.Queues[0].ID != op.QueueID {
|
||||
t.Fatal("rotation changed identity")
|
||||
}
|
||||
rows, err := db.QueryContext(ctx, `SELECT actor_id,CAST(`+"`after`"+` AS CHAR) FROM orbit_audit_events WHERE namespace_id=? AND action IN ('console.application.create','credential.rotate')`, op.NamespaceID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
count := 0
|
||||
for rows.Next() {
|
||||
var actor, body string
|
||||
if err := rows.Scan(&actor, &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if actor != "console:"+login.User.ID || strings.Contains(body, op.Token) || strings.Contains(body, rotated.Token) {
|
||||
t.Fatal("audit principal or secrecy")
|
||||
}
|
||||
count++
|
||||
}
|
||||
err = rows.Err()
|
||||
rows.Close()
|
||||
if err != nil || count != 2 {
|
||||
t.Fatalf("audit count=%d err=%v", count, err)
|
||||
}
|
||||
// 登出在数据库生效后,原会话不能再次应用操作,即使持有原回执内容。
|
||||
if err := NewConsoleAuthStore(db).RevokeSession(ctx, admin); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := store.ApplyApplication(ctx, admin, rotated); fault.Code(err) != "PERMISSION_DENIED" {
|
||||
t.Fatalf("logged out replay accepted: %v", err)
|
||||
}
|
||||
if err := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM information_schema.referential_constraints WHERE constraint_schema=DATABASE()`).Scan(&n); err != nil || n != 0 {
|
||||
t.Fatalf("foreign keys=%d err=%v", n, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user