feat: select users in file access explanations
This commit is contained in:
@@ -978,7 +978,7 @@ manifest = ModuleManifest(
|
||||
summary="List, grant, update, expire, or revoke direct read, write, and manage access without changing ownership.",
|
||||
body=(
|
||||
"File owners and file administrators can manage direct shares for users, groups, the tenant, and Campaign. Expired and revoked grants stop authorizing access immediately while independent active grants remain effective. "
|
||||
"The Files share dialog lists active and historical grants, and revocation is idempotent."
|
||||
"The Files share dialog lists active and historical grants, and revocation is idempotent. The access explanation action evaluates the signed-in user by default. When Policy permits selected-user diagnostics, the same shared dialog can evaluate another active user in the current tenant; those cross-user diagnostics are recorded in audit evidence and do not grant access."
|
||||
),
|
||||
layer="available",
|
||||
documentation_types=("user",),
|
||||
@@ -1003,7 +1003,7 @@ manifest = ModuleManifest(
|
||||
kind="repository",
|
||||
),
|
||||
),
|
||||
related_modules=("campaigns",),
|
||||
related_modules=("access", "audit", "campaigns", "policy"),
|
||||
unlocks=(
|
||||
"A supporting process can grant governed file access without changing file ownership.",
|
||||
),
|
||||
@@ -1025,6 +1025,7 @@ manifest = ModuleManifest(
|
||||
"limitations": [
|
||||
"Campaign-target grants are normally created by the Campaign integration rather than selected manually in the Files dialog.",
|
||||
"A user may retain access through another active direct grant or ownership path after one share is revoked.",
|
||||
"Selected-user explanations require Policy permission and are diagnostics only; they do not impersonate the user or grant file access.",
|
||||
],
|
||||
"outcome": "Direct access has the requested permission and lifetime while the managed asset keeps its owner.",
|
||||
"verification": "Test one intended and one denied path, then expire or revoke the grant and verify that only independent access paths remain.",
|
||||
|
||||
@@ -10,11 +10,15 @@ import {
|
||||
type FilesManagedFileLinkTarget,
|
||||
type ReferenceOptionProvider
|
||||
} from "@govoplan/core-webui";
|
||||
export { fetchResourceAccessExplanation } from "@govoplan/core-webui";
|
||||
export {
|
||||
fetchResourceAccessExplanation,
|
||||
fetchResourceAccessExplanationSubjects
|
||||
} from "@govoplan/core-webui";
|
||||
export type {
|
||||
AccessDecisionProvenanceItem,
|
||||
ResourceAccessExplanationUser as AccessExplanationUser,
|
||||
ResourceAccessExplanationResponse
|
||||
ResourceAccessExplanationResponse,
|
||||
ResourceAccessExplanationSubjectsResponse
|
||||
} from "@govoplan/core-webui";
|
||||
|
||||
export type FileSpace = {
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
downloadFile,
|
||||
downloadFilesAsZip,
|
||||
fetchResourceAccessExplanation,
|
||||
fetchResourceAccessExplanationSubjects,
|
||||
listFilesDelta,
|
||||
listFilesByProperties,
|
||||
listFileConnectorProfiles,
|
||||
@@ -52,7 +53,8 @@ import {
|
||||
type FileSpace,
|
||||
type ManagedFile,
|
||||
type RenameResponse,
|
||||
type ResourceAccessExplanationResponse } from
|
||||
type ResourceAccessExplanationResponse,
|
||||
type ResourceAccessExplanationSubjectsResponse } from
|
||||
"../../api/files";
|
||||
import { EMPTY_FILES, EMPTY_FOLDERS, EMPTY_SPACES, INTERNAL_DRAG_TYPE } from "./constants";
|
||||
import { FileConflictDialog, FileContextMenu, FileDialog, FolderTree, RenamePreviewList, TransferFolderSelector } from "./components/FileManagerComponents";
|
||||
@@ -176,6 +178,8 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
const [error, setError] = useState("");
|
||||
const [accessExplanationTarget, setAccessExplanationTarget] = useState<FileAccessExplanationTarget | null>(null);
|
||||
const [resourceAccessExplanation, setResourceAccessExplanation] = useState<ResourceAccessExplanationResponse | null>(null);
|
||||
const [resourceAccessSubjects, setResourceAccessSubjects] = useState<ResourceAccessExplanationSubjectsResponse | null>(null);
|
||||
const [resourceAccessSubjectId, setResourceAccessSubjectId] = useState("");
|
||||
const [resourceAccessLoading, setResourceAccessLoading] = useState(false);
|
||||
const [shareDialogFile, setShareDialogFile] = useState<ManagedFile | null>(null);
|
||||
const { setDragActive, internalDrag, setInternalDrag, dropTargetKey, setDropTargetKey, clearDropState: clearDragDropState } = useFileDragDropState();
|
||||
@@ -1911,14 +1915,25 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
|
||||
async function openAccessExplanation(target: FileAccessExplanationTarget): Promise<void> {
|
||||
if (!auth.user?.id) return;
|
||||
const currentUserId = auth.principal?.membership_id || auth.user?.id;
|
||||
if (!currentUserId) return;
|
||||
setAccessExplanationTarget(target);
|
||||
setResourceAccessExplanation(null);
|
||||
setResourceAccessSubjects(null);
|
||||
setResourceAccessLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const subjects = await fetchResourceAccessExplanationSubjects(settings, {
|
||||
tenantId: (auth.active_tenant ?? auth.tenant)?.id
|
||||
});
|
||||
const initialSubjectId = subjects.users.some((user) => user.id === currentUserId)
|
||||
? currentUserId
|
||||
: subjects.users[0]?.id;
|
||||
if (!initialSubjectId) throw new Error("No permitted access-explanation subject is available.");
|
||||
setResourceAccessSubjects(subjects);
|
||||
setResourceAccessSubjectId(initialSubjectId);
|
||||
setResourceAccessExplanation(await fetchResourceAccessExplanation(settings, {
|
||||
userId: auth.user.id,
|
||||
userId: initialSubjectId,
|
||||
resourceType: target.resourceType,
|
||||
resourceId: target.resourceId,
|
||||
action: target.action,
|
||||
@@ -1932,6 +1947,27 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
}
|
||||
|
||||
async function selectAccessExplanationSubject(userId: string): Promise<void> {
|
||||
if (!accessExplanationTarget || userId === resourceAccessSubjectId) return;
|
||||
setResourceAccessLoading(true);
|
||||
setError("");
|
||||
try {
|
||||
const explanation = await fetchResourceAccessExplanation(settings, {
|
||||
userId,
|
||||
resourceType: accessExplanationTarget.resourceType,
|
||||
resourceId: accessExplanationTarget.resourceId,
|
||||
action: accessExplanationTarget.action,
|
||||
tenantId: (auth.active_tenant ?? auth.tenant)?.id
|
||||
});
|
||||
setResourceAccessExplanation(explanation);
|
||||
setResourceAccessSubjectId(userId);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setResourceAccessLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
function openTransferDialogForContext(menu: ContextMenuState | null, mode: TransferMode) {
|
||||
const sets = selectedSetsForContext(menu);
|
||||
const space = spaceForContext(menu);
|
||||
@@ -2595,13 +2631,16 @@ export default function FilesPage({ settings, auth }: {settings: ApiSettings;aut
|
||||
}
|
||||
|
||||
{accessExplanationTarget &&
|
||||
<FileDialog title="i18n:govoplan-core.access_explanation.75ee7f62" onClose={() => {if (!resourceAccessLoading) {setAccessExplanationTarget(null);setResourceAccessExplanation(null);}}}>
|
||||
<FileDialog title="i18n:govoplan-core.access_explanation.75ee7f62" onClose={() => {if (!resourceAccessLoading) {setAccessExplanationTarget(null);setResourceAccessExplanation(null);setResourceAccessSubjects(null);setResourceAccessSubjectId("");}}}>
|
||||
<ResourceAccessExplanation
|
||||
loading={resourceAccessLoading}
|
||||
explanation={resourceAccessExplanation}
|
||||
subjects={resourceAccessSubjects}
|
||||
selectedUserId={resourceAccessSubjectId}
|
||||
onSelectedUserIdChange={(userId) => void selectAccessExplanationSubject(userId)}
|
||||
fallbackResourceLabel={accessExplanationTarget.label} />
|
||||
<div className="button-row compact-actions align-end">
|
||||
<Button onClick={() => {setAccessExplanationTarget(null);setResourceAccessExplanation(null);}} disabled={resourceAccessLoading}>i18n:govoplan-files.close.bbfa773e</Button>
|
||||
<Button onClick={() => {setAccessExplanationTarget(null);setResourceAccessExplanation(null);setResourceAccessSubjects(null);setResourceAccessSubjectId("");}} disabled={resourceAccessLoading}>i18n:govoplan-files.close.bbfa773e</Button>
|
||||
</div>
|
||||
</FileDialog>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user