Files
orbit/internal/storage/mysql/console_applications_integration_test.go

166 lines
6.3 KiB
Go

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)
}
}