1
0
forked from baron/baron-sso

feat: allow regular users to view their own tenant's org chart

Changes the /users endpoint to allow RoleUser access and securely restricts the returned data to only users within their affiliated tenants. Removes the unnecessary back button from the Org Chart view since it's now a top-level nav item.
This commit is contained in:
2026-04-13 10:47:56 +09:00
parent 984adcfa62
commit d3a82d1653
4 changed files with 39 additions and 24 deletions

View File

@@ -125,11 +125,15 @@ function AppLayout() {
icon: Building2,
});
}
filteredItems.splice(manageableCount <= 1 && profile?.tenantId ? 2 : 2, 0, {
label: "ui.admin.nav.org_chart",
to: "/tenants/org-chart",
icon: Network,
});
filteredItems.splice(
manageableCount <= 1 && profile?.tenantId ? 2 : 2,
0,
{
label: "ui.admin.nav.org_chart",
to: "/tenants/org-chart",
icon: Network,
},
);
} else {
// 일반 사용자(Tenant Member)도 조직도 메뉴를 볼 수 있도록 추가합니다.
filteredItems.splice(1, 0, {
@@ -439,9 +443,9 @@ function AppLayout() {
{navItems.map(({ label, to, icon: Icon }) => {
const isOrgChart = location.pathname === "/tenants/org-chart";
const isTenantsRoot = to === "/tenants";
const isCustomActive = isTenantsRoot
? (location.pathname.startsWith("/tenants") && !isOrgChart)
: to === "/"
const isCustomActive = isTenantsRoot
? location.pathname.startsWith("/tenants") && !isOrgChart
: to === "/"
? location.pathname === "/"
: location.pathname.startsWith(to);

View File

@@ -211,11 +211,6 @@ export function TenantOrgChartPage() {
<div className="flex flex-col h-[calc(100vh-theme(spacing.32))] bg-slate-50 rounded-xl overflow-hidden shadow-sm border border-slate-200">
<header className="flex items-center justify-between px-6 py-4 bg-white border-b border-slate-200 shadow-sm z-10 shrink-0">
<div className="flex items-center gap-4">
<Button variant="outline" size="icon" asChild className="h-8 w-8">
<Link to="/tenants">
<ChevronLeft size={16} />
</Link>
</Button>
<div>
<h2 className="text-xl font-bold text-slate-800"></h2>
<p className="text-xs text-slate-500">

View File

@@ -599,13 +599,17 @@ func main() {
KetoService: ketoService,
})
requireAdmin := middleware.RequireRole(middleware.RBACConfig{
AllowedRoles: []string{domain.RoleSuperAdmin, domain.RoleTenantAdmin},
AuthHandler: authHandler,
KetoService: ketoService,
AllowedRoles: []string{domain.RoleSuperAdmin, domain.RoleTenantAdmin},
AuthHandler: authHandler,
KetoService: ketoService,
})
requireAnyUser := middleware.RequireRole(middleware.RBACConfig{
AllowedRoles: []string{domain.RoleSuperAdmin, domain.RoleTenantAdmin, domain.RoleRPAdmin, domain.RoleUser},
AuthHandler: authHandler,
KetoService: ketoService,
})
admin.Get("/check", adminHandler.CheckAuth) // 기본 Admin 체크는 requireAdmin 없이 ApiKeyAuth로만 보호될 수 있음 (또는 추가 가능)
admin.Get("/stats", requireSuperAdmin, adminHandler.GetSystemStats)
admin.Get("/check", adminHandler.CheckAuth) // 기본 Admin 체크는 requireAdmin 없이 ApiKeyAuth로만 보호될 수 있음 (또는 추가 가능) admin.Get("/stats", requireSuperAdmin, adminHandler.GetSystemStats)
// Tenant Management (Mixed roles, handler filters results)
admin.Get("/tenants", requireAdmin, tenantHandler.ListTenants)
@@ -668,9 +672,8 @@ func main() {
relyingPartyHandler.Delete)
// Admin User Management
admin.Get("/users", requireAdmin, userHandler.ListUsers)
admin.Get("/users/export", userHandler.ExportUsersCSV) // Removed requireAdmin to handle mock role in query param
admin.Post("/users", requireAdmin, userHandler.CreateUser)
admin.Get("/users", requireAnyUser, userHandler.ListUsers)
admin.Get("/users/export", userHandler.ExportUsersCSV) // Removed requireAdmin to handle mock role in query param admin.Post("/users", requireAdmin, userHandler.CreateUser)
admin.Post("/users/bulk", requireAdmin, userHandler.BulkCreateUsers)
admin.Put("/users/bulk", requireAdmin, userHandler.BulkUpdateUsers)
admin.Delete("/users/bulk", requireAdmin, userHandler.BulkDeleteUsers)

View File

@@ -100,13 +100,17 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
// [New] Manageable Tenants Map for efficient lookup
manageableSlugs := make(map[string]bool)
if requesterRole == domain.RoleTenantAdmin {
if requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin {
profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
if profile != nil {
for _, t := range profile.ManageableTenants {
manageableSlugs[strings.ToLower(t.Slug)] = true
manageableSlugs[strings.ToLower(t.ID)] = true // Add ID as well
}
for _, t := range profile.JoinedTenants {
manageableSlugs[strings.ToLower(t.Slug)] = true
manageableSlugs[strings.ToLower(t.ID)] = true
}
// Include primary tenant slug if not already there
if profile.CompanyCode != "" {
manageableSlugs[strings.ToLower(profile.CompanyCode)] = true
@@ -137,8 +141,8 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
compCode := strings.ToLower(extractTraitString(identity.Traits, "companyCode"))
tID := strings.ToLower(extractTraitString(identity.Traits, "tenant_id"))
// Tenant Admin filtering
if requesterRole == domain.RoleTenantAdmin {
// Tenant Admin & Member filtering
if requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin {
if !manageableSlugs[compCode] && !manageableSlugs[tID] {
continue
}
@@ -194,6 +198,15 @@ func (h *UserHandler) ListUsers(c *fiber.Ctx) error {
// 2. Fallback to Local DB if Kratos is down
slog.Warn("Kratos unavailable, falling back to local DB for user list", "error", err)
// If requester is not Super Admin, we should technically filter by manageable slugs in DB too.
// For simplicity in fallback, if tenantSlug is empty we default to their primary company code.
if (requesterRole == domain.RoleTenantAdmin || requesterRole == domain.RoleUser || requesterRole == domain.RoleRPAdmin) && tenantSlug == "" {
profile, _ := c.Locals("user_profile").(*domain.UserProfileResponse)
if profile != nil && profile.CompanyCode != "" {
tenantSlug = profile.CompanyCode
}
}
// Fetch from UserRepo
users, total, err := h.UserRepo.List(c.Context(), offset, limit, search, tenantSlug)
if err != nil {