forked from baron/baron-sso
chore: consolidate local integration changes
This commit is contained in:
@@ -11,22 +11,44 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type adminHydraClientLister interface {
|
||||
ListClients(ctx context.Context, limit, offset int) ([]domain.HydraClient, error)
|
||||
}
|
||||
|
||||
type identityCacheAdmin interface {
|
||||
GetIdentityCacheStatus(ctx context.Context) (domain.IdentityCacheStatus, error)
|
||||
FlushIdentityCache(ctx context.Context) (domain.IdentityCacheFlushResult, error)
|
||||
}
|
||||
|
||||
type AdminHandler struct {
|
||||
Keto service.KetoService
|
||||
KetoOutbox repository.KetoOutboxRepository
|
||||
RPUsageQueries domain.RPUsageQueryRepository
|
||||
TenantRepo repository.TenantRepository
|
||||
Hydra adminHydraClientLister
|
||||
AuditRepo domain.AuditRepository
|
||||
UserProjectionRepo repository.UserProjectionRepository
|
||||
UserProjectionSyncer service.UserProjectionReconciler
|
||||
IntegrityChecker repository.DataIntegrityChecker
|
||||
DB *gorm.DB
|
||||
Keto service.KetoService
|
||||
KetoOutbox repository.KetoOutboxRepository
|
||||
RPUsageQueries domain.RPUsageQueryRepository
|
||||
TenantRepo repository.TenantRepository
|
||||
Hydra adminHydraClientLister
|
||||
AuditRepo domain.AuditRepository
|
||||
UserProjectionRepo repository.UserProjectionRepository
|
||||
IdentityCache identityCacheAdmin
|
||||
IntegrityChecker repository.DataIntegrityChecker
|
||||
}
|
||||
|
||||
const globalCustomClaimsSettingKey = "global_custom_claim_definitions"
|
||||
|
||||
type globalCustomClaimDefinition struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
ValueType string `json:"valueType"`
|
||||
ReadPermission string `json:"readPermission"`
|
||||
WritePermission string `json:"writePermission"`
|
||||
Description string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type globalCustomClaimDefinitionsResponse struct {
|
||||
Items []globalCustomClaimDefinition `json:"items"`
|
||||
}
|
||||
|
||||
func NewAdminHandler(keto service.KetoService, ketoOutbox repository.KetoOutboxRepository) *AdminHandler {
|
||||
@@ -110,6 +132,154 @@ func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "ok"})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) GetGlobalCustomClaimDefinitions(c *fiber.Ctx) error {
|
||||
if h == nil || h.DB == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
||||
"error": "settings store unavailable",
|
||||
})
|
||||
}
|
||||
|
||||
var setting domain.SystemSetting
|
||||
if err := h.DB.WithContext(c.Context()).First(&setting, "key = ?", globalCustomClaimsSettingKey).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return c.JSON(globalCustomClaimDefinitionsResponse{Items: []globalCustomClaimDefinition{}})
|
||||
}
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(globalCustomClaimDefinitionsResponse{
|
||||
Items: normalizeGlobalCustomClaimDefinitions(setting.Value["items"]),
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) UpdateGlobalCustomClaimDefinitions(c *fiber.Ctx) error {
|
||||
if h == nil || h.DB == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{
|
||||
"error": "settings store unavailable",
|
||||
})
|
||||
}
|
||||
|
||||
var req globalCustomClaimDefinitionsResponse
|
||||
if err := c.BodyParser(&req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "invalid request body"})
|
||||
}
|
||||
items, err := validateGlobalCustomClaimDefinitions(req.Items)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
setting := domain.SystemSetting{
|
||||
Key: globalCustomClaimsSettingKey,
|
||||
Value: domain.JSONMap{"items": globalCustomClaimDefinitionsToJSON(items)},
|
||||
}
|
||||
if err := h.DB.WithContext(c.Context()).Save(&setting).Error; err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(globalCustomClaimDefinitionsResponse{Items: items})
|
||||
}
|
||||
|
||||
func normalizeGlobalCustomClaimDefinitions(value any) []globalCustomClaimDefinition {
|
||||
rawItems, ok := value.([]any)
|
||||
if !ok {
|
||||
return []globalCustomClaimDefinition{}
|
||||
}
|
||||
items := make([]globalCustomClaimDefinition, 0, len(rawItems))
|
||||
for _, item := range rawItems {
|
||||
raw, ok := item.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
def := globalCustomClaimDefinition{
|
||||
Key: strings.TrimSpace(stringValue(raw["key"])),
|
||||
Label: strings.TrimSpace(stringValue(raw["label"])),
|
||||
ValueType: normalizeGlobalCustomClaimType(stringValue(raw["valueType"])),
|
||||
ReadPermission: adminNormalizeCustomClaimPermission(stringValue(raw["readPermission"])),
|
||||
WritePermission: adminNormalizeCustomClaimPermission(stringValue(raw["writePermission"])),
|
||||
Description: strings.TrimSpace(stringValue(raw["description"])),
|
||||
}
|
||||
if def.Key != "" {
|
||||
items = append(items, def)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func validateGlobalCustomClaimDefinitions(items []globalCustomClaimDefinition) ([]globalCustomClaimDefinition, error) {
|
||||
seen := map[string]struct{}{}
|
||||
normalized := make([]globalCustomClaimDefinition, 0, len(items))
|
||||
for _, item := range items {
|
||||
key := strings.TrimSpace(item.Key)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if !isValidCustomClaimKey(key) {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "claim key must use letters, numbers, underscore, dot, or hyphen")
|
||||
}
|
||||
if _, exists := seen[key]; exists {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "duplicate claim key: "+key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
normalized = append(normalized, globalCustomClaimDefinition{
|
||||
Key: key,
|
||||
Label: strings.TrimSpace(item.Label),
|
||||
ValueType: normalizeGlobalCustomClaimType(item.ValueType),
|
||||
ReadPermission: adminNormalizeCustomClaimPermission(item.ReadPermission),
|
||||
WritePermission: adminNormalizeCustomClaimPermission(item.WritePermission),
|
||||
Description: strings.TrimSpace(item.Description),
|
||||
})
|
||||
}
|
||||
return normalized, nil
|
||||
}
|
||||
|
||||
func globalCustomClaimDefinitionsToJSON(items []globalCustomClaimDefinition) []any {
|
||||
values := make([]any, 0, len(items))
|
||||
for _, item := range items {
|
||||
values = append(values, map[string]any{
|
||||
"key": item.Key,
|
||||
"label": item.Label,
|
||||
"valueType": item.ValueType,
|
||||
"readPermission": item.ReadPermission,
|
||||
"writePermission": item.WritePermission,
|
||||
"description": item.Description,
|
||||
})
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func normalizeGlobalCustomClaimType(value string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(value)) {
|
||||
case "number", "boolean", "array", "object", "date", "datetime":
|
||||
return strings.ToLower(strings.TrimSpace(value))
|
||||
default:
|
||||
return "text"
|
||||
}
|
||||
}
|
||||
|
||||
func adminNormalizeCustomClaimPermission(value string) string {
|
||||
if strings.TrimSpace(value) == "user_and_admin" {
|
||||
return "user_and_admin"
|
||||
}
|
||||
return "admin_only"
|
||||
}
|
||||
|
||||
func isValidCustomClaimKey(value string) bool {
|
||||
for _, r := range value {
|
||||
if r >= 'a' && r <= 'z' || r >= 'A' && r <= 'Z' || r >= '0' && r <= '9' || r == '_' || r == '-' || r == '.' {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func stringValue(value any) string {
|
||||
if text, ok := value.(string); ok {
|
||||
return text
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func requireSuperAdminProfile(c *fiber.Ctx) bool {
|
||||
profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||
if profile == nil || domain.NormalizeRole(profile.Role) != domain.RoleSuperAdmin {
|
||||
@@ -133,26 +303,48 @@ func (h *AdminHandler) GetUserProjectionStatus(c *fiber.Ctx) error {
|
||||
return c.JSON(status)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ReconcileUserProjection(c *fiber.Ctx) error {
|
||||
func (h *AdminHandler) GetOrySSOTSystemStatus(c *fiber.Ctx) error {
|
||||
if !requireSuperAdminProfile(c) {
|
||||
return nil
|
||||
}
|
||||
if h == nil || h.UserProjectionSyncer == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "user projection sync service unavailable"})
|
||||
if h == nil || h.UserProjectionRepo == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "user projection service unavailable"})
|
||||
}
|
||||
count, err := h.UserProjectionSyncer.Reconcile(c.Context())
|
||||
projectionStatus, err := h.UserProjectionRepo.GetStatus(c.Context())
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": err.Error()})
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
|
||||
cacheStatus := domain.IdentityCacheStatus{
|
||||
Status: "unavailable",
|
||||
RedisReady: false,
|
||||
LastError: "identity cache service unavailable",
|
||||
}
|
||||
if h.IdentityCache != nil {
|
||||
cacheStatus, err = h.IdentityCache.GetIdentityCacheStatus(c.Context())
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"status": "success",
|
||||
"syncedUsers": count,
|
||||
"updatedAt": time.Now().UTC().Format(time.RFC3339),
|
||||
"userProjection": projectionStatus,
|
||||
"identityCache": cacheStatus,
|
||||
})
|
||||
}
|
||||
|
||||
func (h *AdminHandler) ResetUserProjection(c *fiber.Ctx) error {
|
||||
return h.ReconcileUserProjection(c)
|
||||
func (h *AdminHandler) FlushIdentityCache(c *fiber.Ctx) error {
|
||||
if !requireSuperAdminProfile(c) {
|
||||
return nil
|
||||
}
|
||||
if h == nil || h.IdentityCache == nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": "identity cache service unavailable"})
|
||||
}
|
||||
result, err := h.IdentityCache.FlushIdentityCache(c.Context())
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(result)
|
||||
}
|
||||
|
||||
func (h *AdminHandler) GetDataIntegrity(c *fiber.Ctx) error {
|
||||
|
||||
Reference in New Issue
Block a user