166 lines
6.3 KiB
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)
|
|
}
|
|
}
|