forked from baron/baron-sso
adminfront 및 백엔드: 세부 권한 변경 시 Keto 동기식 실시간 쓰기 및 프론트 일괄 갱신 적용하여 지연/롤백 버그 해결 완료
This commit is contained in:
@@ -6,7 +6,7 @@ import {
|
|||||||
ShieldCheck,
|
ShieldCheck,
|
||||||
UserPlus,
|
UserPlus,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { useTenantPermission } from "../hooks/useTenantPermission";
|
import { useTenantPermission } from "../hooks/useTenantPermission";
|
||||||
import { Badge } from "../../../components/ui/badge";
|
import { Badge } from "../../../components/ui/badge";
|
||||||
@@ -58,13 +58,45 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
||||||
|
|
||||||
|
// 🌟 테넌트 탭별 드롭다운 즉각 변경을 위한 임시 로컬 맵 선언
|
||||||
|
const [localTenantPermissions, setLocalTenantPermissions] = useState<Record<string, Record<string, "none" | "read" | "write">>>({});
|
||||||
|
|
||||||
const relationsQuery = useQuery({
|
const relationsQuery = useQuery({
|
||||||
queryKey: ["tenant-relations", tenantId],
|
queryKey: ["tenant-relations", tenantId],
|
||||||
queryFn: () => fetchTenantRelations(tenantId),
|
queryFn: () => fetchTenantRelations(tenantId),
|
||||||
enabled: !!tenantId,
|
enabled: !!tenantId,
|
||||||
});
|
});
|
||||||
|
const relationsData = relationsQuery.data ?? [];
|
||||||
|
|
||||||
|
// 🌟 서버 데이터를 수신하면 로컬 변경 상태 맵을 실시간 동기화
|
||||||
|
useEffect(() => {
|
||||||
|
if (relationsQuery.data) {
|
||||||
|
const initialMap: Record<string, Record<string, "none" | "read" | "write">> = {};
|
||||||
|
for (const user of relationsQuery.data) {
|
||||||
|
initialMap[user.userId] = {};
|
||||||
|
const tabs = ["profile", "permissions", "organization", "schema"];
|
||||||
|
for (const tab of tabs) {
|
||||||
|
const isWrite = user.relations.includes(`${tab}_managers`);
|
||||||
|
const isRead = user.relations.includes(`${tab}_viewers`);
|
||||||
|
initialMap[user.userId][tab] = isWrite ? "write" : isRead ? "read" : "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setLocalTenantPermissions(initialMap);
|
||||||
|
}
|
||||||
|
}, [relationsQuery.data]);
|
||||||
const relations = relationsQuery.data ?? [];
|
const relations = relationsQuery.data ?? [];
|
||||||
|
|
||||||
|
const invalidateAllQueries = () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tenant-relations", tenantId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["me"] });
|
||||||
|
setTimeout(() => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tenant-relations", tenantId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["tenant", tenantId] });
|
||||||
|
queryClient.invalidateQueries({ queryKey: ["me"] });
|
||||||
|
}, 500);
|
||||||
|
};
|
||||||
|
|
||||||
const addRelationMutation = useMutation({
|
const addRelationMutation = useMutation({
|
||||||
mutationFn: (payload: { userId: string; relation: string }) =>
|
mutationFn: (payload: { userId: string; relation: string }) =>
|
||||||
addTenantRelation(tenantId, payload.userId, payload.relation),
|
addTenantRelation(tenantId, payload.userId, payload.relation),
|
||||||
@@ -96,10 +128,7 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t("msg.admin.tenants.relations.add_success", "세부 권한이 추가되었습니다."));
|
// Quiet mutate
|
||||||
},
|
|
||||||
onSettled: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["tenant-relations", tenantId] });
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -132,10 +161,7 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
toast.error(err.response?.data?.error || t("msg.common.error", "오류가 발생했습니다."));
|
||||||
},
|
},
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(t("msg.admin.tenants.relations.remove_success", "세부 권한이 회수되었습니다."));
|
// Quiet mutate
|
||||||
},
|
|
||||||
onSettled: () => {
|
|
||||||
queryClient.invalidateQueries({ queryKey: ["tenant-relations", tenantId] });
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,6 +176,7 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
|
|
||||||
if (currentVal === newVal) return;
|
if (currentVal === newVal) return;
|
||||||
|
|
||||||
|
try {
|
||||||
if (currentVal === "read") {
|
if (currentVal === "read") {
|
||||||
await removeRelationMutation.mutateAsync({ userId, relation: readRel });
|
await removeRelationMutation.mutateAsync({ userId, relation: readRel });
|
||||||
} else if (currentVal === "write") {
|
} else if (currentVal === "write") {
|
||||||
@@ -161,6 +188,14 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
} else if (newVal === "write") {
|
} else if (newVal === "write") {
|
||||||
await addRelationMutation.mutateAsync({ userId, relation: writeRel });
|
await addRelationMutation.mutateAsync({ userId, relation: writeRel });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invalidateAllQueries();
|
||||||
|
|
||||||
|
// 🌟 Trigger a single consolidated success toast at the very end
|
||||||
|
toast.success(t("msg.admin.tenants.relations.update_success", "세부 권한이 성공적으로 변경되었습니다."));
|
||||||
|
} catch {
|
||||||
|
// Individual mutations handle error toast via onError
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemoveAllRelations = async (userId: string, userRelations: string[]) => {
|
const handleRemoveAllRelations = async (userId: string, userRelations: string[]) => {
|
||||||
@@ -170,6 +205,7 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
for (const rel of userRelations) {
|
for (const rel of userRelations) {
|
||||||
await removeRelationMutation.mutateAsync({ userId, relation: rel });
|
await removeRelationMutation.mutateAsync({ userId, relation: rel });
|
||||||
}
|
}
|
||||||
|
invalidateAllQueries();
|
||||||
};
|
};
|
||||||
|
|
||||||
const usersQuery = useQuery({
|
const usersQuery = useQuery({
|
||||||
@@ -179,7 +215,11 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleAddUser = (userId: string) => {
|
const handleAddUser = (userId: string) => {
|
||||||
addRelationMutation.mutate({ userId, relation: "profile_viewers" });
|
addRelationMutation.mutate({ userId, relation: "profile_viewers" }, {
|
||||||
|
onSettled: () => {
|
||||||
|
invalidateAllQueries();
|
||||||
|
}
|
||||||
|
});
|
||||||
setIsDialogOpen(false);
|
setIsDialogOpen(false);
|
||||||
setSearchTerm("");
|
setSearchTerm("");
|
||||||
};
|
};
|
||||||
@@ -259,6 +299,11 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
? "read"
|
? "read"
|
||||||
: "none";
|
: "none";
|
||||||
|
|
||||||
|
const curProfileVal = localTenantPermissions[user.userId]?.profile ?? profileVal;
|
||||||
|
const curPermissionsVal = localTenantPermissions[user.userId]?.permissions ?? permissionsVal;
|
||||||
|
const curOrganizationVal = localTenantPermissions[user.userId]?.organization ?? organizationVal;
|
||||||
|
const curSchemaVal = localTenantPermissions[user.userId]?.schema ?? schemaVal;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={user.userId} className="hover:bg-muted/10 transition-colors">
|
<TableRow key={user.userId} className="hover:bg-muted/10 transition-colors">
|
||||||
<TableCell className="font-medium">
|
<TableCell className="font-medium">
|
||||||
@@ -270,16 +315,25 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<select
|
<select
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
value={profileVal}
|
value={curProfileVal}
|
||||||
disabled={!isWritable}
|
disabled={!isWritable}
|
||||||
onChange={(e) =>
|
name={`tenant-fine-grained-profile-${user.userId}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const nextVal = e.target.value as "none" | "read" | "write";
|
||||||
|
setLocalTenantPermissions(prev => ({
|
||||||
|
...prev,
|
||||||
|
[user.userId]: {
|
||||||
|
...(prev[user.userId] ?? {}),
|
||||||
|
profile: nextVal
|
||||||
|
}
|
||||||
|
}));
|
||||||
handleRelationChange(
|
handleRelationChange(
|
||||||
user.userId,
|
user.userId,
|
||||||
"profile",
|
"profile",
|
||||||
profileVal,
|
profileVal,
|
||||||
e.target.value as "none" | "read" | "write",
|
nextVal,
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
||||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
||||||
@@ -289,16 +343,25 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<select
|
<select
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
value={permissionsVal}
|
value={curPermissionsVal}
|
||||||
disabled={!isWritable}
|
disabled={!isWritable}
|
||||||
onChange={(e) =>
|
name={`tenant-fine-grained-permissions-${user.userId}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const nextVal = e.target.value as "none" | "read" | "write";
|
||||||
|
setLocalTenantPermissions(prev => ({
|
||||||
|
...prev,
|
||||||
|
[user.userId]: {
|
||||||
|
...(prev[user.userId] ?? {}),
|
||||||
|
permissions: nextVal
|
||||||
|
}
|
||||||
|
}));
|
||||||
handleRelationChange(
|
handleRelationChange(
|
||||||
user.userId,
|
user.userId,
|
||||||
"permissions",
|
"permissions",
|
||||||
permissionsVal,
|
permissionsVal,
|
||||||
e.target.value as "none" | "read" | "write",
|
nextVal,
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
||||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
||||||
@@ -308,16 +371,25 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<select
|
<select
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
value={organizationVal}
|
value={curOrganizationVal}
|
||||||
disabled={!isWritable}
|
disabled={!isWritable}
|
||||||
onChange={(e) =>
|
name={`tenant-fine-grained-organization-${user.userId}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const nextVal = e.target.value as "none" | "read" | "write";
|
||||||
|
setLocalTenantPermissions(prev => ({
|
||||||
|
...prev,
|
||||||
|
[user.userId]: {
|
||||||
|
...(prev[user.userId] ?? {}),
|
||||||
|
organization: nextVal
|
||||||
|
}
|
||||||
|
}));
|
||||||
handleRelationChange(
|
handleRelationChange(
|
||||||
user.userId,
|
user.userId,
|
||||||
"organization",
|
"organization",
|
||||||
organizationVal,
|
organizationVal,
|
||||||
e.target.value as "none" | "read" | "write",
|
nextVal,
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
||||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
||||||
@@ -327,16 +399,25 @@ export function TenantFineGrainedPermissionsTab({ tenantIdProp }: TenantFineGrai
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<select
|
<select
|
||||||
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
className="flex h-9 w-full rounded-md border border-input bg-background px-3 py-1 text-sm shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
value={schemaVal}
|
value={curSchemaVal}
|
||||||
disabled={!isWritable}
|
disabled={!isWritable}
|
||||||
onChange={(e) =>
|
name={`tenant-fine-grained-schema-${user.userId}`}
|
||||||
|
onChange={(e) => {
|
||||||
|
const nextVal = e.target.value as "none" | "read" | "write";
|
||||||
|
setLocalTenantPermissions(prev => ({
|
||||||
|
...prev,
|
||||||
|
[user.userId]: {
|
||||||
|
...(prev[user.userId] ?? {}),
|
||||||
|
schema: nextVal
|
||||||
|
}
|
||||||
|
}));
|
||||||
handleRelationChange(
|
handleRelationChange(
|
||||||
user.userId,
|
user.userId,
|
||||||
"schema",
|
"schema",
|
||||||
schemaVal,
|
schemaVal,
|
||||||
e.target.value as "none" | "read" | "write",
|
nextVal,
|
||||||
)
|
);
|
||||||
}
|
}}
|
||||||
>
|
>
|
||||||
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
<option value="none">{t("ui.common.none", "권한 없음")}</option>
|
||||||
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
<option value="read">{t("ui.common.read", "조회 가능 (Read)")}</option>
|
||||||
|
|||||||
@@ -88,6 +88,15 @@ type tenantPermissions struct {
|
|||||||
View bool `json:"view"`
|
View bool `json:"view"`
|
||||||
Manage bool `json:"manage"`
|
Manage bool `json:"manage"`
|
||||||
ManageAdmins bool `json:"manage_admins"`
|
ManageAdmins bool `json:"manage_admins"`
|
||||||
|
|
||||||
|
ViewProfile bool `json:"view_profile"`
|
||||||
|
ManageProfile bool `json:"manage_profile"`
|
||||||
|
ViewPermissions bool `json:"view_permissions"`
|
||||||
|
ManagePermissions bool `json:"manage_permissions"`
|
||||||
|
ViewOrganization bool `json:"view_organization"`
|
||||||
|
ManageOrganization bool `json:"manage_organization"`
|
||||||
|
ViewSchema bool `json:"view_schema"`
|
||||||
|
ManageSchema bool `json:"manage_schema"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type tenantSummary struct {
|
type tenantSummary struct {
|
||||||
@@ -1694,6 +1703,14 @@ func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
|
|||||||
View: true,
|
View: true,
|
||||||
Manage: true,
|
Manage: true,
|
||||||
ManageAdmins: true,
|
ManageAdmins: true,
|
||||||
|
ViewProfile: true,
|
||||||
|
ManageProfile: true,
|
||||||
|
ViewPermissions: true,
|
||||||
|
ManagePermissions: true,
|
||||||
|
ViewOrganization: true,
|
||||||
|
ManageOrganization: true,
|
||||||
|
ViewSchema: true,
|
||||||
|
ManageSchema: true,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Query Keto in parallel for maximum performance
|
// Query Keto in parallel for maximum performance
|
||||||
@@ -1703,8 +1720,14 @@ func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
|
|||||||
allowed bool
|
allowed bool
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
ch := make(chan checkResult, 3)
|
ch := make(chan checkResult, 11)
|
||||||
relations := []string{"view", "manage", "manage_admins"}
|
relations := []string{
|
||||||
|
"view", "manage", "manage_admins",
|
||||||
|
"view_profile", "manage_profile",
|
||||||
|
"view_permissions", "manage_permissions",
|
||||||
|
"view_organization", "manage_organization",
|
||||||
|
"view_schema", "manage_schema",
|
||||||
|
}
|
||||||
for _, rel := range relations {
|
for _, rel := range relations {
|
||||||
go func(r string) {
|
go func(r string) {
|
||||||
allowed, err := h.Keto.CheckPermission(c.Context(), subject, "Tenant", tenant.ID, r)
|
allowed, err := h.Keto.CheckPermission(c.Context(), subject, "Tenant", tenant.ID, r)
|
||||||
@@ -1726,6 +1749,22 @@ func (h *TenantHandler) GetTenant(c *fiber.Ctx) error {
|
|||||||
perms.Manage = res.allowed
|
perms.Manage = res.allowed
|
||||||
case "manage_admins":
|
case "manage_admins":
|
||||||
perms.ManageAdmins = res.allowed
|
perms.ManageAdmins = res.allowed
|
||||||
|
case "view_profile":
|
||||||
|
perms.ViewProfile = res.allowed
|
||||||
|
case "manage_profile":
|
||||||
|
perms.ManageProfile = res.allowed
|
||||||
|
case "view_permissions":
|
||||||
|
perms.ViewPermissions = res.allowed
|
||||||
|
case "manage_permissions":
|
||||||
|
perms.ManagePermissions = res.allowed
|
||||||
|
case "view_organization":
|
||||||
|
perms.ViewOrganization = res.allowed
|
||||||
|
case "manage_organization":
|
||||||
|
perms.ManageOrganization = res.allowed
|
||||||
|
case "view_schema":
|
||||||
|
perms.ViewSchema = res.allowed
|
||||||
|
case "manage_schema":
|
||||||
|
perms.ManageSchema = res.allowed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
summary.UserPermissions = perms
|
summary.UserPermissions = perms
|
||||||
@@ -3331,16 +3370,35 @@ func (h *TenantHandler) AddRelation(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var directWriteErr error
|
||||||
|
if h.Keto != nil {
|
||||||
|
directWriteErr = h.Keto.CreateRelation(c.Context(), "Tenant", tenantID, req.Relation, "User:"+req.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
if h.KetoOutbox != nil {
|
if h.KetoOutbox != nil {
|
||||||
|
status := domain.KetoOutboxStatusPending
|
||||||
|
var processedAt *time.Time
|
||||||
|
if directWriteErr == nil && h.Keto != nil {
|
||||||
|
status = domain.KetoOutboxStatusProcessed
|
||||||
|
now := time.Now()
|
||||||
|
processedAt = &now
|
||||||
|
}
|
||||||
|
|
||||||
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
||||||
Namespace: "Tenant",
|
Namespace: "Tenant",
|
||||||
Object: tenantID,
|
Object: tenantID,
|
||||||
Relation: req.Relation,
|
Relation: req.Relation,
|
||||||
Subject: "User:" + req.UserID,
|
Subject: "User:" + req.UserID,
|
||||||
Action: domain.KetoOutboxActionCreate,
|
Action: domain.KetoOutboxActionCreate,
|
||||||
|
Status: status,
|
||||||
|
ProcessedAt: processedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if directWriteErr != nil {
|
||||||
|
return errorJSON(c, fiber.StatusInternalServerError, "Keto 동기화 실패: "+directWriteErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3359,16 +3417,35 @@ func (h *TenantHandler) RemoveRelation(c *fiber.Ctx) error {
|
|||||||
return errorJSON(c, fiber.StatusBadRequest, "userId and relation are required")
|
return errorJSON(c, fiber.StatusBadRequest, "userId and relation are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var directWriteErr error
|
||||||
|
if h.Keto != nil {
|
||||||
|
directWriteErr = h.Keto.DeleteRelation(c.Context(), "Tenant", tenantID, req.Relation, "User:"+req.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
if h.KetoOutbox != nil {
|
if h.KetoOutbox != nil {
|
||||||
|
status := domain.KetoOutboxStatusPending
|
||||||
|
var processedAt *time.Time
|
||||||
|
if directWriteErr == nil && h.Keto != nil {
|
||||||
|
status = domain.KetoOutboxStatusProcessed
|
||||||
|
now := time.Now()
|
||||||
|
processedAt = &now
|
||||||
|
}
|
||||||
|
|
||||||
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
||||||
Namespace: "Tenant",
|
Namespace: "Tenant",
|
||||||
Object: tenantID,
|
Object: tenantID,
|
||||||
Relation: req.Relation,
|
Relation: req.Relation,
|
||||||
Subject: "User:" + req.UserID,
|
Subject: "User:" + req.UserID,
|
||||||
Action: domain.KetoOutboxActionDelete,
|
Action: domain.KetoOutboxActionDelete,
|
||||||
|
Status: status,
|
||||||
|
ProcessedAt: processedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if directWriteErr != nil {
|
||||||
|
return errorJSON(c, fiber.StatusInternalServerError, "Keto 동기화 실패: "+directWriteErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3390,6 +3467,18 @@ func (h *TenantHandler) ListSystemRelations(c *fiber.Ctx) error {
|
|||||||
"auth_guard_viewers": true,
|
"auth_guard_viewers": true,
|
||||||
"api_keys_viewers": true,
|
"api_keys_viewers": true,
|
||||||
"audit_logs_viewers": true,
|
"audit_logs_viewers": true,
|
||||||
|
|
||||||
|
"overview_managers": true,
|
||||||
|
"tenants_managers": true,
|
||||||
|
"org_chart_managers": true,
|
||||||
|
"worksmobile_managers": true,
|
||||||
|
"ory_ssot_managers": true,
|
||||||
|
"data_integrity_managers": true,
|
||||||
|
"users_managers": true,
|
||||||
|
"permissions_direct_managers": true,
|
||||||
|
"auth_guard_managers": true,
|
||||||
|
"api_keys_managers": true,
|
||||||
|
"audit_logs_managers": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
type userRelationInfo struct {
|
type userRelationInfo struct {
|
||||||
@@ -3474,6 +3563,18 @@ func (h *TenantHandler) AddSystemRelation(c *fiber.Ctx) error {
|
|||||||
"auth_guard_viewers": true,
|
"auth_guard_viewers": true,
|
||||||
"api_keys_viewers": true,
|
"api_keys_viewers": true,
|
||||||
"audit_logs_viewers": true,
|
"audit_logs_viewers": true,
|
||||||
|
|
||||||
|
"overview_managers": true,
|
||||||
|
"tenants_managers": true,
|
||||||
|
"org_chart_managers": true,
|
||||||
|
"worksmobile_managers": true,
|
||||||
|
"ory_ssot_managers": true,
|
||||||
|
"data_integrity_managers": true,
|
||||||
|
"users_managers": true,
|
||||||
|
"permissions_direct_managers": true,
|
||||||
|
"auth_guard_managers": true,
|
||||||
|
"api_keys_managers": true,
|
||||||
|
"audit_logs_managers": true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if !allowedRelations[req.Relation] {
|
if !allowedRelations[req.Relation] {
|
||||||
@@ -3487,16 +3588,35 @@ func (h *TenantHandler) AddSystemRelation(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var directWriteErr error
|
||||||
|
if h.Keto != nil {
|
||||||
|
directWriteErr = h.Keto.CreateRelation(c.Context(), "System", "system", req.Relation, "User:"+req.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
if h.KetoOutbox != nil {
|
if h.KetoOutbox != nil {
|
||||||
|
status := domain.KetoOutboxStatusPending
|
||||||
|
var processedAt *time.Time
|
||||||
|
if directWriteErr == nil && h.Keto != nil {
|
||||||
|
status = domain.KetoOutboxStatusProcessed
|
||||||
|
now := time.Now()
|
||||||
|
processedAt = &now
|
||||||
|
}
|
||||||
|
|
||||||
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
||||||
Namespace: "System",
|
Namespace: "System",
|
||||||
Object: "system",
|
Object: "system",
|
||||||
Relation: req.Relation,
|
Relation: req.Relation,
|
||||||
Subject: "User:" + req.UserID,
|
Subject: "User:" + req.UserID,
|
||||||
Action: domain.KetoOutboxActionCreate,
|
Action: domain.KetoOutboxActionCreate,
|
||||||
|
Status: status,
|
||||||
|
ProcessedAt: processedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if directWriteErr != nil {
|
||||||
|
return errorJSON(c, fiber.StatusInternalServerError, "Keto 동기화 실패: "+directWriteErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3510,15 +3630,34 @@ func (h *TenantHandler) RemoveSystemRelation(c *fiber.Ctx) error {
|
|||||||
return errorJSON(c, fiber.StatusBadRequest, "userId and relation are required")
|
return errorJSON(c, fiber.StatusBadRequest, "userId and relation are required")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var directWriteErr error
|
||||||
|
if h.Keto != nil {
|
||||||
|
directWriteErr = h.Keto.DeleteRelation(c.Context(), "System", "system", req.Relation, "User:"+req.UserID)
|
||||||
|
}
|
||||||
|
|
||||||
if h.KetoOutbox != nil {
|
if h.KetoOutbox != nil {
|
||||||
|
status := domain.KetoOutboxStatusPending
|
||||||
|
var processedAt *time.Time
|
||||||
|
if directWriteErr == nil && h.Keto != nil {
|
||||||
|
status = domain.KetoOutboxStatusProcessed
|
||||||
|
now := time.Now()
|
||||||
|
processedAt = &now
|
||||||
|
}
|
||||||
|
|
||||||
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
_ = h.KetoOutbox.Create(c.Context(), &domain.KetoOutbox{
|
||||||
Namespace: "System",
|
Namespace: "System",
|
||||||
Object: "system",
|
Object: "system",
|
||||||
Relation: req.Relation,
|
Relation: req.Relation,
|
||||||
Subject: "User:" + req.UserID,
|
Subject: "User:" + req.UserID,
|
||||||
Action: domain.KetoOutboxActionDelete,
|
Action: domain.KetoOutboxActionDelete,
|
||||||
|
Status: status,
|
||||||
|
ProcessedAt: processedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if directWriteErr != nil {
|
||||||
|
return errorJSON(c, fiber.StatusInternalServerError, "Keto 동기화 실패: "+directWriteErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
return c.SendStatus(fiber.StatusOK)
|
return c.SendStatus(fiber.StatusOK)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ func TestTenantHandler_Relations(t *testing.T) {
|
|||||||
app.Post("/tenants/:id/relations", h.AddRelation)
|
app.Post("/tenants/:id/relations", h.AddRelation)
|
||||||
|
|
||||||
mockKeto.On("ListRelations", mock.Anything, "Tenant", tenantID, "schema_managers", "User:"+userID).Return([]service.RelationTuple{}, nil).Once()
|
mockKeto.On("ListRelations", mock.Anything, "Tenant", tenantID, "schema_managers", "User:"+userID).Return([]service.RelationTuple{}, nil).Once()
|
||||||
|
mockKeto.On("CreateRelation", mock.Anything, "Tenant", tenantID, "schema_managers", "User:"+userID).Return(nil).Once()
|
||||||
|
|
||||||
body, _ := json.Marshal(map[string]string{
|
body, _ := json.Marshal(map[string]string{
|
||||||
"userId": userID,
|
"userId": userID,
|
||||||
@@ -135,6 +136,7 @@ func TestTenantHandler_Relations(t *testing.T) {
|
|||||||
assert.Len(t, outboxEntries, 1)
|
assert.Len(t, outboxEntries, 1)
|
||||||
assert.Equal(t, "Tenant", outboxEntries[0].Namespace)
|
assert.Equal(t, "Tenant", outboxEntries[0].Namespace)
|
||||||
assert.Equal(t, "User:"+userID, outboxEntries[0].Subject)
|
assert.Equal(t, "User:"+userID, outboxEntries[0].Subject)
|
||||||
|
assert.Equal(t, domain.KetoOutboxStatusProcessed, outboxEntries[0].Status)
|
||||||
mockKeto.AssertExpectations(t)
|
mockKeto.AssertExpectations(t)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -142,6 +144,8 @@ func TestTenantHandler_Relations(t *testing.T) {
|
|||||||
app := fiber.New()
|
app := fiber.New()
|
||||||
app.Delete("/tenants/:id/relations", h.RemoveRelation)
|
app.Delete("/tenants/:id/relations", h.RemoveRelation)
|
||||||
|
|
||||||
|
mockKeto.On("DeleteRelation", mock.Anything, "Tenant", tenantID, "schema_managers", "User:"+userID).Return(nil).Once()
|
||||||
|
|
||||||
body, _ := json.Marshal(map[string]string{
|
body, _ := json.Marshal(map[string]string{
|
||||||
"userId": userID,
|
"userId": userID,
|
||||||
"relation": "schema_managers",
|
"relation": "schema_managers",
|
||||||
@@ -165,6 +169,7 @@ func TestTenantHandler_Relations(t *testing.T) {
|
|||||||
assert.Len(t, outboxEntries, 1)
|
assert.Len(t, outboxEntries, 1)
|
||||||
assert.Equal(t, "Tenant", outboxEntries[0].Namespace)
|
assert.Equal(t, "Tenant", outboxEntries[0].Namespace)
|
||||||
assert.Equal(t, "User:"+userID, outboxEntries[0].Subject)
|
assert.Equal(t, "User:"+userID, outboxEntries[0].Subject)
|
||||||
|
assert.Equal(t, domain.KetoOutboxStatusProcessed, outboxEntries[0].Status)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,6 +246,7 @@ func TestTenantHandler_SystemRelations(t *testing.T) {
|
|||||||
app.Post("/system/relations", h.AddSystemRelation)
|
app.Post("/system/relations", h.AddSystemRelation)
|
||||||
|
|
||||||
mockKeto.On("ListRelations", mock.Anything, "System", "system", "ory_ssot_viewers", "User:"+userID).Return([]service.RelationTuple{}, nil).Once()
|
mockKeto.On("ListRelations", mock.Anything, "System", "system", "ory_ssot_viewers", "User:"+userID).Return([]service.RelationTuple{}, nil).Once()
|
||||||
|
mockKeto.On("CreateRelation", mock.Anything, "System", "system", "ory_ssot_viewers", "User:"+userID).Return(nil).Once()
|
||||||
|
|
||||||
body, _ := json.Marshal(map[string]string{
|
body, _ := json.Marshal(map[string]string{
|
||||||
"userId": userID,
|
"userId": userID,
|
||||||
@@ -264,6 +270,7 @@ func TestTenantHandler_SystemRelations(t *testing.T) {
|
|||||||
assert.Len(t, outboxEntries, 1)
|
assert.Len(t, outboxEntries, 1)
|
||||||
assert.Equal(t, "System", outboxEntries[0].Namespace)
|
assert.Equal(t, "System", outboxEntries[0].Namespace)
|
||||||
assert.Equal(t, "User:"+userID, outboxEntries[0].Subject)
|
assert.Equal(t, "User:"+userID, outboxEntries[0].Subject)
|
||||||
|
assert.Equal(t, domain.KetoOutboxStatusProcessed, outboxEntries[0].Status)
|
||||||
mockKeto.AssertExpectations(t)
|
mockKeto.AssertExpectations(t)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user