feat: add business line permissions and Wayne namespace mapping
This commit is contained in:
+2
-1
@@ -11,4 +11,5 @@ web/dist/
|
||||
certs/
|
||||
scripts/
|
||||
data/
|
||||
docs/
|
||||
docs/*
|
||||
!docs/mysql-init.sql
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
CREATE DATABASE IF NOT EXISTS authserver
|
||||
DEFAULT CHARACTER SET utf8mb4
|
||||
DEFAULT COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
GRANT ALL PRIVILEGES ON authserver.* TO 'auth'@'localhost' IDENTIFIED BY 'auth';
|
||||
GRANT ALL PRIVILEGES ON authserver.* TO 'auth'@'127.0.0.1' IDENTIFIED BY 'auth';
|
||||
GRANT ALL PRIVILEGES ON authserver.* TO 'auth'@'192.168.65.%' IDENTIFIED BY 'auth';
|
||||
GRANT ALL PRIVILEGES ON authserver.* TO 'auth'@'%' IDENTIFIED BY 'auth';
|
||||
|
||||
FLUSH PRIVILEGES;
|
||||
|
||||
USE authserver;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `business_lines` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL,
|
||||
`created_at` datetime(3) DEFAULT NULL,
|
||||
`updated_at` datetime(3) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_business_lines_name` (`name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `business_line_users` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`business_line_id` bigint unsigned NOT NULL,
|
||||
`user_id` bigint unsigned NOT NULL,
|
||||
`permission` bigint NOT NULL DEFAULT '1',
|
||||
`created_at` datetime(3) DEFAULT NULL,
|
||||
`updated_at` datetime(3) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_business_line_users_unique` (`business_line_id`,`user_id`),
|
||||
KEY `idx_business_line_users_business_line_id` (`business_line_id`),
|
||||
KEY `idx_business_line_users_user_id` (`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS `business_line_wayne_namespaces` (
|
||||
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
|
||||
`business_line_id` bigint unsigned NOT NULL,
|
||||
`wayne_namespace_id` bigint unsigned NOT NULL,
|
||||
`wayne_namespace_name` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`kube_namespace` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '',
|
||||
`created_at` datetime(3) DEFAULT NULL,
|
||||
`updated_at` datetime(3) DEFAULT NULL,
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `idx_business_line_wayne_namespaces_unique` (`business_line_id`,`wayne_namespace_id`),
|
||||
KEY `idx_business_line_wayne_namespaces_business_line_id` (`business_line_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -15,6 +15,9 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
return db.AutoMigrate(
|
||||
&model.User{},
|
||||
&model.WayenCredential{},
|
||||
&model.BusinessLine{},
|
||||
&model.BusinessLineUser{},
|
||||
&model.BusinessLineWayneNamespace{},
|
||||
&model.AccessToken{},
|
||||
&model.AuditLog{},
|
||||
)
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type BusinessLineHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
type BusinessLineWithPermission struct {
|
||||
ID uint64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Permission int `json:"permission"`
|
||||
}
|
||||
|
||||
type GrantBusinessLinePermissionRequest struct {
|
||||
BusinessLineID uint64 `json:"business_line_id" binding:"required"`
|
||||
TargetUserID uint64 `json:"target_user_id" binding:"required"`
|
||||
TargetBusinessLineID uint64 `json:"target_business_line_id" binding:"required"`
|
||||
Permission int `json:"permission"`
|
||||
}
|
||||
|
||||
type BusinessLinePayload struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
}
|
||||
|
||||
type WayneNamespaceBindingPayload struct {
|
||||
Namespaces []WayneNamespaceBindingItem `json:"namespaces"`
|
||||
}
|
||||
|
||||
type WayneNamespaceBindingItem struct {
|
||||
ID uint64 `json:"id" binding:"required"`
|
||||
Name string `json:"name"`
|
||||
KubeNamespace string `json:"kubeNamespace"`
|
||||
}
|
||||
|
||||
func NewBusinessLineHandler(db *gorm.DB) *BusinessLineHandler {
|
||||
return &BusinessLineHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ListCurrentUserBusinessLines(c *gin.Context) {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return
|
||||
}
|
||||
|
||||
if claims.IsAdmin {
|
||||
var rows []model.BusinessLine
|
||||
if err := h.db.Order("id ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]BusinessLineWithPermission, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, BusinessLineWithPermission{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
CreatedAt: row.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: row.UpdatedAt.Format(time.RFC3339),
|
||||
Permission: 0,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
return
|
||||
}
|
||||
|
||||
var rows []struct {
|
||||
ID uint64
|
||||
Name string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
Permission int
|
||||
}
|
||||
if err := h.db.
|
||||
Table("business_line_users").
|
||||
Select("business_lines.id, business_lines.name, business_lines.created_at, business_lines.updated_at, business_line_users.permission").
|
||||
Joins("JOIN business_lines ON business_lines.id = business_line_users.business_line_id").
|
||||
Where("business_line_users.user_id = ?", claims.UserID).
|
||||
Order("business_lines.id ASC").
|
||||
Scan(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]BusinessLineWithPermission, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, BusinessLineWithPermission{
|
||||
ID: row.ID,
|
||||
Name: row.Name,
|
||||
CreatedAt: row.CreatedAt.Format(time.RFC3339),
|
||||
UpdatedAt: row.UpdatedAt.Format(time.RFC3339),
|
||||
Permission: row.Permission,
|
||||
})
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ListAll(c *gin.Context) {
|
||||
if !requirePlatformAdmin(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var rows []model.BusinessLine
|
||||
if err := h.db.Order("id ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]gin.H, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, gin.H{
|
||||
"id": row.ID,
|
||||
"name": row.Name,
|
||||
"created_at": row.CreatedAt.Format(time.RFC3339),
|
||||
"updated_at": row.UpdatedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) Create(c *gin.Context) {
|
||||
if !requirePlatformAdmin(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var req BusinessLinePayload
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
item := model.BusinessLine{Name: req.Name}
|
||||
if err := h.db.Create(&item).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
writeBusinessLine(c, item)
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) Update(c *gin.Context) {
|
||||
if !requirePlatformAdmin(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var req BusinessLinePayload
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var item model.BusinessLine
|
||||
if err := h.db.First(&item, "id = ?", c.Param("id")).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.db.Model(&item).Update("name", req.Name).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
item.Name = req.Name
|
||||
writeBusinessLine(c, item)
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) Delete(c *gin.Context) {
|
||||
if !requirePlatformAdmin(c) {
|
||||
return
|
||||
}
|
||||
|
||||
var item model.BusinessLine
|
||||
if err := h.db.First(&item, "id = ?", c.Param("id")).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if err := h.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("business_line_id = ?", item.ID).Delete(&model.BusinessLineUser{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("business_line_id = ?", item.ID).Delete(&model.BusinessLineWayneNamespace{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&item).Error
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) GrantPermission(c *gin.Context) {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return
|
||||
}
|
||||
|
||||
var req GrantBusinessLinePermissionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
if req.Permission != 0 && req.Permission != 1 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "permission must be 0 or 1"})
|
||||
return
|
||||
}
|
||||
|
||||
if !claims.IsAdmin {
|
||||
var currentBinding model.BusinessLineUser
|
||||
if err := h.db.Where("business_line_id = ? AND user_id = ? AND permission = ?", req.BusinessLineID, claims.UserID, 0).
|
||||
First(¤tBinding).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "current user is not platform admin or business line admin"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var businessLine model.BusinessLine
|
||||
if err := h.db.First(&businessLine, "id = ?", req.BusinessLineID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var targetUser model.User
|
||||
if err := h.db.Where("id = ? AND deleted_at IS NULL", req.TargetUserID).First(&targetUser).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "target user not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var targetBusinessLine model.BusinessLine
|
||||
if err := h.db.First(&targetBusinessLine, "id = ?", req.TargetBusinessLineID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "target business line not found"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
var binding model.BusinessLineUser
|
||||
err := h.db.Where("business_line_id = ? AND user_id = ?", req.TargetBusinessLineID, req.TargetUserID).First(&binding).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
binding = model.BusinessLineUser{
|
||||
BusinessLineID: req.TargetBusinessLineID,
|
||||
UserID: req.TargetUserID,
|
||||
Permission: req.Permission,
|
||||
}
|
||||
if err := h.db.Create(&binding).Error; err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
} else if binding.Permission != req.Permission {
|
||||
if err := h.db.Model(&binding).Update("permission", req.Permission).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
binding.Permission = req.Permission
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": binding.ID,
|
||||
"business_line_id": binding.BusinessLineID,
|
||||
"user_id": binding.UserID,
|
||||
"permission": binding.Permission,
|
||||
"created_at": binding.CreatedAt.Format(time.RFC3339),
|
||||
"updated_at": binding.UpdatedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ListWayneNamespaces(c *gin.Context) {
|
||||
businessLineID, ok := parseBusinessLineID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !h.canManageBusinessLine(c, businessLineID) {
|
||||
return
|
||||
}
|
||||
|
||||
var rows []model.BusinessLineWayneNamespace
|
||||
if err := h.db.Where("business_line_id = ?", businessLineID).Order("wayne_namespace_id ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]gin.H, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, gin.H{
|
||||
"id": row.WayneNamespaceID,
|
||||
"name": row.WayneNamespaceName,
|
||||
"kubeNamespace": row.KubeNamespace,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ReplaceWayneNamespaces(c *gin.Context) {
|
||||
businessLineID, ok := parseBusinessLineID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !h.canManageBusinessLine(c, businessLineID) {
|
||||
return
|
||||
}
|
||||
|
||||
var req WayneNamespaceBindingPayload
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("business_line_id = ?", businessLineID).Delete(&model.BusinessLineWayneNamespace{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range req.Namespaces {
|
||||
row := model.BusinessLineWayneNamespace{
|
||||
BusinessLineID: businessLineID,
|
||||
WayneNamespaceID: item.ID,
|
||||
WayneNamespaceName: item.Name,
|
||||
KubeNamespace: item.KubeNamespace,
|
||||
}
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) canManageBusinessLine(c *gin.Context, businessLineID uint64) bool {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return false
|
||||
}
|
||||
|
||||
var businessLine model.BusinessLine
|
||||
if err := h.db.First(&businessLine, "id = ?", businessLineID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
||||
return false
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return false
|
||||
}
|
||||
if claims.IsAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
var binding model.BusinessLineUser
|
||||
if err := h.db.Where("business_line_id = ? AND user_id = ? AND permission = ?", businessLineID, claims.UserID, 0).
|
||||
First(&binding).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "current user is not platform admin or business line admin"})
|
||||
return false
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func parseBusinessLineID(c *gin.Context) (uint64, bool) {
|
||||
value, err := strconv.ParseUint(c.Param("id"), 10, 64)
|
||||
if err != nil || value == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid business line id"})
|
||||
return 0, false
|
||||
}
|
||||
return value, true
|
||||
}
|
||||
|
||||
func requirePlatformAdmin(c *gin.Context) bool {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return false
|
||||
}
|
||||
if !claims.IsAdmin {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "platform admin required"})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func writeBusinessLine(c *gin.Context, item model.BusinessLine) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"id": item.ID,
|
||||
"name": item.Name,
|
||||
"created_at": item.CreatedAt.Format(time.RFC3339),
|
||||
"updated_at": item.UpdatedAt.Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
@@ -3,13 +3,18 @@ package handler
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserHandler struct{}
|
||||
type UserHandler struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserHandler() *UserHandler {
|
||||
return &UserHandler{}
|
||||
func NewUserHandler(db *gorm.DB) *UserHandler {
|
||||
return &UserHandler{db: db}
|
||||
}
|
||||
|
||||
func (h *UserHandler) Me(c *gin.Context) {
|
||||
@@ -25,3 +30,20 @@ func (h *UserHandler) Me(c *gin.Context) {
|
||||
"is_admin": claims.IsAdmin,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *UserHandler) List(c *gin.Context) {
|
||||
var users []model.User
|
||||
if err := h.db.Where("deleted_at IS NULL").Order("id ASC").Find(&users).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]gin.H, 0, len(users))
|
||||
for _, user := range users {
|
||||
items = append(items, gin.H{
|
||||
"uid": user.ID,
|
||||
"username": user.Username,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/auth"
|
||||
@@ -40,7 +42,12 @@ func (h *WayenHandler) Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.wayen.Login(email, claims.Username)
|
||||
refOverride, ok := h.resolveWayneRef(c, claims)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.wayen.Login(email, claims.Username, refOverride)
|
||||
if err != nil {
|
||||
h.writeAudit(c, claims.UserID, claims.Username, "deny", err.Error())
|
||||
switch {
|
||||
@@ -73,6 +80,54 @@ func (h *WayenHandler) Login(c *gin.Context) {
|
||||
c.Redirect(http.StatusFound, result.TargetURL)
|
||||
}
|
||||
|
||||
func (h *WayenHandler) resolveWayneRef(c *gin.Context, claims *auth.Claims) (string, bool) {
|
||||
rawBusinessLineID := strings.TrimSpace(c.Query("business_line_id"))
|
||||
if rawBusinessLineID == "" {
|
||||
return "", true
|
||||
}
|
||||
|
||||
businessLineID, err := strconv.ParseUint(rawBusinessLineID, 10, 64)
|
||||
if err != nil || businessLineID == 0 {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid business_line_id"})
|
||||
return "", false
|
||||
}
|
||||
|
||||
var businessLine model.BusinessLine
|
||||
if err := h.db.First(&businessLine, "id = ?", businessLineID).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
|
||||
return "", false
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return "", false
|
||||
}
|
||||
|
||||
if !claims.IsAdmin {
|
||||
var binding model.BusinessLineUser
|
||||
if err := h.db.Where("business_line_id = ? AND user_id = ?", businessLineID, claims.UserID).First(&binding).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "current user does not have business line permission"})
|
||||
return "", false
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
var mapping model.BusinessLineWayneNamespace
|
||||
if err := h.db.Where("business_line_id = ?", businessLineID).Order("wayne_namespace_id ASC").First(&mapping).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", true
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return "", false
|
||||
}
|
||||
if mapping.WayneNamespaceID == 0 {
|
||||
return "", true
|
||||
}
|
||||
return fmt.Sprintf("/portal/namespace/%d/app", mapping.WayneNamespaceID), true
|
||||
}
|
||||
|
||||
func (h *WayenHandler) GetCredential(c *gin.Context) {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
|
||||
@@ -32,6 +32,32 @@ type WayenCredential struct {
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
type BusinessLine struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:128;not null;uniqueIndex" json:"name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type BusinessLineUser struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
BusinessLineID uint64 `gorm:"not null;uniqueIndex:idx_business_line_users_unique,priority:1;index" json:"business_line_id"`
|
||||
UserID uint64 `gorm:"not null;uniqueIndex:idx_business_line_users_unique,priority:2;index" json:"user_id"`
|
||||
Permission int `gorm:"not null;default:1" json:"permission"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type BusinessLineWayneNamespace struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
BusinessLineID uint64 `gorm:"not null;uniqueIndex:idx_business_line_wayne_namespaces_unique,priority:1;index" json:"business_line_id"`
|
||||
WayneNamespaceID uint64 `gorm:"not null;uniqueIndex:idx_business_line_wayne_namespaces_unique,priority:2" json:"wayne_namespace_id"`
|
||||
WayneNamespaceName string `gorm:"size:128;not null;default:''" json:"wayne_namespace_name"`
|
||||
KubeNamespace string `gorm:"size:128;not null;default:''" json:"kube_namespace"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AccessToken struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
UserID uint64 `gorm:"not null;index" json:"user_id"`
|
||||
|
||||
@@ -73,7 +73,8 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
||||
|
||||
healthHandler := handler.NewHealthHandler(deps.DB)
|
||||
authHandler := handler.NewAuthHandler(deps.Config, authService)
|
||||
userHandler := handler.NewUserHandler()
|
||||
userHandler := handler.NewUserHandler(deps.DB)
|
||||
businessLineHandler := handler.NewBusinessLineHandler(deps.DB)
|
||||
wayenHandler := handler.NewWayenHandler(deps.DB, wayenService, auditService)
|
||||
wayneRoleBindingHandler := handler.NewWayneRoleBindingHandler(wayneRoleBindingService, auditService)
|
||||
clouddmHandler := handler.NewCloudDMHandler(deps.Config, auditService)
|
||||
@@ -100,6 +101,15 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
||||
protected := v1.Group("")
|
||||
protected.Use(handler.AuthMiddleware(deps.Config))
|
||||
protected.GET("/users/me", userHandler.Me)
|
||||
protected.GET("/users", userHandler.List)
|
||||
protected.GET("/business-lines", businessLineHandler.ListCurrentUserBusinessLines)
|
||||
protected.GET("/business-lines/all", businessLineHandler.ListAll)
|
||||
protected.POST("/business-lines", businessLineHandler.Create)
|
||||
protected.PUT("/business-lines/:id", businessLineHandler.Update)
|
||||
protected.DELETE("/business-lines/:id", businessLineHandler.Delete)
|
||||
protected.POST("/business-lines/authorizations", businessLineHandler.GrantPermission)
|
||||
protected.GET("/business-lines/:id/wayne-namespaces", businessLineHandler.ListWayneNamespaces)
|
||||
protected.PUT("/business-lines/:id/wayne-namespaces", businessLineHandler.ReplaceWayneNamespaces)
|
||||
protected.GET("/wayen/login", wayenHandler.Login)
|
||||
protected.GET("/wayen/credential", wayenHandler.GetCredential)
|
||||
protected.PUT("/wayen/credential", wayenHandler.SaveCredential)
|
||||
|
||||
@@ -48,7 +48,7 @@ func NewWayenService(cfg config.Config, db *gorm.DB) *WayenService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WayenService) Login(email, username string) (*WayenLoginResult, error) {
|
||||
func (s *WayenService) Login(email, username, refOverride string) (*WayenLoginResult, error) {
|
||||
email = strings.TrimSpace(email)
|
||||
if email == "" {
|
||||
return nil, ErrWayenEmailMissing
|
||||
@@ -58,7 +58,7 @@ func (s *WayenService) Login(email, username string) (*WayenLoginResult, error)
|
||||
oauthLoginURL = strings.TrimSpace(s.cfg.OAuthRedirectURI)
|
||||
}
|
||||
if oauthLoginURL != "" && strings.TrimSpace(s.cfg.WayenTargetURL) != "" {
|
||||
target, err := s.oauthLoginURL(oauthLoginURL, s.cfg.WayenTargetURL)
|
||||
target, err := s.oauthLoginURL(oauthLoginURL, s.cfg.WayenTargetURL, refOverride)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -111,7 +111,7 @@ func (s *WayenService) Login(email, username string) (*WayenLoginResult, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *WayenService) oauthLoginURL(redirectURI, targetURL string) (string, error) {
|
||||
func (s *WayenService) oauthLoginURL(redirectURI, targetURL, refOverride string) (string, error) {
|
||||
parsed, err := url.Parse(strings.TrimSpace(redirectURI))
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -124,7 +124,7 @@ func (s *WayenService) oauthLoginURL(redirectURI, targetURL string) (string, err
|
||||
next.Path = "/sign-in"
|
||||
}
|
||||
values := next.Query()
|
||||
values.Set("ref", defaultConfigValue(s.cfg.WayenOAuthRef, "/portal/namespace/1/app"))
|
||||
values.Set("ref", defaultConfigValue(refOverride, defaultConfigValue(s.cfg.WayenOAuthRef, "/portal/namespace/1/app")))
|
||||
next.RawQuery = values.Encode()
|
||||
|
||||
query := parsed.Query()
|
||||
|
||||
@@ -15,7 +15,7 @@ func TestWayenLoginUsesDedicatedOAuthLoginURL(t *testing.T) {
|
||||
WayenOAuthRef: "/portal/namespace/1/app",
|
||||
}, nil)
|
||||
|
||||
result, err := service.Login("eastsales@qiniu.com", "eastsales@qiniu.com")
|
||||
result, err := service.Login("eastsales@qiniu.com", "eastsales@qiniu.com", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
@@ -55,7 +55,7 @@ func TestWayenLoginFallsBackToOAuthRedirectURI(t *testing.T) {
|
||||
WayenTargetURL: "http://218.11.5.223:32000/",
|
||||
}, nil)
|
||||
|
||||
result, err := service.Login("eastsales@qiniu.com", "eastsales@qiniu.com")
|
||||
result, err := service.Login("eastsales@qiniu.com", "eastsales@qiniu.com", "")
|
||||
if err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
@@ -67,3 +67,27 @@ func TestWayenLoginFallsBackToOAuthRedirectURI(t *testing.T) {
|
||||
t.Fatalf("target host = %q, want fallback redirect host", parsed.Host)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWayenLoginUsesRefOverride(t *testing.T) {
|
||||
service := NewWayenService(config.Config{
|
||||
WayenOAuthLoginURL: "http://218.11.5.223:32000/login/oauth2/oauth2",
|
||||
WayenTargetURL: "http://218.11.5.223:32000/",
|
||||
WayenOAuthRef: "/portal/namespace/1/app",
|
||||
}, nil)
|
||||
|
||||
result, err := service.Login("eastsales@qiniu.com", "eastsales@qiniu.com", "/portal/namespace/3/app")
|
||||
if err != nil {
|
||||
t.Fatalf("Login() error = %v", err)
|
||||
}
|
||||
parsed, err := url.Parse(result.TargetURL)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid target url: %v", err)
|
||||
}
|
||||
parsedNext, err := url.Parse(parsed.Query().Get("next"))
|
||||
if err != nil {
|
||||
t.Fatalf("invalid next url: %v", err)
|
||||
}
|
||||
if parsedNext.Query().Get("ref") != "/portal/namespace/3/app" {
|
||||
t.Fatalf("next ref = %q", parsedNext.Query().Get("ref"))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user