feat(auth): use Wayne native role APIs
This commit is contained in:
@@ -2,235 +2,209 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/config"
|
||||
"github.com/1024XEngineer/xinfra/server/internal/wayne"
|
||||
)
|
||||
|
||||
func TestWayneRoleBindingServiceBindNamespaceSignsAndOverridesOperator(t *testing.T) {
|
||||
var requestPath string
|
||||
var payload WayneRoleBindingRequest
|
||||
func TestWayneRoleBindingServiceBindNamespaceUsesNativeAPI(t *testing.T) {
|
||||
var got []string
|
||||
var updateBody map[string]any
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestPath = r.URL.RequestURI()
|
||||
body := readTestBody(t, r)
|
||||
if !wayne.Verify("service-secret", r.Header.Get(wayne.HeaderSignature), r.Method, r.URL.RequestURI(), r.Header.Get(wayne.HeaderTimestamp), r.Header.Get(wayne.HeaderNonce), body) {
|
||||
t.Fatalf("invalid signature headers: %#v body=%s", r.Header, string(body))
|
||||
got = append(got, r.Method+" "+r.URL.RequestURI())
|
||||
switch r.URL.RequestURI() {
|
||||
case "/login/db":
|
||||
assertLoginBody(t, r)
|
||||
_, _ = w.Write([]byte(`{"data":{"token":"` + testJWT(time.Now().Add(time.Hour)) + `"}}`))
|
||||
case "/api/v1/users?name=target%40example.com":
|
||||
assertBearer(t, r)
|
||||
_, _ = w.Write([]byte(`{"data":{"list":[{"id":7,"name":"target@example.com","email":"target@example.com"}]}}`))
|
||||
case "/api/v1/namespaces/1/users?userId=7":
|
||||
assertBearer(t, r)
|
||||
_, _ = w.Write([]byte(`{"data":{"list":[{"id":99}]}}`))
|
||||
case "/api/v1/namespaces/1/users/99":
|
||||
assertBearer(t, r)
|
||||
body := readTestBody(t, r)
|
||||
if err := json.Unmarshal(body, &updateBody); err != nil {
|
||||
t.Fatalf("invalid update body: %v", err)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"data":{"changed":true}}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.RequestURI())
|
||||
}
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
t.Fatalf("invalid request body: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"data":{"changed":true}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
svc := NewWayneRoleBindingService(config.Config{
|
||||
WayneInternalAPIBaseURL: server.URL,
|
||||
WayneServiceName: "xinfra",
|
||||
WayneServiceAPISecretKey: "service-secret",
|
||||
})
|
||||
svc.now = func() time.Time { return time.Unix(1721000000, 0) }
|
||||
|
||||
operatorUserID := uint64(123)
|
||||
replace := false
|
||||
result, err := svc.BindNamespace(context.Background(), 1, "target@example.com", "eastsales@qiniu.com", WayneRoleBindingRequest{
|
||||
Username: "target@example.com",
|
||||
GroupIDs: []uint64{10, 11},
|
||||
OperatorUserID: &operatorUserID,
|
||||
OperatorName: "attacker@example.com",
|
||||
Replace: &replace,
|
||||
RequestID: "req-001",
|
||||
Reason: "grant",
|
||||
})
|
||||
svc := NewWayneRoleBindingService(testWayneConfig(server.URL), nil)
|
||||
result, err := svc.BindNamespace(context.Background(), 1, "target@example.com", "operator@example.com", WayneRoleBindingRequest{GroupIDs: []uint64{10, 11}})
|
||||
if err != nil {
|
||||
t.Fatalf("BindNamespace() error = %v", err)
|
||||
}
|
||||
if result.StatusCode != http.StatusOK {
|
||||
t.Fatalf("StatusCode = %d, want 200", result.StatusCode)
|
||||
}
|
||||
if requestPath != "/api/v1/internal/namespaces/1/users/target@example.com/roles" {
|
||||
t.Fatalf("requestPath = %q", requestPath)
|
||||
want := []string{
|
||||
"POST /login/db",
|
||||
"GET /api/v1/users?name=target%40example.com",
|
||||
"GET /api/v1/namespaces/1/users?userId=7",
|
||||
"PUT /api/v1/namespaces/1/users/99",
|
||||
}
|
||||
if payload.OperatorName != "eastsales@qiniu.com" {
|
||||
t.Fatalf("OperatorName = %q, want token email", payload.OperatorName)
|
||||
if strings.Join(got, "\n") != strings.Join(want, "\n") {
|
||||
t.Fatalf("requests:\n%s\nwant:\n%s", strings.Join(got, "\n"), strings.Join(want, "\n"))
|
||||
}
|
||||
if payload.OperatorUserID != nil {
|
||||
t.Fatalf("OperatorUserID should be omitted, got %v", *payload.OperatorUserID)
|
||||
if updateBody["id"].(float64) != 99 {
|
||||
t.Fatalf("update id = %v, want 99", updateBody["id"])
|
||||
}
|
||||
if payload.Username != "" {
|
||||
t.Fatalf("Username should be omitted from Wayne body, got %q", payload.Username)
|
||||
if updateBody["namespace"].(map[string]any)["id"].(float64) != 1 {
|
||||
t.Fatalf("namespace body = %#v", updateBody["namespace"])
|
||||
}
|
||||
if payload.Replace == nil || *payload.Replace {
|
||||
t.Fatalf("Replace = %v, want false", payload.Replace)
|
||||
if updateBody["user"].(map[string]any)["id"].(float64) != 7 {
|
||||
t.Fatalf("user body = %#v", updateBody["user"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestWayneRoleBindingServiceCallsAllDocumentedEndpoints(t *testing.T) {
|
||||
func TestParseWayneUsersSupportsNativePageList(t *testing.T) {
|
||||
users, err := parseWayneUsers([]byte(`{"data":{"pageNo":1,"pageSize":10,"totalCount":1,"list":[{"id":7,"name":"target@example.com","email":"target@example.com"}]}}`))
|
||||
if err != nil {
|
||||
t.Fatalf("parseWayneUsers() error = %v", err)
|
||||
}
|
||||
if len(users) != 1 || users[0].ID != 7 || users[0].Name != "target@example.com" {
|
||||
t.Fatalf("users = %#v", users)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWayneRoleBindingServiceCreateAndDeleteNativeBindings(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
call func(*WayneRoleBindingService) (*WayneRoleBindingResult, error)
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "unbind namespace",
|
||||
name: "create app binding",
|
||||
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
|
||||
return s.UnbindNamespace(context.Background(), 1, "target@example.com", "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{10}})
|
||||
return s.BindApp(context.Background(), 3, "target@example.com", "operator@example.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}})
|
||||
},
|
||||
want: "DELETE /api/v1/internal/namespaces/1/users/target@example.com/roles",
|
||||
want: "POST /api/v1/apps/3/users",
|
||||
},
|
||||
{
|
||||
name: "bind app",
|
||||
name: "delete namespace binding",
|
||||
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
|
||||
return s.BindApp(context.Background(), 3, "target@example.com", "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}})
|
||||
return s.UnbindNamespace(context.Background(), 1, "target@example.com", "operator@example.com", WayneRoleBindingRequest{})
|
||||
},
|
||||
want: "PUT /api/v1/internal/apps/3/users/target@example.com/roles",
|
||||
},
|
||||
{
|
||||
name: "unbind app",
|
||||
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
|
||||
return s.UnbindApp(context.Background(), 3, "target@example.com", "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}})
|
||||
},
|
||||
want: "DELETE /api/v1/internal/apps/3/users/target@example.com/roles",
|
||||
},
|
||||
{
|
||||
name: "list namespaces",
|
||||
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
|
||||
return s.ListNamespaces(context.Background())
|
||||
},
|
||||
want: "GET /api/v1/internal/namespaces",
|
||||
},
|
||||
{
|
||||
name: "list namespace groups",
|
||||
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
|
||||
groupType := 1
|
||||
return s.ListGroups(context.Background(), &groupType)
|
||||
},
|
||||
want: "GET /api/v1/internal/groups?type=1",
|
||||
},
|
||||
{
|
||||
name: "list all groups",
|
||||
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
|
||||
return s.ListGroups(context.Background(), nil)
|
||||
},
|
||||
want: "GET /api/v1/internal/groups",
|
||||
},
|
||||
{
|
||||
name: "get user roles",
|
||||
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
|
||||
return s.GetUserRoles(context.Background(), "target@example.com")
|
||||
},
|
||||
want: "GET /api/v1/internal/users/target@example.com/roles",
|
||||
},
|
||||
{
|
||||
name: "namespace operator permissions",
|
||||
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
|
||||
return s.NamespaceOperatorPermissions(context.Background(), 1, "eastsales@qiniu.com")
|
||||
},
|
||||
want: "GET /api/v1/internal/namespaces/1/operator-permissions?operatorName=eastsales%40qiniu.com",
|
||||
},
|
||||
{
|
||||
name: "app operator permissions",
|
||||
call: func(s *WayneRoleBindingService) (*WayneRoleBindingResult, error) {
|
||||
return s.AppOperatorPermissions(context.Background(), 3, "eastsales@qiniu.com")
|
||||
},
|
||||
want: "GET /api/v1/internal/apps/3/operator-permissions?operatorName=eastsales%40qiniu.com",
|
||||
want: "DELETE /api/v1/namespaces/1/users/44",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var got string
|
||||
var finalRequest string
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
got = r.Method + " " + r.URL.RequestURI()
|
||||
_, _ = w.Write([]byte(`{"data":{"changed":true}}`))
|
||||
switch {
|
||||
case r.URL.RequestURI() == "/login/db":
|
||||
_, _ = w.Write([]byte(`{"data":{"token":"` + testJWT(time.Now().Add(time.Hour)) + `"}}`))
|
||||
case strings.HasPrefix(r.URL.RequestURI(), "/api/v1/users?"):
|
||||
_, _ = w.Write([]byte(`{"data":{"list":[{"id":7,"name":"target@example.com"}]}}`))
|
||||
case strings.Contains(r.URL.RequestURI(), "userId=7"):
|
||||
if strings.Contains(tt.want, "POST ") {
|
||||
_, _ = w.Write([]byte(`{"data":{"list":[]}}`))
|
||||
} else {
|
||||
_, _ = w.Write([]byte(`{"data":{"list":[{"id":44}]}}`))
|
||||
}
|
||||
default:
|
||||
finalRequest = r.Method + " " + r.URL.RequestURI()
|
||||
_, _ = w.Write([]byte(`{"data":{"ok":true}}`))
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
svc := NewWayneRoleBindingService(config.Config{
|
||||
WayneInternalAPIBaseURL: server.URL,
|
||||
WayneServiceName: "xinfra",
|
||||
WayneServiceAPISecretKey: "service-secret",
|
||||
})
|
||||
svc := NewWayneRoleBindingService(testWayneConfig(server.URL), nil)
|
||||
if _, err := tt.call(svc); err != nil {
|
||||
t.Fatalf("call error = %v", err)
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Fatalf("got endpoint %q, want %q", got, tt.want)
|
||||
if finalRequest != tt.want {
|
||||
t.Fatalf("final request = %q, want %q", finalRequest, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWayneRoleBindingServiceQuerySignsEmptyBodyAndQueryURI(t *testing.T) {
|
||||
func TestWayneRoleBindingServiceRefreshesTokenAfterUnauthorized(t *testing.T) {
|
||||
loginCount := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body := readTestBody(t, r)
|
||||
if len(body) != 0 {
|
||||
t.Fatalf("GET body length = %d, want 0", len(body))
|
||||
switch r.URL.RequestURI() {
|
||||
case "/login/db":
|
||||
loginCount++
|
||||
_, _ = w.Write([]byte(`{"data":{"token":"` + testJWT(time.Now().Add(time.Hour)) + `"}}`))
|
||||
case "/api/v1/groups?type=1":
|
||||
if loginCount == 1 {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_, _ = w.Write([]byte(`{"msg":"expired"}`))
|
||||
return
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"data":{"list":[]}}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request %s", r.URL.RequestURI())
|
||||
}
|
||||
if r.URL.RequestURI() != "/api/v1/internal/groups?type=1" {
|
||||
t.Fatalf("RequestURI = %q", r.URL.RequestURI())
|
||||
}
|
||||
if !wayne.Verify("service-secret", r.Header.Get(wayne.HeaderSignature), r.Method, r.URL.RequestURI(), r.Header.Get(wayne.HeaderTimestamp), r.Header.Get(wayne.HeaderNonce), body) {
|
||||
t.Fatalf("invalid GET signature headers: %#v", r.Header)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"data":[]}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
svc := NewWayneRoleBindingService(config.Config{
|
||||
WayneInternalAPIBaseURL: server.URL,
|
||||
WayneServiceName: "xinfra",
|
||||
WayneServiceAPISecretKey: "service-secret",
|
||||
})
|
||||
svc := NewWayneRoleBindingService(testWayneConfig(server.URL), nil)
|
||||
groupType := 1
|
||||
if _, err := svc.ListGroups(context.Background(), &groupType); err != nil {
|
||||
t.Fatalf("ListGroups() error = %v", err)
|
||||
}
|
||||
if loginCount != 2 {
|
||||
t.Fatalf("loginCount = %d, want 2", loginCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWayneRoleBindingServiceOperatorPermissionsSignsQueryURI(t *testing.T) {
|
||||
func TestWayneRoleBindingServiceOperatorPermissionsFromNativePermissions(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
body := readTestBody(t, r)
|
||||
if r.URL.RequestURI() != "/api/v1/internal/namespaces/1/operator-permissions?operatorName=eastsales%40qiniu.com" {
|
||||
t.Fatalf("RequestURI = %q", r.URL.RequestURI())
|
||||
switch r.URL.RequestURI() {
|
||||
case "/login/db":
|
||||
_, _ = w.Write([]byte(`{"data":{"token":"` + testJWT(time.Now().Add(time.Hour)) + `"}}`))
|
||||
case "/api/v1/namespaces/1/users/permissions/1":
|
||||
_, _ = w.Write([]byte(`{"data":{"namespaceUser":{"create":true,"update":true,"delete":false}}}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request %s", r.URL.RequestURI())
|
||||
}
|
||||
if !wayne.Verify("service-secret", r.Header.Get(wayne.HeaderSignature), r.Method, r.URL.RequestURI(), r.Header.Get(wayne.HeaderTimestamp), r.Header.Get(wayne.HeaderNonce), body) {
|
||||
t.Fatalf("invalid operator permissions signature headers: %#v", r.Header)
|
||||
}
|
||||
_, _ = w.Write([]byte(`{"data":{"permissions":{"create":true,"update":true,"delete":false}}}`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
svc := NewWayneRoleBindingService(config.Config{
|
||||
WayneInternalAPIBaseURL: server.URL,
|
||||
WayneServiceName: "xinfra",
|
||||
WayneServiceAPISecretKey: "service-secret",
|
||||
})
|
||||
if _, err := svc.NamespaceOperatorPermissions(context.Background(), 1, "eastsales@qiniu.com"); err != nil {
|
||||
t.Fatalf("NamespaceOperatorPermissions() error = %v", err)
|
||||
svc := NewWayneRoleBindingService(testWayneConfig(server.URL), nil)
|
||||
permissions, err := svc.NamespaceOperatorPermissionsParsed(context.Background(), 1, "operator@example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("NamespaceOperatorPermissionsParsed() error = %v", err)
|
||||
}
|
||||
if !permissions.Create || !permissions.Update || permissions.Delete {
|
||||
t.Fatalf("permissions = %#v", permissions)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWayneRoleBindingServiceHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, _ = w.Write([]byte(`{"code":403,"msg":"denied"}`))
|
||||
switch r.URL.RequestURI() {
|
||||
case "/login/db":
|
||||
_, _ = w.Write([]byte(`{"data":{"token":"` + testJWT(time.Now().Add(time.Hour)) + `"}}`))
|
||||
case "/api/v1/groups":
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
_, _ = w.Write([]byte(`{"code":403,"msg":"denied"}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request %s", r.URL.RequestURI())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
svc := NewWayneRoleBindingService(config.Config{
|
||||
WayneInternalAPIBaseURL: server.URL,
|
||||
WayneServiceName: "xinfra",
|
||||
WayneServiceAPISecretKey: "service-secret",
|
||||
})
|
||||
result, err := svc.BindApp(context.Background(), 3, "target@example.com", "eastsales@qiniu.com", WayneRoleBindingRequest{GroupIDs: []uint64{20}})
|
||||
svc := NewWayneRoleBindingService(testWayneConfig(server.URL), nil)
|
||||
result, err := svc.ListGroups(context.Background(), nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
@@ -242,6 +216,39 @@ func TestWayneRoleBindingServiceHTTPError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func testWayneConfig(baseURL string) config.Config {
|
||||
return config.Config{
|
||||
WayneAPIBaseURL: baseURL,
|
||||
WayneAdminUsername: "wayne-admin",
|
||||
WayneAdminPassword: "wayne-password",
|
||||
WayneTokenTTLMinutes: 60,
|
||||
}
|
||||
}
|
||||
|
||||
func assertLoginBody(t *testing.T, r *http.Request) {
|
||||
t.Helper()
|
||||
var body map[string]string
|
||||
if err := json.Unmarshal(readTestBody(t, r), &body); err != nil {
|
||||
t.Fatalf("invalid login body: %v", err)
|
||||
}
|
||||
if body["username"] != "wayne-admin" || body["password"] != "wayne-password" {
|
||||
t.Fatalf("login body = %#v", body)
|
||||
}
|
||||
}
|
||||
|
||||
func assertBearer(t *testing.T, r *http.Request) {
|
||||
t.Helper()
|
||||
if !strings.HasPrefix(r.Header.Get("Authorization"), "Bearer ") {
|
||||
t.Fatalf("missing bearer header: %#v", r.Header)
|
||||
}
|
||||
}
|
||||
|
||||
func testJWT(exp time.Time) string {
|
||||
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"RS256","typ":"JWT"}`))
|
||||
payload := base64.RawURLEncoding.EncodeToString([]byte(`{"exp":` + strconv.FormatInt(exp.Unix(), 10) + `}`))
|
||||
return header + "." + payload + ".sig"
|
||||
}
|
||||
|
||||
func readTestBody(t *testing.T, r *http.Request) []byte {
|
||||
t.Helper()
|
||||
body, err := io.ReadAll(r.Body)
|
||||
|
||||
Reference in New Issue
Block a user