forked from baron/baron-sso
관계 조회 권한 사용자 검색 안내 강화
This commit is contained in:
@@ -695,13 +695,21 @@ func (h *DevHandler) SearchUsers(c *fiber.Ctx) error {
|
|||||||
if profile == nil {
|
if profile == nil {
|
||||||
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
return errorJSON(c, fiber.StatusUnauthorized, "unauthorized: authentication required")
|
||||||
}
|
}
|
||||||
if !isDevConsoleRoleAllowed(normalizeUserRole(profile.Role)) {
|
|
||||||
|
// Tightened Security: Only SuperAdmin bypasses the client-specific manage check.
|
||||||
|
// Regular users (RoleUser) or RPAdmins must have the 'manage' permit for the requested clientId.
|
||||||
|
if normalizeUserRole(profile.Role) != domain.RoleSuperAdmin {
|
||||||
clientID := strings.TrimSpace(c.Query("clientId"))
|
clientID := strings.TrimSpace(c.Query("clientId"))
|
||||||
|
if clientID == "" {
|
||||||
|
return errorJSON(c, fiber.StatusBadRequest, "clientId is required for user search")
|
||||||
|
}
|
||||||
summary, err := h.loadClientSummary(c.Context(), clientID)
|
summary, err := h.loadClientSummary(c.Context(), clientID)
|
||||||
if clientID == "" || err != nil || !h.canManageClientRelations(c, profile, summary) {
|
if err != nil || !h.canManageClientRelations(c, profile, summary) {
|
||||||
return errorJSON(c, fiber.StatusForbidden, "forbidden")
|
// canManageClientRelations checks for 'manage' permit in Keto.
|
||||||
|
return errorJSON(c, fiber.StatusForbidden, "forbidden: manage permission required for user search")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.KratosAdmin == nil {
|
if h.KratosAdmin == nil {
|
||||||
return errorJSON(c, fiber.StatusServiceUnavailable, "kratos admin unavailable")
|
return errorJSON(c, fiber.StatusServiceUnavailable, "kratos admin unavailable")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ import {
|
|||||||
removeClientRelation,
|
removeClientRelation,
|
||||||
} from "../../lib/devApi";
|
} from "../../lib/devApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
|
import { resolveProfileRole } from "../../lib/role";
|
||||||
|
import { useAuth } from "react-oidc-context";
|
||||||
import { ClientDetailTabs } from "./ClientDetailTabs";
|
import { ClientDetailTabs } from "./ClientDetailTabs";
|
||||||
|
|
||||||
const relationOptions = [
|
const relationOptions = [
|
||||||
@@ -68,6 +70,7 @@ function formatUserLabel(user: DevAssignableUser) {
|
|||||||
|
|
||||||
function ClientRelationsPage() {
|
function ClientRelationsPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const auth = useAuth();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const clientId = params.id ?? "";
|
const clientId = params.id ?? "";
|
||||||
const [selectedRelations, setSelectedRelations] = useState<RelationOption[]>(
|
const [selectedRelations, setSelectedRelations] = useState<RelationOption[]>(
|
||||||
@@ -79,6 +82,11 @@ function ClientRelationsPage() {
|
|||||||
null,
|
null,
|
||||||
);
|
);
|
||||||
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
const [isSearchOpen, setIsSearchOpen] = useState(false);
|
||||||
|
|
||||||
|
const systemRole = resolveProfileRole(
|
||||||
|
auth.user?.profile as Record<string, unknown> | undefined,
|
||||||
|
);
|
||||||
|
|
||||||
const { data: clientData } = useQuery({
|
const { data: clientData } = useQuery({
|
||||||
queryKey: ["client", clientId],
|
queryKey: ["client", clientId],
|
||||||
queryFn: () => fetchClient(clientId),
|
queryFn: () => fetchClient(clientId),
|
||||||
@@ -95,6 +103,19 @@ function ClientRelationsPage() {
|
|||||||
enabled: clientId.length > 0,
|
enabled: clientId.length > 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Calculate permissions for UI hints and button states
|
||||||
|
const isSuperAdmin = systemRole === "super_admin";
|
||||||
|
const myUserId = auth.user?.profile.sub;
|
||||||
|
const isRpAdmin = useMemo(() => {
|
||||||
|
if (isSuperAdmin) return true;
|
||||||
|
if (!relationData?.items || !myUserId) return false;
|
||||||
|
return relationData.items.some(
|
||||||
|
(item) => item.subject === `User:${myUserId}` && item.relation === "admins"
|
||||||
|
);
|
||||||
|
}, [relationData?.items, myUserId, isSuperAdmin]);
|
||||||
|
|
||||||
|
const canManageRelations = isRpAdmin || isSuperAdmin;
|
||||||
|
|
||||||
const isRelationshipViewForbidden =
|
const isRelationshipViewForbidden =
|
||||||
(error as AxiosError | null)?.response?.status === 403;
|
(error as AxiosError | null)?.response?.status === 403;
|
||||||
const relationshipViewForbiddenMessage = t(
|
const relationshipViewForbiddenMessage = t(
|
||||||
@@ -221,6 +242,16 @@ function ClientRelationsPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
|
if (!canManageRelations) {
|
||||||
|
toast(
|
||||||
|
t(
|
||||||
|
"msg.dev.clients.relationships.add_forbidden_viewer",
|
||||||
|
"'관계 조회' 권한만으로는 새로운 관계를 추가하거나 사용자를 검색할 수 없습니다. 'RP 관리자' 권한이 필요합니다.",
|
||||||
|
),
|
||||||
|
"error",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!selectedUser) {
|
if (!selectedUser) {
|
||||||
toast(
|
toast(
|
||||||
t(
|
t(
|
||||||
@@ -398,18 +429,26 @@ function ClientRelationsPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : isUserSearchForbidden ? (
|
) : isUserSearchForbidden ? (
|
||||||
<div className="px-3 py-2 text-sm text-destructive font-medium">
|
<div className="px-4 py-8 text-center text-sm text-destructive font-medium border-b border-border/40 bg-destructive/5 flex flex-col gap-2">
|
||||||
{t(
|
<p>
|
||||||
"msg.dev.clients.relationships.search_forbidden_user",
|
{t(
|
||||||
"일반 사용자는 관계 추가를 위한 사용자 검색을 사용할 수 없습니다.",
|
"msg.dev.clients.relationships.search_forbidden_user",
|
||||||
)}
|
"일반 사용자는 관계 추가를 위한 사용자 검색을 사용할 수 없습니다.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground/80 font-normal">
|
||||||
|
{t(
|
||||||
|
"msg.dev.clients.relationships.search_forbidden_user_hint",
|
||||||
|
"'관계 조회' 권한만으로는 사용자 검색이 제한됩니다. 'RP 관리자' 관계가 필요합니다.",
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (userSearchData?.items ?? []).length > 0 ? (
|
) : (userSearchData?.items ?? []).length > 0 ? (
|
||||||
(userSearchData?.items ?? []).map((user) => (
|
(userSearchData?.items ?? []).map((user) => (
|
||||||
<button
|
<button
|
||||||
key={user.id}
|
key={user.id}
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full flex-col gap-1 px-3 py-2 text-left hover:bg-muted/40"
|
className="flex w-full flex-col gap-1 px-3 py-2 text-left hover:bg-muted/40 border-b border-border/40 last:border-b-0"
|
||||||
onMouseDown={(event) => {
|
onMouseDown={(event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handleSelectUser(user);
|
handleSelectUser(user);
|
||||||
@@ -425,7 +464,7 @@ function ClientRelationsPage() {
|
|||||||
</button>
|
</button>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<div className="px-3 py-2 text-sm text-muted-foreground">
|
<div className="px-3 py-4 text-center text-sm text-muted-foreground">
|
||||||
{t(
|
{t(
|
||||||
"msg.dev.clients.relationships.search_empty",
|
"msg.dev.clients.relationships.search_empty",
|
||||||
"검색 결과가 없습니다.",
|
"검색 결과가 없습니다.",
|
||||||
@@ -503,7 +542,7 @@ function ClientRelationsPage() {
|
|||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Button
|
<Button
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
disabled={addMutation.isPending}
|
disabled={addMutation.isPending || !canManageRelations}
|
||||||
className="gap-2"
|
className="gap-2"
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4" />
|
<Plus className="h-4 w-4" />
|
||||||
@@ -625,7 +664,7 @@ function ClientRelationsPage() {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="gap-2 text-destructive hover:text-destructive"
|
className="gap-2 text-destructive hover:text-destructive"
|
||||||
disabled={removeMutation.isPending}
|
disabled={removeMutation.isPending || !canManageRelations}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleRemove(item.relation, item.subject)
|
handleRemove(item.relation, item.subject)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user