forked from baron/baron-sso
4단계 역할 정규화 및 dev 권한 스코프 검증 강화
This commit is contained in:
@@ -140,6 +140,127 @@ type clientUpsertRequest struct {
|
||||
Metadata *map[string]interface{} `json:"metadata"`
|
||||
}
|
||||
|
||||
func normalizeUserRole(role string) string {
|
||||
return domain.NormalizeRole(role)
|
||||
}
|
||||
|
||||
func isDevConsoleRoleAllowed(role string) bool {
|
||||
switch normalizeUserRole(role) {
|
||||
case domain.RoleSuperAdmin, domain.RoleTenantAdmin, domain.RoleRPAdmin:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DevHandler) getCurrentProfile(c *fiber.Ctx) *domain.UserProfileResponse {
|
||||
if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil {
|
||||
return profile
|
||||
}
|
||||
if h.Auth != nil {
|
||||
enriched, err := h.Auth.GetEnrichedProfile(c)
|
||||
if err == nil && enriched != nil {
|
||||
c.Locals("user_profile", enriched)
|
||||
return enriched
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tenantIDFromProfile(profile *domain.UserProfileResponse) string {
|
||||
if profile == nil || profile.TenantID == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(*profile.TenantID)
|
||||
}
|
||||
|
||||
func addClientIDToSet(set map[string]struct{}, raw any) {
|
||||
switch value := raw.(type) {
|
||||
case string:
|
||||
for _, chunk := range strings.Split(value, ",") {
|
||||
id := strings.TrimSpace(chunk)
|
||||
if id != "" {
|
||||
set[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
case []string:
|
||||
for _, item := range value {
|
||||
id := strings.TrimSpace(item)
|
||||
if id != "" {
|
||||
set[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, item := range value {
|
||||
if str, ok := item.(string); ok {
|
||||
id := strings.TrimSpace(str)
|
||||
if id != "" {
|
||||
set[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func managedClientIDsFromProfile(profile *domain.UserProfileResponse) map[string]struct{} {
|
||||
ids := make(map[string]struct{})
|
||||
if profile == nil {
|
||||
return ids
|
||||
}
|
||||
|
||||
if profile.RelyingPartyID != nil {
|
||||
if id := strings.TrimSpace(*profile.RelyingPartyID); id != "" {
|
||||
ids[id] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if profile.Metadata == nil {
|
||||
return ids
|
||||
}
|
||||
|
||||
for _, key := range []string{
|
||||
"managed_client_ids",
|
||||
"managedClientIds",
|
||||
"relying_party_id",
|
||||
"relyingPartyId",
|
||||
"client_id",
|
||||
"clientId",
|
||||
} {
|
||||
if raw, ok := profile.Metadata[key]; ok {
|
||||
addClientIDToSet(ids, raw)
|
||||
}
|
||||
}
|
||||
|
||||
return ids
|
||||
}
|
||||
|
||||
func resolveClientTenantID(summary clientSummary) string {
|
||||
if summary.Metadata == nil {
|
||||
return ""
|
||||
}
|
||||
clientTenantID, _ := summary.Metadata["tenant_id"].(string)
|
||||
return strings.TrimSpace(clientTenantID)
|
||||
}
|
||||
|
||||
func isRPAdminClientAllowed(profile *domain.UserProfileResponse, clientID string) bool {
|
||||
if normalizeUserRole(profileRole(profile)) != domain.RoleRPAdmin {
|
||||
return true
|
||||
}
|
||||
allowed := managedClientIDsFromProfile(profile)
|
||||
if len(allowed) == 0 {
|
||||
return false
|
||||
}
|
||||
_, ok := allowed[strings.TrimSpace(clientID)]
|
||||
return ok
|
||||
}
|
||||
|
||||
func profileRole(profile *domain.UserProfileResponse) string {
|
||||
if profile == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(profile.Role)
|
||||
}
|
||||
|
||||
func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
|
||||
profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if (!ok || profile == nil) && h.Auth != nil {
|
||||
@@ -151,11 +272,19 @@ func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
|
||||
}
|
||||
}
|
||||
if ok && profile != nil {
|
||||
// Super Admin bypass
|
||||
if profile.Role == domain.RoleSuperAdmin {
|
||||
role := normalizeUserRole(profile.Role)
|
||||
switch role {
|
||||
case domain.RoleSuperAdmin:
|
||||
slog.Info("Dev private permission granted by super_admin role", "user_id", profile.ID)
|
||||
return true, nil
|
||||
case domain.RoleTenantAdmin, domain.RoleRPAdmin:
|
||||
slog.Info("Dev private permission granted by role", "user_id", profile.ID, "role", role)
|
||||
return true, nil
|
||||
case domain.RoleUser:
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Super Admin bypass
|
||||
if isAdminEmail(profile.Email) {
|
||||
slog.Info("Dev private permission granted by ADMIN_EMAIL match", "email", profile.Email)
|
||||
return true, nil
|
||||
@@ -203,16 +332,24 @@ func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
|
||||
if tokenEmail == "" {
|
||||
tokenEmail = strings.TrimSpace(info.Email)
|
||||
}
|
||||
tokenRole = strings.TrimSpace(info.Role)
|
||||
tokenRole = normalizeUserRole(info.Role)
|
||||
} else if err != nil {
|
||||
slog.Warn("Dev private permission userinfo fallback failed", "error", err)
|
||||
}
|
||||
}
|
||||
tokenRole = normalizeUserRole(tokenRole)
|
||||
|
||||
if tokenRole == domain.RoleSuperAdmin {
|
||||
slog.Info("Dev private permission granted by token role", "role", tokenRole)
|
||||
return true, nil
|
||||
}
|
||||
if tokenRole == domain.RoleTenantAdmin || tokenRole == domain.RoleRPAdmin {
|
||||
slog.Info("Dev private permission granted by token role", "role", tokenRole)
|
||||
return true, nil
|
||||
}
|
||||
if tokenRole == domain.RoleUser {
|
||||
return false, nil
|
||||
}
|
||||
if isAdminEmail(tokenEmail) {
|
||||
slog.Info("Dev private permission granted by token email", "email", tokenEmail)
|
||||
return true, nil
|
||||
@@ -237,7 +374,7 @@ func (h *DevHandler) checkAppManagerPermission(c *fiber.Ctx) (bool, error) {
|
||||
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 {
|
||||
if rawRole, ok := identity.Traits["role"].(string); ok && normalizeUserRole(rawRole) == domain.RoleSuperAdmin {
|
||||
slog.Info("Dev private permission granted by Kratos role", "subject", tokenSubject)
|
||||
return true, nil
|
||||
}
|
||||
@@ -383,90 +520,20 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
// [Tenant Isolation] Get current user's tenant ID
|
||||
userTenantID := ""
|
||||
isSuperAdmin := false
|
||||
profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if (!ok || profile == nil) && h.Auth != nil {
|
||||
enriched, _ := h.Auth.GetEnrichedProfile(c)
|
||||
if enriched != nil {
|
||||
profile = enriched
|
||||
ok = true
|
||||
c.Locals("user_profile", enriched)
|
||||
}
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
role := normalizeUserRole(profile.Role)
|
||||
if !isDevConsoleRoleAllowed(role) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
if ok && profile != nil {
|
||||
if profile.TenantID != nil {
|
||||
userTenantID = *profile.TenantID
|
||||
}
|
||||
isSuperAdmin = profile.Role == domain.RoleSuperAdmin
|
||||
} else {
|
||||
// If profile resolution failed, verify bearer token via OIDC userinfo fallback.
|
||||
authHeader := c.Get("Authorization")
|
||||
bearerToken := extractBearerToken(authHeader)
|
||||
if bearerToken == "" {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
|
||||
sub, email := extractAuthClaimsFromBearer(authHeader)
|
||||
if sub == "" {
|
||||
info, infoErr := h.fetchOIDCUserInfo(c.Context(), bearerToken)
|
||||
if infoErr != nil {
|
||||
slog.Warn("ListClients userinfo fallback failed", "error", infoErr)
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
sub = strings.TrimSpace(info.Sub)
|
||||
if email == "" {
|
||||
email = strings.TrimSpace(info.Email)
|
||||
}
|
||||
if userTenantID == "" {
|
||||
userTenantID = strings.TrimSpace(info.TenantID)
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(info.Role), domain.RoleSuperAdmin) {
|
||||
isSuperAdmin = true
|
||||
}
|
||||
}
|
||||
|
||||
if sub == "" && email == "" {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
|
||||
if h.KratosAdmin != nil && (userTenantID == "" || !isSuperAdmin) {
|
||||
identityID := strings.TrimSpace(sub)
|
||||
if identityID == "" && email != "" {
|
||||
if resolved, err := h.KratosAdmin.FindIdentityIDByIdentifier(c.Context(), email); err == nil {
|
||||
identityID = strings.TrimSpace(resolved)
|
||||
}
|
||||
}
|
||||
if identityID != "" {
|
||||
if identity, err := h.KratosAdmin.GetIdentity(c.Context(), identityID); err == nil && identity != nil {
|
||||
if userTenantID == "" {
|
||||
if tid, ok := identity.Traits["tenant_id"].(string); ok {
|
||||
userTenantID = strings.TrimSpace(tid)
|
||||
}
|
||||
}
|
||||
role := ""
|
||||
if rawRole, ok := identity.Traits["role"].(string); ok {
|
||||
role = strings.TrimSpace(rawRole)
|
||||
}
|
||||
if role == domain.RoleSuperAdmin {
|
||||
isSuperAdmin = true
|
||||
}
|
||||
profile = &domain.UserProfileResponse{
|
||||
ID: identityID,
|
||||
Email: email,
|
||||
Role: role,
|
||||
TenantID: nil,
|
||||
}
|
||||
if userTenantID != "" {
|
||||
tid := userTenantID
|
||||
profile.TenantID = &tid
|
||||
}
|
||||
c.Locals("user_profile", profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
userTenantID := tenantIDFromProfile(profile)
|
||||
isSuperAdmin := role == domain.RoleSuperAdmin
|
||||
allowedClientIDs := managedClientIDsFromProfile(profile)
|
||||
if role == domain.RoleRPAdmin && len(allowedClientIDs) == 0 {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin has no managed clients")
|
||||
}
|
||||
|
||||
isAppManager, err := h.checkAppManagerPermission(c)
|
||||
@@ -503,6 +570,13 @@ func (h *DevHandler) ListClients(c *fiber.Ctx) error {
|
||||
}
|
||||
}
|
||||
|
||||
// 3. [Role Scope] RP Admin can only access managed RP IDs
|
||||
if role == domain.RoleRPAdmin {
|
||||
if _, ok := allowedClientIDs[summary.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
items = append(items, summary)
|
||||
}
|
||||
|
||||
@@ -529,22 +603,27 @@ func (h *DevHandler) GetClient(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
summary := h.mapClientSummary(*client)
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
role := normalizeUserRole(profile.Role)
|
||||
if !isDevConsoleRoleAllowed(role) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
// [Tenant Isolation] Check if user has access to this client
|
||||
isSuperAdmin := false
|
||||
userTenantID := ""
|
||||
if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil {
|
||||
isSuperAdmin = profile.Role == domain.RoleSuperAdmin
|
||||
if profile.TenantID != nil {
|
||||
userTenantID = *profile.TenantID
|
||||
}
|
||||
}
|
||||
isSuperAdmin := role == domain.RoleSuperAdmin
|
||||
userTenantID := tenantIDFromProfile(profile)
|
||||
if !isSuperAdmin {
|
||||
clientTenantID, _ := summary.Metadata["tenant_id"].(string)
|
||||
clientTenantID := resolveClientTenantID(summary)
|
||||
if clientTenantID != userTenantID {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: access denied to client in another tenant")
|
||||
}
|
||||
}
|
||||
if !isRPAdminClientAllowed(profile, summary.ID) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
|
||||
}
|
||||
|
||||
// Check permission for private clients
|
||||
if summary.Type == "private" {
|
||||
@@ -598,22 +677,27 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
summary := h.mapClientSummary(*current)
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
role := normalizeUserRole(profile.Role)
|
||||
if !isDevConsoleRoleAllowed(role) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
// [Tenant Isolation]
|
||||
isSuperAdmin := false
|
||||
userTenantID := ""
|
||||
if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil {
|
||||
isSuperAdmin = profile.Role == domain.RoleSuperAdmin
|
||||
if profile.TenantID != nil {
|
||||
userTenantID = *profile.TenantID
|
||||
}
|
||||
}
|
||||
isSuperAdmin := role == domain.RoleSuperAdmin
|
||||
userTenantID := tenantIDFromProfile(profile)
|
||||
if !isSuperAdmin {
|
||||
clientTenantID, _ := summary.Metadata["tenant_id"].(string)
|
||||
clientTenantID := resolveClientTenantID(summary)
|
||||
if clientTenantID != userTenantID {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: access denied to client in another tenant")
|
||||
}
|
||||
}
|
||||
if !isRPAdminClientAllowed(profile, summary.ID) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
|
||||
}
|
||||
|
||||
if summary.Type == "private" {
|
||||
isAppManager, _ := h.checkAppManagerPermission(c)
|
||||
@@ -655,6 +739,15 @@ func (h *DevHandler) UpdateClientStatus(c *fiber.Ctx) error {
|
||||
|
||||
func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
||||
tenantID := h.injectTenantContextFromHeader(c)
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
role := normalizeUserRole(profile.Role)
|
||||
if !isDevConsoleRoleAllowed(role) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
var req clientUpsertRequest
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "invalid request body")
|
||||
@@ -706,7 +799,7 @@ func (h *DevHandler) CreateClient(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
// [Tenant Isolation] Record owner information
|
||||
if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil {
|
||||
if profile != nil {
|
||||
metadata["user_id"] = profile.ID
|
||||
if tenantID == "" && profile.TenantID != nil {
|
||||
tenantID = *profile.TenantID
|
||||
@@ -805,22 +898,27 @@ func (h *DevHandler) UpdateClient(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
currentSummary := h.mapClientSummary(*current)
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
role := normalizeUserRole(profile.Role)
|
||||
if !isDevConsoleRoleAllowed(role) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
// [Tenant Isolation]
|
||||
isSuperAdmin := false
|
||||
userTenantID := ""
|
||||
if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil {
|
||||
isSuperAdmin = profile.Role == domain.RoleSuperAdmin
|
||||
if profile.TenantID != nil {
|
||||
userTenantID = *profile.TenantID
|
||||
}
|
||||
}
|
||||
isSuperAdmin := role == domain.RoleSuperAdmin
|
||||
userTenantID := tenantIDFromProfile(profile)
|
||||
if !isSuperAdmin {
|
||||
clientTenantID, _ := currentSummary.Metadata["tenant_id"].(string)
|
||||
clientTenantID := resolveClientTenantID(currentSummary)
|
||||
if clientTenantID != userTenantID {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: access denied to client in another tenant")
|
||||
}
|
||||
}
|
||||
if !isRPAdminClientAllowed(profile, currentSummary.ID) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
|
||||
}
|
||||
|
||||
clientType := ""
|
||||
if req.Type != nil {
|
||||
@@ -931,22 +1029,27 @@ func (h *DevHandler) DeleteClient(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
summary := h.mapClientSummary(*current)
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
role := normalizeUserRole(profile.Role)
|
||||
if !isDevConsoleRoleAllowed(role) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
// [Tenant Isolation]
|
||||
isSuperAdmin := false
|
||||
userTenantID := ""
|
||||
if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil {
|
||||
isSuperAdmin = profile.Role == domain.RoleSuperAdmin
|
||||
if profile.TenantID != nil {
|
||||
userTenantID = *profile.TenantID
|
||||
}
|
||||
}
|
||||
isSuperAdmin := role == domain.RoleSuperAdmin
|
||||
userTenantID := tenantIDFromProfile(profile)
|
||||
if !isSuperAdmin {
|
||||
clientTenantID, _ := summary.Metadata["tenant_id"].(string)
|
||||
clientTenantID := resolveClientTenantID(summary)
|
||||
if clientTenantID != userTenantID {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: access denied to client in another tenant")
|
||||
}
|
||||
}
|
||||
if !isRPAdminClientAllowed(profile, summary.ID) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
|
||||
}
|
||||
|
||||
// [Security] Check permission for private clients
|
||||
if summary.Type == "private" {
|
||||
@@ -986,6 +1089,18 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "client_id is required")
|
||||
}
|
||||
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
role := normalizeUserRole(profile.Role)
|
||||
if !isDevConsoleRoleAllowed(role) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
if !isRPAdminClientAllowed(profile, clientID) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
|
||||
}
|
||||
|
||||
subject := strings.TrimSpace(c.Query("subject"))
|
||||
limit := c.QueryInt("limit", 50)
|
||||
offset := c.QueryInt("offset", 0)
|
||||
@@ -995,8 +1110,8 @@ func (h *DevHandler) ListConsents(c *fiber.Ctx) error {
|
||||
|
||||
// [Isolation] Get admin tenant ID from locals or header
|
||||
adminTenantID := ""
|
||||
if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil {
|
||||
if profile.Role != domain.RoleSuperAdmin && profile.TenantID != nil {
|
||||
if profile != nil {
|
||||
if role != domain.RoleSuperAdmin && profile.TenantID != nil {
|
||||
adminTenantID = *profile.TenantID
|
||||
}
|
||||
}
|
||||
@@ -1091,6 +1206,17 @@ func (h *DevHandler) RevokeConsents(c *fiber.Ctx) error {
|
||||
return errorJSON(c, fiber.StatusBadRequest, "subject is required")
|
||||
}
|
||||
clientID := strings.TrimSpace(c.Query("client_id"))
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
role := normalizeUserRole(profile.Role)
|
||||
if !isDevConsoleRoleAllowed(role) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
if clientID != "" && !isRPAdminClientAllowed(profile, clientID) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
|
||||
}
|
||||
|
||||
// If subject is not a UUID, try to resolve it as an identifier (email/username)
|
||||
if _, err := uuid.Parse(subject); err != nil {
|
||||
@@ -1138,22 +1264,27 @@ func (h *DevHandler) RotateClientSecret(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
summary := h.mapClientSummary(*current)
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
role := normalizeUserRole(profile.Role)
|
||||
if !isDevConsoleRoleAllowed(role) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
|
||||
// [Tenant Isolation]
|
||||
isSuperAdmin := false
|
||||
userTenantID := ""
|
||||
if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil {
|
||||
isSuperAdmin = profile.Role == domain.RoleSuperAdmin
|
||||
if profile.TenantID != nil {
|
||||
userTenantID = *profile.TenantID
|
||||
}
|
||||
}
|
||||
isSuperAdmin := role == domain.RoleSuperAdmin
|
||||
userTenantID := tenantIDFromProfile(profile)
|
||||
if !isSuperAdmin {
|
||||
clientTenantID, _ := summary.Metadata["tenant_id"].(string)
|
||||
clientTenantID := resolveClientTenantID(summary)
|
||||
if clientTenantID != userTenantID {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: access denied to client in another tenant")
|
||||
}
|
||||
}
|
||||
if !isRPAdminClientAllowed(profile, summary.ID) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin scope does not include this client")
|
||||
}
|
||||
|
||||
// [Security] Check permission for private clients
|
||||
if summary.Type == "private" {
|
||||
@@ -1216,13 +1347,18 @@ func (h *DevHandler) ListAuditLogs(c *fiber.Ctx) error {
|
||||
}
|
||||
|
||||
h.injectTenantContextFromHeader(c)
|
||||
allowed, err := h.checkAppManagerPermission(c)
|
||||
if err != nil {
|
||||
return errorJSON(c, fiber.StatusInternalServerError, "permission check error")
|
||||
profile := h.getCurrentProfile(c)
|
||||
if profile == nil {
|
||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||
}
|
||||
if !allowed {
|
||||
role := normalizeUserRole(profile.Role)
|
||||
if !isDevConsoleRoleAllowed(role) {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
||||
}
|
||||
allowedClientIDs := managedClientIDsFromProfile(profile)
|
||||
if role == domain.RoleRPAdmin && len(allowedClientIDs) == 0 {
|
||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: rp_admin has no managed clients")
|
||||
}
|
||||
|
||||
limit := c.QueryInt("limit", 50)
|
||||
if limit <= 0 {
|
||||
@@ -1239,6 +1375,9 @@ func (h *DevHandler) ListAuditLogs(c *fiber.Ctx) error {
|
||||
if tenantFilter == "" {
|
||||
tenantFilter = h.resolveDevTenantScope(c)
|
||||
}
|
||||
if role != domain.RoleSuperAdmin && tenantFilter == "" {
|
||||
tenantFilter = tenantIDFromProfile(profile)
|
||||
}
|
||||
|
||||
cursorRaw := c.Query("cursor")
|
||||
cursor, err := parseAuditCursor(cursorRaw)
|
||||
@@ -1263,7 +1402,7 @@ func (h *DevHandler) ListAuditLogs(c *fiber.Ctx) error {
|
||||
|
||||
for _, logItem := range page {
|
||||
scanned++
|
||||
if h.matchesDevAuditFilter(logItem, tenantFilter, clientFilter, actionFilter, statusFilter) {
|
||||
if h.matchesDevAuditFilter(logItem, tenantFilter, clientFilter, actionFilter, statusFilter, allowedClientIDs) {
|
||||
collected = append(collected, logItem)
|
||||
if len(collected) == limit+1 {
|
||||
break
|
||||
@@ -1308,7 +1447,7 @@ func (h *DevHandler) GetStats(c *fiber.Ctx) error {
|
||||
userTenantID := ""
|
||||
isSuperAdmin := false
|
||||
if profile, ok := c.Locals("user_profile").(*domain.UserProfileResponse); ok && profile != nil {
|
||||
isSuperAdmin = profile.Role == domain.RoleSuperAdmin
|
||||
isSuperAdmin = normalizeUserRole(profile.Role) == domain.RoleSuperAdmin
|
||||
if profile.TenantID != nil {
|
||||
userTenantID = *profile.TenantID
|
||||
}
|
||||
@@ -1553,6 +1692,7 @@ func clientTypeOrDefault(tokenEndpointAuthMethod string) string {
|
||||
func (h *DevHandler) matchesDevAuditFilter(
|
||||
logItem domain.AuditLog,
|
||||
tenantFilter, clientFilter, actionFilter, statusFilter string,
|
||||
allowedClientIDs map[string]struct{},
|
||||
) bool {
|
||||
if !strings.Contains(logItem.EventType, "/api/v1/dev/") {
|
||||
return false
|
||||
@@ -1574,6 +1714,20 @@ func (h *DevHandler) matchesDevAuditFilter(
|
||||
return false
|
||||
}
|
||||
}
|
||||
if len(allowedClientIDs) > 0 {
|
||||
targetID, _ := details["target_id"].(string)
|
||||
clientID, _ := details["client_id"].(string)
|
||||
resolvedID := strings.TrimSpace(targetID)
|
||||
if resolvedID == "" {
|
||||
resolvedID = strings.TrimSpace(clientID)
|
||||
}
|
||||
if resolvedID == "" {
|
||||
return false
|
||||
}
|
||||
if _, ok := allowedClientIDs[resolvedID]; !ok {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if actionFilter != "" {
|
||||
if normalizeAuditAction(logItem.EventType, details) != actionFilter {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user