forked from baron/baron-sso
PRIVATE 클라이언트 권한 판정 로직 수정
This commit is contained in:
@@ -7,9 +7,11 @@ import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -25,9 +27,28 @@ type DevHandler struct {
|
||||
ConsentRepo repository.ClientConsentRepository
|
||||
Keto service.KetoService
|
||||
RPSvc service.RelyingPartyService
|
||||
Auth interface {
|
||||
GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
|
||||
}
|
||||
}
|
||||
|
||||
func NewDevHandler(redis domain.RedisRepository, secretRepo domain.ClientSecretRepository, consentRepo repository.ClientConsentRepository, rpSvc service.RelyingPartyService, keto service.KetoService) *DevHandler {
|
||||
func NewDevHandler(
|
||||
redis domain.RedisRepository,
|
||||
secretRepo domain.ClientSecretRepository,
|
||||
consentRepo repository.ClientConsentRepository,
|
||||
rpSvc service.RelyingPartyService,
|
||||
keto service.KetoService,
|
||||
auth ...interface {
|
||||
GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
|
||||
},
|
||||
) *DevHandler {
|
||||
var authProvider interface {
|
||||
GetEnrichedProfile(c *fiber.Ctx) (*domain.UserProfileResponse, error)
|
||||
}
|
||||
if len(auth) > 0 {
|
||||
authProvider = auth[0]
|
||||
}
|
||||
|
||||
return &DevHandler{
|
||||
Hydra: service.NewHydraAdminService(),
|
||||
Redis: redis,
|
||||
@@ -36,6 +57,7 @@ func NewDevHandler(redis domain.RedisRepository, secretRepo domain.ClientSecretR
|
||||
ConsentRepo: consentRepo,
|
||||
Keto: keto,
|
||||
RPSvc: rpSvc,
|
||||
Auth: authProvider,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,24 +123,140 @@ type clientUpsertRequest struct {
|
||||
|
||||
func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
|
||||
profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if !ok || profile == nil {
|
||||
if (!ok || profile == nil) && h.Auth != nil {
|
||||
enriched, err := h.Auth.GetEnrichedProfile(c)
|
||||
if err == nil && enriched != nil {
|
||||
profile = enriched
|
||||
ok = true
|
||||
c.Locals("user_profile", enriched)
|
||||
}
|
||||
}
|
||||
if ok && profile != nil {
|
||||
// Super Admin bypass
|
||||
if profile.Role == domain.RoleSuperAdmin {
|
||||
slog.Info("Dev private permission granted by super_admin role", "user_id", profile.ID)
|
||||
return true, nil
|
||||
}
|
||||
if isAdminEmail(profile.Email) {
|
||||
slog.Info("Dev private permission granted by ADMIN_EMAIL match", "email", profile.Email)
|
||||
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
|
||||
}
|
||||
slog.Info("Dev private permission evaluated by Keto", "user_id", profile.ID, "allowed", allowed)
|
||||
|
||||
return allowed, nil
|
||||
}
|
||||
|
||||
tokenSubject, tokenEmail := extractAuthClaimsFromBearer(c.Get("Authorization"))
|
||||
if isAdminEmail(tokenEmail) {
|
||||
slog.Info("Dev private permission granted by token email", "email", tokenEmail)
|
||||
return true, nil
|
||||
}
|
||||
if tokenSubject == "" {
|
||||
if isTrustedLocalDevfrontRequest(c) {
|
||||
// Local devfront fallback: allow localhost developer flow even if auth context is missing.
|
||||
slog.Warn("Dev private permission fallback granted for trusted local devfront request", "path", c.Path(), "origin", c.Get("Origin"))
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Super Admin bypass
|
||||
if profile.Role == domain.RoleSuperAdmin {
|
||||
return true, nil
|
||||
// Fallback: resolve role from Kratos identity traits when user_profile is not injected.
|
||||
if h.KratosAdmin != nil {
|
||||
identity, err := h.KratosAdmin.GetIdentity(c.Context(), tokenSubject)
|
||||
if err == nil && identity != nil {
|
||||
if rawRole, ok := identity.Traits["role"].(string); ok && rawRole == domain.RoleSuperAdmin {
|
||||
slog.Info("Dev private permission granted by Kratos role", "subject", tokenSubject)
|
||||
return true, nil
|
||||
}
|
||||
if email, ok := identity.Traits["email"].(string); ok && isAdminEmail(email) {
|
||||
slog.Info("Dev private permission granted by Kratos email", "subject", tokenSubject, "email", email)
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check with Keto: System:AppManager#member
|
||||
allowed, err := h.Keto.CheckPermission(c.Context(), profile.ID, "System", "AppManager", "member")
|
||||
allowed, err := h.Keto.CheckPermission(c.Context(), tokenSubject, "System", "AppManager", "member")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
slog.Info("Dev private permission evaluated by Keto(subject)", "subject", tokenSubject, "allowed", allowed)
|
||||
|
||||
return allowed, nil
|
||||
}
|
||||
|
||||
func extractAuthClaimsFromBearer(authHeader string) (string, string) {
|
||||
authHeader = strings.TrimSpace(authHeader)
|
||||
if !strings.HasPrefix(strings.ToLower(authHeader), "bearer ") {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(authHeader[len("Bearer "):])
|
||||
if token == "" || strings.Count(token, ".") != 2 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 3 {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
payload, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
payload, err = base64.URLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
|
||||
var claims map[string]interface{}
|
||||
if err := json.Unmarshal(payload, &claims); err != nil {
|
||||
return "", ""
|
||||
}
|
||||
sub := ""
|
||||
if sub, ok := claims["sub"].(string); ok {
|
||||
sub = strings.TrimSpace(sub)
|
||||
}
|
||||
email := ""
|
||||
if claimEmail, ok := claims["email"].(string); ok {
|
||||
email = strings.TrimSpace(claimEmail)
|
||||
}
|
||||
|
||||
return sub, email
|
||||
}
|
||||
|
||||
func isAdminEmail(email string) bool {
|
||||
adminEmail := strings.TrimSpace(os.Getenv("ADMIN_EMAIL"))
|
||||
return adminEmail != "" && strings.EqualFold(strings.TrimSpace(email), adminEmail)
|
||||
}
|
||||
|
||||
func isTrustedLocalDevfrontRequest(c *fiber.Ctx) bool {
|
||||
if c == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
origin := strings.ToLower(strings.TrimSpace(c.Get("Origin")))
|
||||
referer := strings.ToLower(strings.TrimSpace(c.Get("Referer")))
|
||||
allowedPrefixes := []string{
|
||||
"http://localhost:5174",
|
||||
"https://localhost:5174",
|
||||
}
|
||||
|
||||
for _, prefix := range allowedPrefixes {
|
||||
if strings.HasPrefix(origin, prefix) || strings.HasPrefix(referer, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
||||
limit := c.QueryInt("limit", 50)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
|
||||
Reference in New Issue
Block a user