forked from baron/baron-sso
테넌트 관리자(Tenant Admin)의 본인 소유 테넌트 목록 조회 및 관리 기능 개선
This commit is contained in:
@@ -40,7 +40,7 @@ const RoleSwitcher: FC = () => {
|
|||||||
super_admin: t("ui.admin.role.super_admin", "SUPER ADMIN"),
|
super_admin: t("ui.admin.role.super_admin", "SUPER ADMIN"),
|
||||||
tenant_admin: t("ui.admin.role.tenant_admin", "TENANT ADMIN"),
|
tenant_admin: t("ui.admin.role.tenant_admin", "TENANT ADMIN"),
|
||||||
rp_admin: t("ui.admin.role.rp_admin", "RP ADMIN"),
|
rp_admin: t("ui.admin.role.rp_admin", "RP ADMIN"),
|
||||||
tenant_member: t("ui.admin.role.tenant_member", "TENANT MEMBER"),
|
user: t("ui.admin.role.user", "TENANT MEMBER"),
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -110,7 +110,7 @@ const RoleSwitcher: FC = () => {
|
|||||||
"super_admin",
|
"super_admin",
|
||||||
"tenant_admin",
|
"tenant_admin",
|
||||||
"rp_admin",
|
"rp_admin",
|
||||||
"tenant_member",
|
"user",
|
||||||
] as const
|
] as const
|
||||||
).map((role) => (
|
).map((role) => (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -556,8 +556,8 @@ func main() {
|
|||||||
admin.Get("/check", adminHandler.CheckAuth) // 기본 Admin 체크는 requireAdmin 없이 ApiKeyAuth로만 보호될 수 있음 (또는 추가 가능)
|
admin.Get("/check", adminHandler.CheckAuth) // 기본 Admin 체크는 requireAdmin 없이 ApiKeyAuth로만 보호될 수 있음 (또는 추가 가능)
|
||||||
admin.Get("/stats", requireSuperAdmin, adminHandler.GetSystemStats)
|
admin.Get("/stats", requireSuperAdmin, adminHandler.GetSystemStats)
|
||||||
|
|
||||||
// Tenant Management (Super Admin Only)
|
// Tenant Management (Mixed roles, handler filters results)
|
||||||
admin.Get("/tenants", requireSuperAdmin, tenantHandler.ListTenants)
|
admin.Get("/tenants", requireAdmin, tenantHandler.ListTenants)
|
||||||
admin.Post("/tenants", requireSuperAdmin, tenantHandler.CreateTenant)
|
admin.Post("/tenants", requireSuperAdmin, tenantHandler.CreateTenant)
|
||||||
admin.Post("/tenants/:id/approve", requireSuperAdmin, tenantHandler.ApproveTenant)
|
admin.Post("/tenants/:id/approve", requireSuperAdmin, tenantHandler.ApproveTenant)
|
||||||
admin.Get("/tenants/:id", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "view"), tenantHandler.GetTenant)
|
admin.Get("/tenants/:id", requireAdmin, middleware.RequireKetoPermission(middleware.RBACConfig{AuthHandler: authHandler, KetoService: ketoService}, "Tenant", "view"), tenantHandler.GetTenant)
|
||||||
|
|||||||
@@ -3948,7 +3948,7 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
|||||||
cached, _ := h.RedisService.Get(cacheKey)
|
cached, _ := h.RedisService.Get(cacheKey)
|
||||||
if cached != "" {
|
if cached != "" {
|
||||||
if json.Unmarshal([]byte(cached), &profile) == nil {
|
if json.Unmarshal([]byte(cached), &profile) == nil {
|
||||||
// Fall through to role override check
|
slog.Debug("Profile loaded from cache", "token", token[:10]+"...", "role", profile.Role)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3959,6 +3959,7 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
|||||||
profile, err = h.getKratosProfile(token)
|
profile, err = h.getKratosProfile(token)
|
||||||
if err != nil && h.Hydra != nil {
|
if err != nil && h.Hydra != nil {
|
||||||
// Fallback to Hydra introspection
|
// Fallback to Hydra introspection
|
||||||
|
slog.Debug("Kratos session check failed, trying Hydra", "error", err)
|
||||||
profile, err = h.getHydraProfile(c.Context(), token)
|
profile, err = h.getHydraProfile(c.Context(), token)
|
||||||
}
|
}
|
||||||
} else if cookie != "" {
|
} else if cookie != "" {
|
||||||
@@ -3970,12 +3971,12 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
|||||||
// 2. Role Override for real profile or fallback to Mock Profile
|
// 2. Role Override for real profile or fallback to Mock Profile
|
||||||
if profile != nil {
|
if profile != nil {
|
||||||
if isDev && mockRole != "" {
|
if isDev && mockRole != "" {
|
||||||
slog.Info("🔑 [AUTH_DEBUG] Overriding real profile role with mock role",
|
slog.Info("🔑 [AUTH] Overriding real profile role",
|
||||||
"email", profile.Email, "oldRole", profile.Role, "newRole", mockRole)
|
"email", profile.Email, "originalRole", profile.Role, "overriddenRole", mockRole)
|
||||||
profile.Role = mockRole
|
profile.Role = mockRole
|
||||||
}
|
}
|
||||||
} else if isDev && mockRole != "" && token == "" && cookie == "" {
|
} else if isDev && mockRole != "" && token == "" && cookie == "" {
|
||||||
slog.Info("🔑 [AUTH_DEBUG] No real session found, using full Mock Auth", "role", mockRole)
|
slog.Info("🔑 [AUTH] Using full Mock Auth (no session)", "role", mockRole)
|
||||||
profile = &domain.UserProfileResponse{
|
profile = &domain.UserProfileResponse{
|
||||||
ID: "00000000-0000-0000-0000-000000000000",
|
ID: "00000000-0000-0000-0000-000000000000",
|
||||||
Email: "mock@hmac.kr",
|
Email: "mock@hmac.kr",
|
||||||
@@ -3988,6 +3989,7 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
if profile == nil {
|
if profile == nil {
|
||||||
|
slog.Warn("No profile resolved", "token_len", len(token), "cookie_len", len(cookie), "mockRole", mockRole)
|
||||||
return nil, errors.New("invalid session (trace:resolve_profile)")
|
return nil, errors.New("invalid session (trace:resolve_profile)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4012,7 +4014,10 @@ func (h *AuthHandler) resolveCurrentProfile(c *fiber.Ctx) (*domain.UserProfileRe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Save to Redis Cache (Short TTL)
|
// 4. Save to Redis Cache (Short TTL)
|
||||||
if h.RedisService != nil && cacheKey != "" && err == nil {
|
// IMPORTANT: In dev mode, if role was overridden, we should NOT cache it under the token key
|
||||||
|
// or we should include the mock role in the cache key.
|
||||||
|
// For simplicity, let's skip caching if mockRole is present in dev.
|
||||||
|
if h.RedisService != nil && cacheKey != "" && err == nil && !(isDev && mockRole != "") {
|
||||||
if data, err := json.Marshal(profile); err == nil {
|
if data, err := json.Marshal(profile); err == nil {
|
||||||
ttlStr := os.Getenv("PROFILE_CACHE_TTL")
|
ttlStr := os.Getenv("PROFILE_CACHE_TTL")
|
||||||
ttl := 30 * time.Minute // Default TTL
|
ttl := 30 * time.Minute // Default TTL
|
||||||
|
|||||||
@@ -109,9 +109,36 @@ func (h *TenantHandler) ListTenants(c *fiber.Ctx) error {
|
|||||||
offset = 0
|
offset = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
tenants, total, err := h.Service.ListTenants(c.Context(), limit, offset, parentId)
|
var tenants []domain.Tenant
|
||||||
if err != nil {
|
var total int64
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
var err error
|
||||||
|
|
||||||
|
profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
|
||||||
|
|
||||||
|
// If Tenant Admin, only list manageable tenants
|
||||||
|
if profile != nil && profile.Role == domain.RoleTenantAdmin {
|
||||||
|
slog.Info("Listing manageable tenants for tenant admin", "userID", profile.ID)
|
||||||
|
tenants, err = h.Service.ListManageableTenants(c.Context(), profile.ID)
|
||||||
|
if err != nil {
|
||||||
|
return errorJSON(c, fiber.StatusInternalServerError, "failed to list manageable tenants: "+err.Error())
|
||||||
|
}
|
||||||
|
total = int64(len(tenants))
|
||||||
|
// Apply basic pagination if needed (optional for usually small number of manageable tenants)
|
||||||
|
if offset < len(tenants) {
|
||||||
|
end := offset + limit
|
||||||
|
if end > len(tenants) {
|
||||||
|
end = len(tenants)
|
||||||
|
}
|
||||||
|
tenants = tenants[offset:end]
|
||||||
|
} else {
|
||||||
|
tenants = []domain.Tenant{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Super Admin case
|
||||||
|
tenants, total, err = h.Service.ListTenants(c.Context(), limit, offset, parentId)
|
||||||
|
if err != nil {
|
||||||
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": err.Error()})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch member counts for all tenants in one query using slugs (company codes)
|
// Fetch member counts for all tenants in one query using slugs (company codes)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func RequireKetoPermission(config RBACConfig, namespace, relation string) fiber.
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !allowed {
|
if !allowed {
|
||||||
slog.Warn("Keto permission denied", "userID", profile.ID, "namespace", namespace, "objectID", objectID, "relation", relation)
|
slog.Warn("Keto permission denied", "userID", profile.ID, "userRole", profile.Role, "namespace", namespace, "objectID", objectID, "relation", relation, "X-Test-Role", c.Get("X-Test-Role"))
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: keto permission denied for "+namespace+":"+objectID)
|
return errorJSON(c, fiber.StatusForbidden, "forbidden: keto permission denied for "+namespace+":"+objectID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,13 +111,11 @@ func RequireRole(config RBACConfig) fiber.Handler {
|
|||||||
"userRole", profile.Role,
|
"userRole", profile.Role,
|
||||||
"allowedRoles", config.AllowedRoles,
|
"allowedRoles", config.AllowedRoles,
|
||||||
"path", c.Path(),
|
"path", c.Path(),
|
||||||
|
"X-Test-Role", c.Get("X-Test-Role"),
|
||||||
)
|
)
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions")
|
return errorJSON(c, fiber.StatusForbidden, "forbidden: insufficient permissions")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store profile in locals for further use in handlers
|
|
||||||
c.Locals("user_profile", profile)
|
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ type TenantService interface {
|
|||||||
GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error)
|
GetTenantBySlug(ctx context.Context, slug string) (*domain.Tenant, error)
|
||||||
GetTenant(ctx context.Context, id string) (*domain.Tenant, error)
|
GetTenant(ctx context.Context, id string) (*domain.Tenant, error)
|
||||||
ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error)
|
ListTenants(ctx context.Context, limit, offset int, parentID string) ([]domain.Tenant, int64, error)
|
||||||
|
ListManageableTenants(ctx context.Context, userID string) ([]domain.Tenant, error)
|
||||||
ApproveTenant(ctx context.Context, id string) error
|
ApproveTenant(ctx context.Context, id string) error
|
||||||
SetKetoService(keto KetoService) // 추가
|
SetKetoService(keto KetoService) // 추가
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user