feat(auth): use Wayne native role APIs
This commit is contained in:
@@ -3,6 +3,7 @@ package service
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -15,14 +16,19 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/config"
|
||||
"github.com/1024XEngineer/xinfra/server/internal/wayne"
|
||||
"github.com/1024XEngineer/xinfra/server/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrWayneRoleBindingNotConfigured = errors.New("wayne internal role binding api is not configured")
|
||||
ErrWayneRoleBindingRequestFailed = errors.New("wayne internal role binding request failed")
|
||||
ErrWayneRoleBindingNotConfigured = errors.New("wayne native api is not configured")
|
||||
ErrWayneRoleBindingRequestFailed = errors.New("wayne native role binding request failed")
|
||||
)
|
||||
|
||||
const wayneAdminTokenAccount = "admin"
|
||||
|
||||
type WayneRoleBindingRequest struct {
|
||||
Username string `json:"username,omitempty"`
|
||||
GroupIDs []uint64 `json:"groupIds,omitempty"`
|
||||
@@ -70,14 +76,18 @@ func (e *WayneRoleBindingHTTPError) Error() string {
|
||||
}
|
||||
|
||||
type WayneRoleBindingService struct {
|
||||
cfg config.Config
|
||||
client *http.Client
|
||||
now func() time.Time
|
||||
cfg config.Config
|
||||
db *gorm.DB
|
||||
client *http.Client
|
||||
now func() time.Time
|
||||
cachedToken string
|
||||
cachedExp time.Time
|
||||
}
|
||||
|
||||
func NewWayneRoleBindingService(cfg config.Config) *WayneRoleBindingService {
|
||||
func NewWayneRoleBindingService(cfg config.Config, db *gorm.DB) *WayneRoleBindingService {
|
||||
return &WayneRoleBindingService{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
@@ -86,33 +96,33 @@ func NewWayneRoleBindingService(cfg config.Config) *WayneRoleBindingService {
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) BindNamespace(ctx context.Context, namespaceID uint64, username string, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
|
||||
return s.call(ctx, http.MethodPut, fmt.Sprintf("/api/v1/internal/namespaces/%d/users/%s/roles", namespaceID, url.PathEscape(username)), operatorEmail, req)
|
||||
return s.bindUserRoles(ctx, "namespace", namespaceID, username, req)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) UnbindNamespace(ctx context.Context, namespaceID uint64, username string, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
|
||||
return s.call(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/internal/namespaces/%d/users/%s/roles", namespaceID, url.PathEscape(username)), operatorEmail, req)
|
||||
return s.unbindUserRoles(ctx, "namespace", namespaceID, username, req)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) BindApp(ctx context.Context, appID uint64, username string, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
|
||||
return s.call(ctx, http.MethodPut, fmt.Sprintf("/api/v1/internal/apps/%d/users/%s/roles", appID, url.PathEscape(username)), operatorEmail, req)
|
||||
return s.bindUserRoles(ctx, "app", appID, username, req)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) UnbindApp(ctx context.Context, appID uint64, username string, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
|
||||
return s.call(ctx, http.MethodDelete, fmt.Sprintf("/api/v1/internal/apps/%d/users/%s/roles", appID, url.PathEscape(username)), operatorEmail, req)
|
||||
return s.unbindUserRoles(ctx, "app", appID, username, req)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) ListNamespaces(ctx context.Context) (*WayneRoleBindingResult, error) {
|
||||
return s.callRaw(ctx, http.MethodGet, "/api/v1/internal/namespaces", nil)
|
||||
return s.callRaw(ctx, http.MethodGet, "/api/v1/namespaces", nil)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) ListGroups(ctx context.Context, groupType *int) (*WayneRoleBindingResult, error) {
|
||||
internalPath := "/api/v1/internal/groups"
|
||||
path := "/api/v1/groups"
|
||||
if groupType != nil {
|
||||
values := url.Values{}
|
||||
values.Set("type", strconv.Itoa(*groupType))
|
||||
internalPath += "?" + values.Encode()
|
||||
path += "?" + values.Encode()
|
||||
}
|
||||
return s.callRaw(ctx, http.MethodGet, internalPath, nil)
|
||||
return s.callRaw(ctx, http.MethodGet, path, nil)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) ListNamespaceRoleGroups(ctx context.Context) ([]WayneRoleGroup, error) {
|
||||
@@ -146,11 +156,44 @@ func (s *WayneRoleBindingService) GetUserRoles(ctx context.Context, username str
|
||||
if username == "" {
|
||||
return nil, ErrWayenEmailMissing
|
||||
}
|
||||
return s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/internal/users/%s/roles", url.PathEscape(username)), nil)
|
||||
user, err := s.findUser(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
namespaces, err := s.listResourceIDs(ctx, "/api/v1/namespaces")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
results := make([]json.RawMessage, 0, len(namespaces))
|
||||
values := url.Values{}
|
||||
values.Set("userId", strconv.FormatUint(user.ID, 10))
|
||||
for _, namespaceID := range namespaces {
|
||||
result, err := s.callRaw(ctx, http.MethodGet, roleBindingBasePath("namespace", namespaceID)+"?"+values.Encode(), nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
results = append(results, rawWayneData(result.Body))
|
||||
}
|
||||
body, err := json.Marshal(map[string]json.RawMessage{
|
||||
"namespaceRoles": mustMarshalRaw(results),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WayneRoleBindingResult{StatusCode: http.StatusOK, ContentType: "application/json", Body: body}, nil
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) listResourceIDs(ctx context.Context, path string) ([]uint64, error) {
|
||||
result, err := s.callRaw(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseWayneResourceIDs(result.Body)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) NamespaceOperatorPermissions(ctx context.Context, namespaceID uint64, operatorEmail string) (*WayneRoleBindingResult, error) {
|
||||
return s.operatorPermissions(ctx, fmt.Sprintf("/api/v1/internal/namespaces/%d/operator-permissions", namespaceID), operatorEmail)
|
||||
return s.permissions(ctx, "namespace", namespaceID)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) NamespaceOperatorPermissionsParsed(ctx context.Context, namespaceID uint64, operatorEmail string) (*WayneOperatorPermissions, error) {
|
||||
@@ -162,59 +205,122 @@ func (s *WayneRoleBindingService) NamespaceOperatorPermissionsParsed(ctx context
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) AppOperatorPermissions(ctx context.Context, appID uint64, operatorEmail string) (*WayneRoleBindingResult, error) {
|
||||
return s.operatorPermissions(ctx, fmt.Sprintf("/api/v1/internal/apps/%d/operator-permissions", appID), operatorEmail)
|
||||
return s.permissions(ctx, "app", appID)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) call(ctx context.Context, method, internalPath, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
|
||||
if err := s.validateConfig(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
operatorEmail = strings.TrimSpace(operatorEmail)
|
||||
if operatorEmail == "" {
|
||||
func (s *WayneRoleBindingService) bindUserRoles(ctx context.Context, scope string, resourceID uint64, username string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
|
||||
if strings.TrimSpace(username) == "" {
|
||||
return nil, ErrWayenEmailMissing
|
||||
}
|
||||
|
||||
req.OperatorUserID = nil
|
||||
req.OperatorName = operatorEmail
|
||||
req.Username = ""
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
user, err := s.findUser(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.callRaw(ctx, method, internalPath, body)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) operatorPermissions(ctx context.Context, internalPath, operatorEmail string) (*WayneRoleBindingResult, error) {
|
||||
operatorEmail = strings.TrimSpace(operatorEmail)
|
||||
if operatorEmail == "" {
|
||||
return nil, ErrWayenEmailMissing
|
||||
}
|
||||
values := url.Values{}
|
||||
values.Set("operatorName", operatorEmail)
|
||||
return s.callRaw(ctx, http.MethodGet, internalPath+"?"+values.Encode(), nil)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) callRaw(ctx context.Context, method, internalPath string, body []byte) (*WayneRoleBindingResult, error) {
|
||||
if err := s.validateConfig(); err != nil {
|
||||
bindings, err := s.listUserBindings(ctx, scope, resourceID, user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body := nativeRoleBindingBody(scope, resourceID, user.ID, req.GroupIDs)
|
||||
if len(bindings) == 0 {
|
||||
return s.callRaw(ctx, http.MethodPost, roleBindingBasePath(scope, resourceID), body)
|
||||
}
|
||||
bodyWithID, err := nativeRoleBindingBodyWithID(body, bindings[0].ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.callRaw(ctx, http.MethodPut, fmt.Sprintf("%s/%d", roleBindingBasePath(scope, resourceID), bindings[0].ID), bodyWithID)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) unbindUserRoles(ctx context.Context, scope string, resourceID uint64, username string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
|
||||
if strings.TrimSpace(username) == "" {
|
||||
return nil, ErrWayenEmailMissing
|
||||
}
|
||||
user, err := s.findUser(ctx, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bindings, err := s.listUserBindings(ctx, scope, resourceID, user.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(bindings) == 0 {
|
||||
return &WayneRoleBindingResult{StatusCode: http.StatusOK, ContentType: "application/json", Body: []byte(`{"data":null}`)}, nil
|
||||
}
|
||||
return s.callRaw(ctx, http.MethodDelete, fmt.Sprintf("%s/%d", roleBindingBasePath(scope, resourceID), bindings[0].ID), nil)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) permissions(ctx context.Context, scope string, resourceID uint64) (*WayneRoleBindingResult, error) {
|
||||
path := fmt.Sprintf("%s/permissions/%d", roleBindingBasePath(scope, resourceID), resourceID)
|
||||
result, err := s.callRaw(ctx, http.MethodGet, path, nil)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
permissions, err := parseNativePermissions(result.Body, scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, err := json.Marshal(map[string]WayneOperatorPermissions{"permissions": permissions})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WayneRoleBindingResult{StatusCode: result.StatusCode, ContentType: "application/json", Body: body}, nil
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) findUser(ctx context.Context, username string) (*wayneNativeUser, error) {
|
||||
values := url.Values{}
|
||||
values.Set("name", strings.TrimSpace(username))
|
||||
result, err := s.callRaw(ctx, http.MethodGet, "/api/v1/users?"+values.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
users, err := parseWayneUsers(result.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, user := range users {
|
||||
if sameWayneUser(user, username) {
|
||||
return &user, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("wayne user %q not found", username)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) listUserBindings(ctx context.Context, scope string, resourceID uint64, userID uint64) ([]wayneRoleBinding, error) {
|
||||
values := url.Values{}
|
||||
values.Set("userId", strconv.FormatUint(userID, 10))
|
||||
result, err := s.callRaw(ctx, http.MethodGet, roleBindingBasePath(scope, resourceID)+"?"+values.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseWayneRoleBindings(result.Body)
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) callRaw(ctx context.Context, method, nativePath string, body []byte) (*WayneRoleBindingResult, error) {
|
||||
token, err := s.adminToken(ctx, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := s.callRawWithToken(ctx, method, nativePath, body, token)
|
||||
if result != nil && result.StatusCode == http.StatusUnauthorized {
|
||||
token, refreshErr := s.adminToken(ctx, true)
|
||||
if refreshErr != nil {
|
||||
return result, refreshErr
|
||||
}
|
||||
return s.callRawWithToken(ctx, method, nativePath, body, token)
|
||||
}
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) callRawWithToken(ctx context.Context, method, nativePath string, body []byte, token string) (*WayneRoleBindingResult, error) {
|
||||
if body == nil {
|
||||
body = []byte{}
|
||||
}
|
||||
target, signingURI, err := s.requestURL(internalPath)
|
||||
target, err := s.requestURL(nativePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log.Printf(
|
||||
"wayne role binding request: method=%s target=%s signing_uri=%s body_bytes=%d service_name=%s secret_configured=%t",
|
||||
method,
|
||||
target,
|
||||
signingURI,
|
||||
len(body),
|
||||
s.cfg.WayneServiceName,
|
||||
strings.TrimSpace(s.cfg.WayneServiceAPISecretKey) != "",
|
||||
)
|
||||
log.Printf("wayne native request: method=%s target=%s body_bytes=%d", method, target, len(body))
|
||||
httpReq, err := http.NewRequestWithContext(ctx, method, target, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -223,16 +329,11 @@ func (s *WayneRoleBindingService) callRaw(ctx context.Context, method, internalP
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
httpReq.Header.Set("Accept", "application/json")
|
||||
|
||||
headers, err := wayne.BuildSignedHeaders(s.cfg.WayneServiceName, s.cfg.WayneServiceAPISecretKey, method, signingURI, body, s.now())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
headers.Apply(httpReq)
|
||||
httpReq.Header.Set("Authorization", "Bearer "+token)
|
||||
|
||||
resp, err := s.client.Do(httpReq)
|
||||
if err != nil {
|
||||
log.Printf("wayne role binding request failed: method=%s target=%s error=%v", method, target, err)
|
||||
log.Printf("wayne native request failed: method=%s target=%s error=%v", method, target, err)
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
@@ -246,48 +347,154 @@ func (s *WayneRoleBindingService) callRaw(ctx context.Context, method, internalP
|
||||
ContentType: resp.Header.Get("Content-Type"),
|
||||
Body: respBody,
|
||||
}
|
||||
log.Printf(
|
||||
"wayne role binding response: method=%s target=%s status=%d content_type=%q body=%q",
|
||||
method,
|
||||
target,
|
||||
resp.StatusCode,
|
||||
result.ContentType,
|
||||
truncateForDebugLog(string(respBody), 512),
|
||||
)
|
||||
log.Printf("wayne native response: method=%s target=%s status=%d content_type=%q body=%q", method, target, resp.StatusCode, result.ContentType, truncateForDebugLog(string(respBody), 512))
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
|
||||
return result, &WayneRoleBindingHTTPError{StatusCode: resp.StatusCode, Body: respBody}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) adminToken(ctx context.Context, forceRefresh bool) (string, error) {
|
||||
if err := s.validateConfig(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
now := s.now()
|
||||
if !forceRefresh && strings.TrimSpace(s.cachedToken) != "" && s.cachedExp.After(now.Add(time.Minute)) {
|
||||
return s.cachedToken, nil
|
||||
}
|
||||
if !forceRefresh && s.db != nil {
|
||||
var saved model.WayneToken
|
||||
err := s.db.WithContext(ctx).Where("account = ?", wayneAdminTokenAccount).First(&saved).Error
|
||||
if err == nil && strings.TrimSpace(saved.Token) != "" && saved.ExpiresAt.After(now.Add(time.Minute)) {
|
||||
s.cachedToken = saved.Token
|
||||
s.cachedExp = saved.ExpiresAt
|
||||
return saved.Token, nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
token, expiresAt, err := s.loginAdmin(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
s.cachedToken = token
|
||||
s.cachedExp = expiresAt
|
||||
if s.db != nil {
|
||||
err = s.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "account"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"token", "expires_at", "updated_at"}),
|
||||
}).Create(&model.WayneToken{
|
||||
Account: wayneAdminTokenAccount,
|
||||
Token: token,
|
||||
ExpiresAt: expiresAt,
|
||||
}).Error
|
||||
}
|
||||
return token, err
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) loginAdmin(ctx context.Context) (string, time.Time, error) {
|
||||
body, err := json.Marshal(map[string]string{
|
||||
"username": s.cfg.WayneAdminUsername,
|
||||
"password": s.cfg.WayneAdminPassword,
|
||||
})
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
target, err := s.requestURL("/login/db")
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, target, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Accept", "application/json")
|
||||
resp, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
|
||||
return "", time.Time{}, &WayneRoleBindingHTTPError{StatusCode: resp.StatusCode, Body: respBody}
|
||||
}
|
||||
token, err := parseWayneLoginToken(respBody)
|
||||
if err != nil {
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
expiresAt := wayneTokenExpiresAt(token, s.now().Add(time.Duration(s.cfg.WayneTokenTTLMinutes)*time.Minute))
|
||||
return token, expiresAt, nil
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) validateConfig() error {
|
||||
baseConfigured := strings.TrimSpace(s.cfg.WayneInternalAPIBaseURL) != ""
|
||||
serviceConfigured := strings.TrimSpace(s.cfg.WayneServiceName) != ""
|
||||
secretConfigured := strings.TrimSpace(s.cfg.WayneServiceAPISecretKey) != ""
|
||||
if !baseConfigured || !serviceConfigured || !secretConfigured {
|
||||
baseConfigured := strings.TrimSpace(s.cfg.WayneAPIBaseURL) != ""
|
||||
usernameConfigured := strings.TrimSpace(s.cfg.WayneAdminUsername) != ""
|
||||
passwordConfigured := strings.TrimSpace(s.cfg.WayneAdminPassword) != ""
|
||||
if !baseConfigured || !usernameConfigured || !passwordConfigured {
|
||||
log.Printf(
|
||||
"wayne role binding config invalid: base_url_configured=%t service_name_configured=%t secret_configured=%t",
|
||||
"wayne native config invalid: base_url_configured=%t admin_username_configured=%t admin_password_configured=%t",
|
||||
baseConfigured,
|
||||
serviceConfigured,
|
||||
secretConfigured,
|
||||
usernameConfigured,
|
||||
passwordConfigured,
|
||||
)
|
||||
return ErrWayneRoleBindingNotConfigured
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WayneRoleBindingService) requestURL(internalPath string) (string, string, error) {
|
||||
base, err := url.Parse(strings.TrimRight(strings.TrimSpace(s.cfg.WayneInternalAPIBaseURL), "/"))
|
||||
func (s *WayneRoleBindingService) requestURL(nativePath string) (string, error) {
|
||||
base, err := url.Parse(strings.TrimRight(strings.TrimSpace(s.cfg.WayneAPIBaseURL), "/"))
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
return "", err
|
||||
}
|
||||
if base.Scheme == "" || base.Host == "" {
|
||||
return "", "", fmt.Errorf("invalid wayne internal api base url: %s", s.cfg.WayneInternalAPIBaseURL)
|
||||
return "", fmt.Errorf("invalid wayne api base url: %s", s.cfg.WayneAPIBaseURL)
|
||||
}
|
||||
path, rawQuery, _ := strings.Cut(internalPath, "?")
|
||||
path, rawQuery, _ := strings.Cut(nativePath, "?")
|
||||
base.Path = strings.TrimRight(base.Path, "/") + path
|
||||
base.RawQuery = rawQuery
|
||||
return base.String(), base.RequestURI(), nil
|
||||
return base.String(), nil
|
||||
}
|
||||
|
||||
func roleBindingBasePath(scope string, resourceID uint64) string {
|
||||
switch scope {
|
||||
case "app":
|
||||
return fmt.Sprintf("/api/v1/apps/%d/users", resourceID)
|
||||
default:
|
||||
return fmt.Sprintf("/api/v1/namespaces/%d/users", resourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func nativeRoleBindingBody(scope string, resourceID, userID uint64, groupIDs []uint64) []byte {
|
||||
groups := make([]map[string]uint64, 0, len(groupIDs))
|
||||
for _, groupID := range groupIDs {
|
||||
groups = append(groups, map[string]uint64{"id": groupID})
|
||||
}
|
||||
body := map[string]any{
|
||||
"user": map[string]uint64{"id": userID},
|
||||
"groups": groups,
|
||||
}
|
||||
if scope == "app" {
|
||||
body["app"] = map[string]uint64{"id": resourceID}
|
||||
} else {
|
||||
body["namespace"] = map[string]uint64{"id": resourceID}
|
||||
}
|
||||
raw, _ := json.Marshal(body)
|
||||
return raw
|
||||
}
|
||||
|
||||
func nativeRoleBindingBodyWithID(body []byte, id uint64) ([]byte, error) {
|
||||
var payload map[string]any
|
||||
if err := json.Unmarshal(body, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload["id"] = id
|
||||
return json.Marshal(payload)
|
||||
}
|
||||
|
||||
func truncateForDebugLog(value string, limit int) string {
|
||||
@@ -298,42 +505,169 @@ func truncateForDebugLog(value string, limit int) string {
|
||||
return value[:limit] + "...(truncated)"
|
||||
}
|
||||
|
||||
func parseWayneLoginToken(body []byte) (string, error) {
|
||||
var wrapped struct {
|
||||
Data struct {
|
||||
Token string `json:"token"`
|
||||
} `json:"data"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapped); err != nil {
|
||||
return "", err
|
||||
}
|
||||
token := strings.TrimSpace(wrapped.Data.Token)
|
||||
if token == "" {
|
||||
token = strings.TrimSpace(wrapped.Token)
|
||||
}
|
||||
if token == "" {
|
||||
return "", fmt.Errorf("wayne login response token is empty")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func wayneTokenExpiresAt(token string, fallback time.Time) time.Time {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) < 2 {
|
||||
return fallback
|
||||
}
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
var claims struct {
|
||||
ExpiresAt int64 `json:"exp"`
|
||||
}
|
||||
if err := json.Unmarshal(payload, &claims); err != nil || claims.ExpiresAt <= 0 {
|
||||
return fallback
|
||||
}
|
||||
return time.Unix(claims.ExpiresAt, 0)
|
||||
}
|
||||
|
||||
func parseWayneRoleGroups(body []byte) ([]WayneRoleGroup, error) {
|
||||
var wrapped struct {
|
||||
Data []WayneRoleGroup `json:"data"`
|
||||
Items []WayneRoleGroup `json:"items"`
|
||||
Data struct {
|
||||
List []WayneRoleGroup `json:"list"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapped); err == nil {
|
||||
if wrapped.Data != nil {
|
||||
return wrapped.Data, nil
|
||||
}
|
||||
if wrapped.Items != nil {
|
||||
return wrapped.Items, nil
|
||||
}
|
||||
}
|
||||
var direct []WayneRoleGroup
|
||||
if err := json.Unmarshal(body, &direct); err != nil {
|
||||
if err := json.Unmarshal(body, &wrapped); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return direct, nil
|
||||
return wrapped.Data.List, nil
|
||||
}
|
||||
|
||||
func parseWayneOperatorPermissions(body []byte) (*WayneOperatorPermissions, error) {
|
||||
var wrapped struct {
|
||||
Data struct {
|
||||
Permissions WayneOperatorPermissions `json:"permissions"`
|
||||
} `json:"data"`
|
||||
Permissions WayneOperatorPermissions `json:"permissions"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapped); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if wrapped.Data.Permissions != (WayneOperatorPermissions{}) {
|
||||
return &wrapped.Data.Permissions, nil
|
||||
}
|
||||
return &wrapped.Permissions, nil
|
||||
}
|
||||
|
||||
func parseNativePermissions(body []byte, scope string) (WayneOperatorPermissions, error) {
|
||||
var wrapped struct {
|
||||
Data map[string]map[string]bool `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapped); err != nil {
|
||||
return WayneOperatorPermissions{}, err
|
||||
}
|
||||
key := "namespaceUser"
|
||||
if scope == "app" {
|
||||
key = "appUser"
|
||||
}
|
||||
return WayneOperatorPermissions{
|
||||
Create: wrapped.Data[key]["create"],
|
||||
Update: wrapped.Data[key]["update"],
|
||||
Delete: wrapped.Data[key]["delete"],
|
||||
}, nil
|
||||
}
|
||||
|
||||
func rawWayneData(body []byte) json.RawMessage {
|
||||
var wrapped struct {
|
||||
Data json.RawMessage `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapped); err == nil && len(wrapped.Data) > 0 {
|
||||
return wrapped.Data
|
||||
}
|
||||
return json.RawMessage(body)
|
||||
}
|
||||
|
||||
func mustMarshalRaw(value any) json.RawMessage {
|
||||
raw, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return json.RawMessage(`null`)
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
func parseWayneResourceIDs(body []byte) ([]uint64, error) {
|
||||
var wrapped struct {
|
||||
Data struct {
|
||||
List []struct {
|
||||
ID uint64 `json:"id"`
|
||||
} `json:"list"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapped); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resourceIDsFromItems(wrapped.Data.List), nil
|
||||
}
|
||||
|
||||
func resourceIDsFromItems(items []struct {
|
||||
ID uint64 `json:"id"`
|
||||
}) []uint64 {
|
||||
ids := make([]uint64, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.ID != 0 {
|
||||
ids = append(ids, item.ID)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
type wayneNativeUser struct {
|
||||
ID uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
Display string `json:"display"`
|
||||
}
|
||||
|
||||
func parseWayneUsers(body []byte) ([]wayneNativeUser, error) {
|
||||
var wrapped struct {
|
||||
Data struct {
|
||||
List []wayneNativeUser `json:"list"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapped); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wrapped.Data.List, nil
|
||||
}
|
||||
|
||||
func sameWayneUser(user wayneNativeUser, username string) bool {
|
||||
username = strings.TrimSpace(strings.ToLower(username))
|
||||
return strings.ToLower(strings.TrimSpace(user.Name)) == username ||
|
||||
strings.ToLower(strings.TrimSpace(user.Email)) == username
|
||||
}
|
||||
|
||||
type wayneRoleBinding struct {
|
||||
ID uint64 `json:"id"`
|
||||
}
|
||||
|
||||
func parseWayneRoleBindings(body []byte) ([]wayneRoleBinding, error) {
|
||||
var wrapped struct {
|
||||
Data struct {
|
||||
List []wayneRoleBinding `json:"list"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(body, &wrapped); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wrapped.Data.List, nil
|
||||
}
|
||||
|
||||
func isWayneVisitorRoleName(name string) bool {
|
||||
normalized := strings.ToLower(strings.TrimSpace(name))
|
||||
return normalized == "访客" || normalized == "visitor" || strings.Contains(normalized, "visitor")
|
||||
|
||||
@@ -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