mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-29 22:55:24 +00:00
feat: remove a folder share from its hosts too (#1719)
This commit is contained in:
1 parent
0e9b3db5fc
commit
a42ea3e5e0
5 files changed
+88
-6
No files matched your search
@@ -10,6 +10,7 @@
|
||||
- Hosts can have a color and an icon, which sync to the desktop and mobile apps
|
||||
- A sidebar setting to turn off alternating row colors
|
||||
- TERMIX_NGINX_TEMPLATE, TERMIX_NGINX_CONF, TERMIX_NGINX_PID and TERMIX_APP_DIR let an install outside Docker, like the Proxmox LXC script, switch nginx to HTTPS when a certificate is added
|
||||
- Removing a user or role from a shared folder can also remove their access from every host in the folder and its subfolders
|
||||
|
||||
### Fixed
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { isAuthOverrideProtocol } from "../../hosts/protocol-auth/registry.js";
|
||||
import {
|
||||
assertHostsUnlocked,
|
||||
hostLock,
|
||||
hasHostLocks,
|
||||
requireUnlockedHost,
|
||||
sendHostLocked,
|
||||
@@ -722,14 +723,40 @@ router.get(
|
||||
},
|
||||
);
|
||||
|
||||
/** The host grants that went to the same user or role as a folder rule. */
|
||||
export function grantsForFolderRule<
|
||||
T extends { userId?: string | null; roleId?: number | null },
|
||||
>(entries: T[], rule: { userId?: string | null; roleId?: number | null }): T[] {
|
||||
return entries.filter((entry) =>
|
||||
rule.userId
|
||||
? entry.userId === rule.userId
|
||||
: rule.roleId != null && entry.roleId === rule.roleId,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @openapi
|
||||
* /rbac/folder/access/{id}:
|
||||
* delete:
|
||||
* summary: Stop a folder share from applying to hosts added later
|
||||
* description: Grants already fanned out to hosts stay; revoke those per host.
|
||||
* description: Grants already fanned out to hosts stay, unless fromHosts=true, which also removes the same user's or role's access from every host in the folder and its subfolders that the caller may manage sharing on.
|
||||
* tags:
|
||||
* - RBAC
|
||||
* parameters:
|
||||
* - in: path
|
||||
* name: id
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* - in: query
|
||||
* name: fromHosts
|
||||
* schema:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Removed. removedFromHosts counts the host grants removed.
|
||||
* 404:
|
||||
* description: No such rule.
|
||||
*/
|
||||
router.delete(
|
||||
"/folder/access/:id",
|
||||
@@ -741,11 +768,35 @@ router.delete(
|
||||
}
|
||||
try {
|
||||
const repository = createCurrentFolderAccessRepository();
|
||||
if (!(await repository.findById(ruleId, req.userId!))) {
|
||||
const rule = await repository.findById(ruleId, req.userId!);
|
||||
if (!rule) {
|
||||
return res.status(404).json({ error: "Rule not found" });
|
||||
}
|
||||
await repository.deleteById(ruleId, req.userId!);
|
||||
res.json({ success: true });
|
||||
|
||||
let removedFromHosts = 0;
|
||||
if (req.query.fromHosts === "true") {
|
||||
const hosts =
|
||||
await createCurrentHostFolderRepository().listHostsInFolder(
|
||||
req.userId!,
|
||||
rule.folder,
|
||||
);
|
||||
const access = createCurrentRbacAccessRepository();
|
||||
for (const host of hosts) {
|
||||
if (hostLock(host.id)) continue;
|
||||
if (!(await canManageHostSharing(req.userId!, host.id)).allowed)
|
||||
continue;
|
||||
let changed = false;
|
||||
const entries = await access.listHostAccess(host.id);
|
||||
for (const entry of grantsForFolderRule(entries, rule)) {
|
||||
await access.revokeHostAccess(entry.id, host.id);
|
||||
removedFromHosts++;
|
||||
changed = true;
|
||||
}
|
||||
if (changed) await pruneHostGrants(host.id);
|
||||
}
|
||||
}
|
||||
res.json({ success: true, removedFromHosts });
|
||||
} catch (error) {
|
||||
databaseLogger.error("Failed to delete folder access rule", error, {
|
||||
operation: "delete_folder_access",
|
||||
|
||||
@@ -69,7 +69,7 @@ vi.mock("../../../database/repositories/factory.js", () => ({
|
||||
}),
|
||||
}));
|
||||
|
||||
const { pruneHostGrants, pruneCredentialGrants } =
|
||||
const { pruneHostGrants, pruneCredentialGrants, grantsForFolderRule } =
|
||||
await import("../../../database/routes/rbac.js");
|
||||
|
||||
const chain = (): Grant[] => [
|
||||
@@ -105,3 +105,27 @@ describe("share pruning", () => {
|
||||
expect(state.credentialGrants.map((g) => g.userId)).toEqual(["dave"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("grantsForFolderRule", () => {
|
||||
const entries = [
|
||||
{ id: 1, userId: "bob", roleId: null },
|
||||
{ id: 2, userId: null, roleId: 7 },
|
||||
{ id: 3, userId: "carol", roleId: null },
|
||||
];
|
||||
|
||||
it("picks the grants of the rule's user or role", () => {
|
||||
expect(
|
||||
grantsForFolderRule(entries, { userId: "bob", roleId: null }).map(
|
||||
(e) => e.id,
|
||||
),
|
||||
).toEqual([1]);
|
||||
expect(
|
||||
grantsForFolderRule(entries, { userId: null, roleId: 7 }).map(
|
||||
(e) => e.id,
|
||||
),
|
||||
).toEqual([2]);
|
||||
expect(
|
||||
grantsForFolderRule(entries, { userId: null, roleId: null }),
|
||||
).toEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -238,9 +238,14 @@ export async function getFolderAccess(
|
||||
}
|
||||
}
|
||||
|
||||
export async function revokeFolderAccess(ruleId: number): Promise<void> {
|
||||
export async function revokeFolderAccess(
|
||||
ruleId: number,
|
||||
options: { fromHosts?: boolean } = {},
|
||||
): Promise<void> {
|
||||
try {
|
||||
await rbacApi.delete(`/rbac/folder/access/${ruleId}`);
|
||||
await rbacApi.delete(`/rbac/folder/access/${ruleId}`, {
|
||||
params: options.fromHosts ? { fromHosts: "true" } : undefined,
|
||||
});
|
||||
} catch (error) {
|
||||
throw handleApiError(error, "revoke folder access");
|
||||
}
|
||||
|
||||
@@ -777,6 +777,7 @@
|
||||
"sharedBadge": "Shared",
|
||||
"sharedBadgeTooltip": "Shared by {{owner}} ({{level}} access)",
|
||||
"viewOnlyBanner": "This host is shared with you by {{owner}} with view access. The configuration is read-only.",
|
||||
"folderRuleRemoveFromHosts": "Also remove this access from every host in the folder and its subfolders",
|
||||
"sharedCopyBanner": "This host is shared with you by {{owner}}. The desktop keeps a read-only copy, so change it on the server and it syncs here.",
|
||||
"sharedEditBanner": "This host is shared with you by {{owner}} with edit access. Changes apply to the real host; authentication references can only be changed by the owner.",
|
||||
"ownerOnlyControl": "Only the host owner can change this",
|
||||
|
||||
Reference in new issue
Block a user