feat: add single-admin application onboarding and token rotation
This commit is contained in:
121
internal/transport/httpserver/console_applications_test.go
Normal file
121
internal/transport/httpserver/console_applications_test.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/orbit/internal/consoleauth"
|
||||
core "git.yuntuo.hk/k2safe/orbit/model"
|
||||
"git.yuntuo.hk/k2safe/orbit/platform/auth"
|
||||
)
|
||||
|
||||
type fakeApplications struct{ calls int }
|
||||
|
||||
func (f *fakeApplications) ListApplications(context.Context, auth.Token, core.ID) (consoleauth.ApplicationPage, error) {
|
||||
f.calls++
|
||||
return consoleauth.ApplicationPage{}, nil
|
||||
}
|
||||
func (f *fakeApplications) GetApplication(context.Context, auth.Token, core.ID) (consoleauth.ApplicationDetail, error) {
|
||||
f.calls++
|
||||
return consoleauth.ApplicationDetail{}, nil
|
||||
}
|
||||
func (f *fakeApplications) PrepareApplication(context.Context, auth.Token, consoleauth.ApplicationInput) (consoleauth.ApplicationOperation, error) {
|
||||
f.calls++
|
||||
return consoleauth.ApplicationOperation{}, nil
|
||||
}
|
||||
func (f *fakeApplications) ApplyApplication(context.Context, auth.Token, consoleauth.ApplicationOperation) (consoleauth.ApplicationReceipt, error) {
|
||||
f.calls++
|
||||
return consoleauth.ApplicationReceipt{}, nil
|
||||
}
|
||||
func TestConsoleApplicationSessionAndCSRF(t *testing.T) {
|
||||
ns, _ := core.NewID()
|
||||
id, _ := core.NewID()
|
||||
raw, _, _ := auth.Issue(ns, id)
|
||||
cases := []struct {
|
||||
name, method, path, origin, header string
|
||||
cookie bool
|
||||
body string
|
||||
status int
|
||||
}{
|
||||
{name: "no login", method: "GET", path: "/console/applications", status: 401},
|
||||
{name: "SDK bearer only", method: "POST", path: "/console/applications/prepare", header: "1", body: `{}`, status: 401},
|
||||
{name: "missing csrf header", method: "POST", path: "/console/applications/prepare", cookie: true, body: `{}`, status: 403},
|
||||
{name: "cross origin", method: "POST", path: "/console/applications/apply", cookie: true, header: "1", origin: "https://evil.example", body: `{}`, status: 403},
|
||||
{name: "null origin", method: "POST", path: "/console/applications/apply", cookie: true, header: "1", origin: "null", body: `{}`, status: 403},
|
||||
{name: "same origin", method: "POST", path: "/console/applications/prepare", cookie: true, header: "1", origin: "http://example.com", body: `{}`, status: 200},
|
||||
{name: "unknown field", method: "POST", path: "/console/applications/prepare", cookie: true, header: "1", body: `{"admin":true}`, status: 400},
|
||||
{name: "read", method: "GET", path: "/console/applications", cookie: true, status: 200},
|
||||
}
|
||||
for _, tt := range cases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
authStore := &fakeConsoleAuth{valid: true}
|
||||
store := &fakeApplications{}
|
||||
server := NewWithConsoleAuth("", nil, authStore)
|
||||
RegisterConsoleApplications(server, authStore, store)
|
||||
req := httptest.NewRequest(tt.method, "http://example.com"+tt.path, strings.NewReader(tt.body))
|
||||
req.Header.Set("Authorization", "Bearer "+raw)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Orbit-Console", tt.header)
|
||||
if tt.origin != "" {
|
||||
req.Header.Set("Origin", tt.origin)
|
||||
}
|
||||
if tt.cookie {
|
||||
req.AddCookie(&http.Cookie{Name: consoleSessionCookie, Value: raw})
|
||||
}
|
||||
res := httptest.NewRecorder()
|
||||
server.Handler.ServeHTTP(res, req)
|
||||
if res.Code != tt.status {
|
||||
t.Fatalf("status=%d wanted=%d", res.Code, tt.status)
|
||||
}
|
||||
if tt.status != 200 && store.calls != 0 {
|
||||
t.Fatal("rejected request reached store")
|
||||
}
|
||||
if res.Header().Get("Cache-Control") != "no-store" {
|
||||
t.Fatal("management response cacheable")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 准备接口生成的内容必须可被严格解码器原样提交,覆盖可选 ID 和空能力组。
|
||||
func TestConsolePreparedApplicationRoundTrip(t *testing.T) {
|
||||
ns, _ := core.NewID()
|
||||
cred, _ := core.NewID()
|
||||
app, _ := core.NewID()
|
||||
queue, _ := core.NewID()
|
||||
opID, _ := core.NewID()
|
||||
token, _, _ := auth.Issue(ns, cred)
|
||||
for _, kind := range []string{"CREATE", "ROTATE"} {
|
||||
op := consoleauth.ApplicationOperation{Input: consoleauth.ApplicationInput{Kind: kind, ValidDays: 30, Reason: "test"}, OperationID: opID, ApplicationID: app, NamespaceID: ns, CredentialID: cred, Token: token, ExpiresAt: time.Now().UTC()}
|
||||
if kind == "CREATE" {
|
||||
op.QueueID = queue
|
||||
op.Input.Application = "sample"
|
||||
op.Input.Namespace = "default"
|
||||
op.Input.Environment = "test"
|
||||
op.Input.Queue = "work"
|
||||
} else {
|
||||
op.Input.NamespaceID = ns
|
||||
op.Input.CredentialID = cred
|
||||
op.Input.ExpectedRevision = 1
|
||||
}
|
||||
raw, err := json.Marshal(op)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
req := httptest.NewRequest("POST", "/console/applications/apply", bytes.NewReader(raw))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
var got consoleauth.ApplicationOperation
|
||||
if err := decodeBody(req, &got); err != nil {
|
||||
t.Fatalf("%s prepared output rejected: %v", kind, err)
|
||||
}
|
||||
if got.Token != token || got.Input.Kind != kind {
|
||||
t.Fatal("prepared operation changed")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user