Files
xinfra/server/internal/service/wayne_role_binding.go
T

775 lines
23 KiB
Go

package service
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/1024XEngineer/xinfra/server/internal/config"
"github.com/1024XEngineer/xinfra/server/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
var (
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"`
OperatorUserID *uint64 `json:"operatorUserId,omitempty"`
OperatorName string `json:"operatorName,omitempty"`
Replace *bool `json:"replace,omitempty"`
RequestID string `json:"requestId,omitempty"`
Reason string `json:"reason,omitempty"`
DryRun bool `json:"dryRun,omitempty"`
}
type WayneRoleBindingResult struct {
StatusCode int
ContentType string
Body []byte
}
type WayneRoleGroup struct {
ID uint64 `json:"id"`
Name string `json:"name"`
Comment string `json:"comment"`
Type int `json:"type"`
}
type WayneNamespace struct {
ID uint64 `json:"id"`
Name string `json:"name"`
KubeNamespace string `json:"kubeNamespace"`
MetaData string `json:"metaData,omitempty"`
}
type WayneOperatorPermissions struct {
Create bool `json:"create"`
Update bool `json:"update"`
Delete bool `json:"delete"`
}
type WayneRoleBindingHTTPError struct {
StatusCode int
Body []byte
}
func (e *WayneRoleBindingHTTPError) Error() string {
body := strings.TrimSpace(string(e.Body))
if body == "" {
return fmt.Sprintf("%s: status %d", ErrWayneRoleBindingRequestFailed, e.StatusCode)
}
if len(body) > 512 {
body = body[:512]
}
return fmt.Sprintf("%s: status %d: %s", ErrWayneRoleBindingRequestFailed, e.StatusCode, body)
}
type WayneRoleBindingService struct {
cfg config.Config
db *gorm.DB
client *http.Client
now func() time.Time
cachedToken string
cachedExp time.Time
}
func NewWayneRoleBindingService(cfg config.Config, db *gorm.DB) *WayneRoleBindingService {
return &WayneRoleBindingService{
cfg: cfg,
db: db,
client: &http.Client{
Timeout: 10 * time.Second,
},
now: time.Now,
}
}
func (s *WayneRoleBindingService) BindNamespace(ctx context.Context, namespaceID uint64, username string, operatorEmail string, req WayneRoleBindingRequest) (*WayneRoleBindingResult, error) {
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.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.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.unbindUserRoles(ctx, "app", appID, username, req)
}
func (s *WayneRoleBindingService) ListNamespaces(ctx context.Context) (*WayneRoleBindingResult, error) {
return s.callRaw(ctx, http.MethodGet, "/api/v1/namespaces", nil)
}
func (s *WayneRoleBindingService) EnsureNamespace(ctx context.Context, name string) (*WayneNamespace, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("wayne namespace name is required")
}
namespace, err := s.FindNamespaceByName(ctx, name)
if err != nil {
return nil, err
}
if namespace != nil {
return namespace, nil
}
return s.CreateNamespace(ctx, WayneNamespace{
Name: name,
KubeNamespace: name,
MetaData: "{}",
})
}
func (s *WayneRoleBindingService) FindNamespaceByName(ctx context.Context, name string) (*WayneNamespace, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("wayne namespace name is required")
}
values := url.Values{}
values.Set("name", name)
values.Set("pageNo", "1")
values.Set("pageSize", "500")
result, err := s.callRaw(ctx, http.MethodGet, "/api/v1/namespaces?"+values.Encode(), nil)
if err != nil {
return nil, err
}
namespaces, err := parseWayneNamespaces(result.Body)
if err != nil {
return nil, err
}
for _, namespace := range namespaces {
if strings.EqualFold(strings.TrimSpace(namespace.Name), name) {
return &namespace, nil
}
}
return nil, nil
}
func (s *WayneRoleBindingService) CreateNamespace(ctx context.Context, namespace WayneNamespace) (*WayneNamespace, error) {
namespace.Name = strings.TrimSpace(namespace.Name)
namespace.KubeNamespace = strings.TrimSpace(namespace.KubeNamespace)
if namespace.Name == "" {
return nil, fmt.Errorf("wayne namespace name is required")
}
if namespace.KubeNamespace == "" {
namespace.KubeNamespace = namespace.Name
}
if strings.TrimSpace(namespace.MetaData) == "" {
namespace.MetaData = "{}"
}
body, err := json.Marshal(namespace)
if err != nil {
return nil, err
}
result, err := s.callRaw(ctx, http.MethodPost, "/api/v1/namespaces", body)
if err != nil {
return nil, err
}
created, err := parseWayneNamespace(result.Body)
if err != nil {
return nil, err
}
return &created, nil
}
func (s *WayneRoleBindingService) ListGroups(ctx context.Context, groupType *int) (*WayneRoleBindingResult, error) {
path := "/api/v1/groups"
if groupType != nil {
values := url.Values{}
values.Set("type", strconv.Itoa(*groupType))
path += "?" + values.Encode()
}
return s.callRaw(ctx, http.MethodGet, path, nil)
}
func (s *WayneRoleBindingService) ListNamespaceRoleGroups(ctx context.Context) ([]WayneRoleGroup, error) {
groupType := 1
result, err := s.ListGroups(ctx, &groupType)
if err != nil {
return nil, err
}
return parseWayneRoleGroups(result.Body)
}
func (s *WayneRoleBindingService) NamespaceVisitorGroupIDs(ctx context.Context) ([]uint64, error) {
groups, err := s.ListNamespaceRoleGroups(ctx)
if err != nil {
return nil, err
}
ids := make([]uint64, 0, 1)
for _, group := range groups {
if isWayneVisitorRoleName(group.Name) {
ids = append(ids, group.ID)
}
}
if len(ids) == 0 {
return nil, fmt.Errorf("wayne visitor role group not found")
}
return ids, nil
}
func (s *WayneRoleBindingService) GetUserRoles(ctx context.Context, username string) (*WayneRoleBindingResult, error) {
username = strings.TrimSpace(username)
if username == "" {
return nil, ErrWayenEmailMissing
}
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.permissions(ctx, "namespace", namespaceID)
}
func (s *WayneRoleBindingService) NamespaceOperatorPermissionsParsed(ctx context.Context, namespaceID uint64, operatorEmail string) (*WayneOperatorPermissions, error) {
result, err := s.NamespaceOperatorPermissions(ctx, namespaceID, operatorEmail)
if err != nil {
return nil, err
}
return parseWayneOperatorPermissions(result.Body)
}
func (s *WayneRoleBindingService) AppOperatorPermissions(ctx context.Context, appID uint64, operatorEmail string) (*WayneRoleBindingResult, error) {
return s.permissions(ctx, "app", appID)
}
func (s *WayneRoleBindingService) bindUserRoles(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
}
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, err := s.requestURL(nativePath)
if err != nil {
return nil, err
}
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
}
if len(body) > 0 {
httpReq.Header.Set("Content-Type", "application/json")
}
httpReq.Header.Set("Accept", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+token)
resp, err := s.client.Do(httpReq)
if err != nil {
log.Printf("wayne native request failed: method=%s target=%s error=%v", method, target, err)
return nil, err
}
defer resp.Body.Close()
respBody, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
if err != nil {
return nil, err
}
result := &WayneRoleBindingResult{
StatusCode: resp.StatusCode,
ContentType: resp.Header.Get("Content-Type"),
Body: respBody,
}
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.WayneAPIBaseURL) != ""
usernameConfigured := strings.TrimSpace(s.cfg.WayneAdminUsername) != ""
passwordConfigured := strings.TrimSpace(s.cfg.WayneAdminPassword) != ""
if !baseConfigured || !usernameConfigured || !passwordConfigured {
log.Printf(
"wayne native config invalid: base_url_configured=%t admin_username_configured=%t admin_password_configured=%t",
baseConfigured,
usernameConfigured,
passwordConfigured,
)
return ErrWayneRoleBindingNotConfigured
}
return nil
}
func (s *WayneRoleBindingService) requestURL(nativePath string) (string, error) {
base, err := url.Parse(strings.TrimRight(strings.TrimSpace(s.cfg.WayneAPIBaseURL), "/"))
if err != nil {
return "", err
}
if base.Scheme == "" || base.Host == "" {
return "", fmt.Errorf("invalid wayne api base url: %s", s.cfg.WayneAPIBaseURL)
}
path, rawQuery, _ := strings.Cut(nativePath, "?")
base.Path = strings.TrimRight(base.Path, "/") + path
base.RawQuery = rawQuery
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 {
value = strings.TrimSpace(value)
if limit <= 0 || len(value) <= limit {
return value
}
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 struct {
List []WayneRoleGroup `json:"list"`
} `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err != nil {
return nil, err
}
return wrapped.Data.List, nil
}
func parseWayneOperatorPermissions(body []byte) (*WayneOperatorPermissions, error) {
var wrapped struct {
Permissions WayneOperatorPermissions `json:"permissions"`
}
if err := json.Unmarshal(body, &wrapped); err != nil {
return nil, err
}
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 parseWayneNamespace(body []byte) (WayneNamespace, error) {
var wrapped struct {
Data WayneNamespace `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err != nil {
return WayneNamespace{}, err
}
return wrapped.Data, nil
}
func parseWayneNamespaces(body []byte) ([]WayneNamespace, error) {
var wrapped struct {
Data struct {
List []WayneNamespace `json:"list"`
} `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err != nil {
return nil, err
}
return 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")
}