forked from baron/baron-sso
Private/PKCE 앱 유형 및 관리자 권한 정책 적용
This commit is contained in:
@@ -9,6 +9,7 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -22,15 +23,19 @@ type DevHandler struct {
|
||||
SecretRepo domain.ClientSecretRepository
|
||||
KratosAdmin *service.KratosAdminService
|
||||
ConsentRepo repository.ClientConsentRepository
|
||||
Keto service.KetoService
|
||||
RPSvc service.RelyingPartyService
|
||||
}
|
||||
|
||||
func NewDevHandler(redis domain.RedisRepository, secretRepo domain.ClientSecretRepository, consentRepo repository.ClientConsentRepository, rpSvc service.RelyingPartyService) *DevHandler {
|
||||
func NewDevHandler(redis domain.RedisRepository, secretRepo domain.ClientSecretRepository, consentRepo repository.ClientConsentRepository, rpSvc service.RelyingPartyService, keto service.KetoService) *DevHandler {
|
||||
return &DevHandler{
|
||||
Hydra: service.NewHydraAdminService(),
|
||||
Redis: redis,
|
||||
SecretRepo: secretRepo,
|
||||
KratosAdmin: service.NewKratosAdminService(),
|
||||
ConsentRepo: consentRepo,
|
||||
Keto: keto,
|
||||
RPSvc: rpSvc,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +99,26 @@ type clientUpsertRequest struct {
|
||||
Metadata *map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
|
||||
profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if !ok || profile == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Super Admin bypass
|
||||
if profile.Role == domain.RoleSuperAdmin {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Check with Keto: System:AppManager#member
|
||||
allowed, err := h.Keto.CheckPermission(c.Context(), profile.ID, "System", "AppManager", "member")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return allowed, nil
|
||||
}
|
||||
|
||||
func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
||||
limit := c.QueryInt("limit", 50)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
@@ -104,6 +129,11 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
isAppManager, err := h.checkAppManagerPermission(c)
|
||||
if err != nil {
|
||||
slog.Error("Failed to check app manager permission", "error", err)
|
||||
}
|
||||
|
||||
clients, err := h.Hydra.ListClients(c.Context(), limit, offset)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrHydraNotFound) {
|
||||
@@ -120,7 +150,12 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
||||
|
||||
items := make([]clientSummary, 0, len(clients))
|
||||
for _, client := range clients {
|
||||
items = append(items, h.mapClientSummary(client))
|
||||
summary := h.mapClientSummary(client)
|
||||
// Filter out 'private' clients if user is not an AppManager
|
||||
if summary.Type == "private" && !isAppManager {
|
||||
continue
|
||||
}
|
||||
items = append(items, summary)
|
||||
}
|
||||
|
||||
return c.JSON(clientListResponse{
|
||||
@@ -145,6 +180,18 @@ func (h *DevHandler) GetClient(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
summary := h.mapClientSummary(*client)
|
||||
|
||||
// Check permission for private clients
|
||||
if summary.Type == "private" {
|
||||
isAppManager, err := h.checkAppManagerPermission(c)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
|
||||
}
|
||||
if !isAppManager {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(clientDetailResponse{
|
||||
Client: summary,
|
||||
Endpoints: clientEndpoints{
|
||||
@@ -175,6 +222,18 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "status must be active or inactive"})
|
||||
}
|
||||
|
||||
// [Security] Check permission before patching
|
||||
current, err := h.Hydra.GetClient(c.Context(), clientID)
|
||||
if err == nil {
|
||||
summary := h.mapClientSummary(*current)
|
||||
if summary.Type == "private" {
|
||||
isAppManager, _ := h.checkAppManagerPermission(c)
|
||||
if !isAppManager {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updated, err := h.Hydra.PatchClientStatus(c.Context(), clientID, status)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrHydraNotFound) {
|
||||
@@ -221,9 +280,20 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
||||
grantTypes := derefSlice(req.GrantTypes, defaultGrantTypes())
|
||||
responseTypes := derefSlice(req.ResponseTypes, defaultResponseTypes())
|
||||
|
||||
clientType := strings.ToLower(strings.TrimSpace(valueOr(req.Type, "confidential")))
|
||||
if clientType != "public" && clientType != "confidential" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be public or confidential"})
|
||||
clientType := strings.ToLower(strings.TrimSpace(valueOr(req.Type, "private")))
|
||||
if clientType != "pkce" && clientType != "private" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be pkce or private"})
|
||||
}
|
||||
|
||||
// [Security] Check permission for private clients
|
||||
if clientType == "private" {
|
||||
isAppManager, err := h.checkAppManagerPermission(c)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
|
||||
}
|
||||
if !isAppManager {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions to create private client"})
|
||||
}
|
||||
}
|
||||
|
||||
status := strings.ToLower(strings.TrimSpace(valueOr(req.Status, "active")))
|
||||
@@ -239,7 +309,7 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
||||
|
||||
tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
|
||||
if tokenAuthMethod == "" {
|
||||
if clientType == "public" {
|
||||
if clientType == "pkce" {
|
||||
tokenAuthMethod = "none"
|
||||
} else {
|
||||
tokenAuthMethod = "client_secret_basic"
|
||||
@@ -310,8 +380,20 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
||||
clientType := ""
|
||||
if req.Type != nil {
|
||||
clientType = strings.ToLower(strings.TrimSpace(*req.Type))
|
||||
if clientType != "public" && clientType != "confidential" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be public or confidential"})
|
||||
if clientType != "pkce" && clientType != "private" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "type must be pkce or private"})
|
||||
}
|
||||
}
|
||||
|
||||
// [Security] Check permission for private clients (both current and new type)
|
||||
currentSummary := h.mapClientSummary(*current)
|
||||
if currentSummary.Type == "private" || clientType == "private" {
|
||||
isAppManager, err := h.checkAppManagerPermission(c)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "permission check error"})
|
||||
}
|
||||
if !isAppManager {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,7 +407,7 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
||||
|
||||
tokenAuthMethod := strings.TrimSpace(valueOr(req.TokenEndpointAuthMethod, ""))
|
||||
if tokenAuthMethod == "" && clientType != "" {
|
||||
if clientType == "public" {
|
||||
if clientType == "pkce" {
|
||||
tokenAuthMethod = "none"
|
||||
} else {
|
||||
tokenAuthMethod = "client_secret_basic"
|
||||
@@ -382,6 +464,18 @@ func (h *DevHandler) DeleteClient(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
|
||||
}
|
||||
|
||||
// [Security] Check permission for private clients
|
||||
current, err := h.Hydra.GetClient(c.Context(), clientID)
|
||||
if err == nil {
|
||||
summary := h.mapClientSummary(*current)
|
||||
if summary.Type == "private" {
|
||||
isAppManager, _ := h.checkAppManagerPermission(c)
|
||||
if !isAppManager {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.Hydra.DeleteClient(c.Context(), clientID); err != nil {
|
||||
if errors.Is(err, service.ErrHydraNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
|
||||
@@ -517,14 +611,25 @@ func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "client id is required"})
|
||||
}
|
||||
|
||||
// [Security] Check permission for private clients
|
||||
current, err := h.Hydra.GetClient(c.Context(), clientID)
|
||||
if err == nil {
|
||||
summary := h.mapClientSummary(*current)
|
||||
if summary.Type == "private" {
|
||||
isAppManager, _ := h.checkAppManagerPermission(c)
|
||||
if !isAppManager {
|
||||
return c.Status(fiber.StatusForbidden).JSON(fiber.Map{"error": "forbidden: insufficient permissions for private client"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Generate new secret
|
||||
newSecret, err := generateRandomSecret(20)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "failed to generate secret"})
|
||||
}
|
||||
|
||||
// 2. Get current client to preserve other fields
|
||||
current, err := h.Hydra.GetClient(c.Context(), clientID)
|
||||
// 2. Get current client to preserve other fields (already fetched above)
|
||||
if err != nil {
|
||||
if errors.Is(err, service.ErrHydraNotFound) {
|
||||
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{"error": "client not found"})
|
||||
@@ -584,9 +689,9 @@ func (h *DevHandler) mapClientSummary(client domain.HydraClient) clientSummary {
|
||||
}
|
||||
}
|
||||
|
||||
clientType := "confidential"
|
||||
clientType := "private"
|
||||
if strings.EqualFold(client.TokenEndpointAuthMethod, "none") {
|
||||
clientType = "public"
|
||||
clientType = "pkce"
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(client.ClientName)
|
||||
|
||||
Reference in New Issue
Block a user