Skip to content

Commit 94a815f

Browse files
author
Allen Manning
authored
Merge pull request #4804 from quarto-dev/task/warn-perms
Display warning if user doesn't have rights to change permissions #4474
2 parents 19ecc6f + ebb1ed1 commit 94a815f

File tree

6 files changed

+200
-148
lines changed

6 files changed

+200
-148
lines changed

src/publish/confluence/api/index.ts

Lines changed: 69 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ import { AccountToken } from "../../provider.ts";
1212
import { ApiError } from "../../types.ts";
1313
import {
1414
AttachmentSummary,
15+
ConfluenceParent,
1516
Content,
1617
ContentArray,
18+
ContentChangeType,
1719
ContentCreate,
1820
ContentDelete,
1921
ContentProperty,
@@ -26,13 +28,14 @@ import {
2628
} from "./types.ts";
2729

2830
import { DESCENDANT_PAGE_SIZE, V2EDITOR_METADATA } from "../constants.ts";
29-
import { logError, logWarning, trace } from "../confluence-logger.ts";
31+
import { logError, trace } from "../confluence-logger.ts";
32+
import { buildContentCreate } from "../confluence-helper.ts";
3033

3134
export class ConfluenceClient {
3235
public constructor(private readonly token_: AccountToken) {}
3336

34-
public getUser(): Promise<User> {
35-
return this.get<User>("user/current");
37+
public getUser(expand = ["operations"]): Promise<User> {
38+
return this.get<User>(`user/current?expand=${expand}`);
3639
}
3740

3841
public getSpace(spaceId: string, expand = ["homepage"]): Promise<Space> {
@@ -92,6 +95,67 @@ export class ConfluenceClient {
9295
return result?.results ?? [];
9396
}
9497

98+
/**
99+
* Perform a test to see if the user can manage permissions. In the space create a simple test page, attempt to set permissions on it, then delete it.
100+
*/
101+
public async canSetPermissions(
102+
parent: ConfluenceParent,
103+
space: Space,
104+
user: User
105+
): Promise<boolean> {
106+
let result = true;
107+
108+
const testContent: ContentCreate = buildContentCreate(
109+
`quarto-permission-test-${globalThis.crypto.randomUUID()}`,
110+
space,
111+
{
112+
storage: {
113+
value: "",
114+
representation: "storage",
115+
},
116+
},
117+
"permisson-test"
118+
);
119+
const testContentCreated = await this.createContent(user, testContent);
120+
121+
const testContentId = testContentCreated.id ?? "";
122+
123+
try {
124+
await this.put<Content>(
125+
`content/${testContentId}/restriction/byOperation/update/user?accountId=${user.accountId}`
126+
);
127+
} catch (error) {
128+
trace("lockDownResult Error", error);
129+
// Note, sometimes a successful call throws a
130+
// "SyntaxError: Unexpected end of JSON input"
131+
// check for the 403 status only
132+
if (error?.status === 403) {
133+
result = false;
134+
}
135+
}
136+
137+
const contentDelete: ContentDelete = {
138+
id: testContentId,
139+
contentChangeType: ContentChangeType.delete,
140+
};
141+
await this.deleteContent(contentDelete);
142+
143+
return result;
144+
}
145+
146+
public async lockDownPermissions(
147+
contentId: string,
148+
user: User
149+
): Promise<any> {
150+
try {
151+
return await this.put<Content>(
152+
`content/${contentId}/restriction/byOperation/update/user?accountId=${user.accountId}`
153+
);
154+
} catch (error) {
155+
trace("lockDownResult Error", error);
156+
}
157+
}
158+
95159
public async createContent(
96160
user: User,
97161
content: ContentCreate,
@@ -107,14 +171,7 @@ export class ConfluenceClient {
107171
const createBody = JSON.stringify(toCreate);
108172
const result: Content = await this.post<Content>("content", createBody);
109173

110-
try {
111-
await this.put<Content>(
112-
`content/${result.id}/restriction/byOperation/update/user?accountId=${user.accountId}`
113-
);
114-
} catch (error) {
115-
//Sometimes the API returns the error 'Unexpected end of JSON input'
116-
trace("lockDownResult Error", error);
117-
}
174+
await this.lockDownPermissions(result.id ?? "", user);
118175

119176
return result;
120177
}
@@ -134,14 +191,7 @@ export class ConfluenceClient {
134191
JSON.stringify(toUpdate)
135192
);
136193

137-
try {
138-
const lockDownResult = await this.put<Content>(
139-
`content/${content.id}/restriction/byOperation/update/user?accountId=${user.accountId}`
140-
);
141-
} catch (error) {
142-
//Sometimes the API returns the error 'Unexpected end of JSON input'
143-
trace("lockDownResult Error", error);
144-
}
194+
await this.lockDownPermissions(content.id ?? "", user);
145195

146196
return result;
147197
}

src/publish/confluence/api/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@ export type User = {
2828
accountId: string;
2929
accountType: "atlassian" | "app";
3030
email: string;
31+
operations: Operation[];
32+
};
33+
34+
export type Operation = {
35+
operation: string;
3136
};
3237

3338
export type Space = {

src/publish/confluence/confluence-verify.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@ import { AccountToken } from "../provider.ts";
22
import { ConfluenceClient } from "./api/index.ts";
33
import { getMessageFromAPIError } from "./confluence-helper.ts";
44
import { withSpinner } from "../../core/console.ts";
5-
import { ConfluenceParent } from "./api/types.ts";
5+
import { Confirm } from "cliffy/prompt/mod.ts";
6+
import { ConfluenceParent, Space, User } from "./api/types.ts";
67

78
const verifyWithSpinner = async (
89
verifyCommand: () => Promise<void>,
@@ -58,3 +59,25 @@ export const verifyConfluenceParent = async (
5859
}
5960
await verifyLocation(parentUrl);
6061
};
62+
63+
export const verifyOrWarnManagePermissions = async (
64+
client: ConfluenceClient,
65+
space: Space,
66+
parent: ConfluenceParent,
67+
user: User
68+
) => {
69+
const canManagePermissions = await client.canSetPermissions(
70+
parent,
71+
space,
72+
user
73+
);
74+
75+
if (!canManagePermissions) {
76+
const confirmed: boolean = await Confirm.prompt(
77+
"We've detected that your account is not able to manage the permissions for this destination.\n\nPublished pages will be directly editable within the Confluence web editor.\n\nThis means that if you republish the page from Quarto, you may be overwriting the web edits.\nWe recommend that you establish a clear policy about how this published page will be revised.\n\nAre you sure you want to continue?"
78+
);
79+
if (!confirmed) {
80+
throw new Error("");
81+
}
82+
}
83+
};

src/publish/confluence/confluence.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { join } from "path/mod.ts";
2-
import { Input, Secret } from "cliffy/prompt/mod.ts";
2+
import { Input, Secret, Confirm } from "cliffy/prompt/mod.ts";
33
import { RenderFlags } from "../../command/render/types.ts";
44
import { pathWithForwardSlashes } from "../../core/path.ts";
55

@@ -82,6 +82,7 @@ import {
8282
verifyAccountToken,
8383
verifyConfluenceParent,
8484
verifyLocation,
85+
verifyOrWarnManagePermissions,
8586
} from "./confluence-verify.ts";
8687
import {
8788
DELETE_DISABLED,
@@ -259,6 +260,8 @@ async function publish(
259260

260261
trace("publish", { parent, server, id: space.id, key: space.key });
261262

263+
await verifyOrWarnManagePermissions(client, space, parent, user);
264+
262265
const uniquifyTitle = async (title: string, idToIgnore: string = "") => {
263266
trace("uniquifyTitle", title);
264267

0 commit comments

Comments
 (0)