Skip to content

Commit

Permalink
Merge pull request #4804 from quarto-dev/task/warn-perms
Browse files Browse the repository at this point in the history
Display warning if user doesn't have rights to change permissions #4474
  • Loading branch information
Allen Manning authored Mar 14, 2023
2 parents 19ecc6f + ebb1ed1 commit 94a815f
Show file tree
Hide file tree
Showing 6 changed files with 200 additions and 148 deletions.
88 changes: 69 additions & 19 deletions src/publish/confluence/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import { AccountToken } from "../../provider.ts";
import { ApiError } from "../../types.ts";
import {
AttachmentSummary,
ConfluenceParent,
Content,
ContentArray,
ContentChangeType,
ContentCreate,
ContentDelete,
ContentProperty,
Expand All @@ -26,13 +28,14 @@ import {
} from "./types.ts";

import { DESCENDANT_PAGE_SIZE, V2EDITOR_METADATA } from "../constants.ts";
import { logError, logWarning, trace } from "../confluence-logger.ts";
import { logError, trace } from "../confluence-logger.ts";
import { buildContentCreate } from "../confluence-helper.ts";

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

public getUser(): Promise<User> {
return this.get<User>("user/current");
public getUser(expand = ["operations"]): Promise<User> {
return this.get<User>(`user/current?expand=${expand}`);
}

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

/**
* 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.
*/
public async canSetPermissions(
parent: ConfluenceParent,
space: Space,
user: User
): Promise<boolean> {
let result = true;

const testContent: ContentCreate = buildContentCreate(
`quarto-permission-test-${globalThis.crypto.randomUUID()}`,
space,
{
storage: {
value: "",
representation: "storage",
},
},
"permisson-test"
);
const testContentCreated = await this.createContent(user, testContent);

const testContentId = testContentCreated.id ?? "";

try {
await this.put<Content>(
`content/${testContentId}/restriction/byOperation/update/user?accountId=${user.accountId}`
);
} catch (error) {
trace("lockDownResult Error", error);
// Note, sometimes a successful call throws a
// "SyntaxError: Unexpected end of JSON input"
// check for the 403 status only
if (error?.status === 403) {
result = false;
}
}

const contentDelete: ContentDelete = {
id: testContentId,
contentChangeType: ContentChangeType.delete,
};
await this.deleteContent(contentDelete);

return result;
}

public async lockDownPermissions(
contentId: string,
user: User
): Promise<any> {
try {
return await this.put<Content>(
`content/${contentId}/restriction/byOperation/update/user?accountId=${user.accountId}`
);
} catch (error) {
trace("lockDownResult Error", error);
}
}

public async createContent(
user: User,
content: ContentCreate,
Expand All @@ -107,14 +171,7 @@ export class ConfluenceClient {
const createBody = JSON.stringify(toCreate);
const result: Content = await this.post<Content>("content", createBody);

try {
await this.put<Content>(
`content/${result.id}/restriction/byOperation/update/user?accountId=${user.accountId}`
);
} catch (error) {
//Sometimes the API returns the error 'Unexpected end of JSON input'
trace("lockDownResult Error", error);
}
await this.lockDownPermissions(result.id ?? "", user);

return result;
}
Expand All @@ -134,14 +191,7 @@ export class ConfluenceClient {
JSON.stringify(toUpdate)
);

try {
const lockDownResult = await this.put<Content>(
`content/${content.id}/restriction/byOperation/update/user?accountId=${user.accountId}`
);
} catch (error) {
//Sometimes the API returns the error 'Unexpected end of JSON input'
trace("lockDownResult Error", error);
}
await this.lockDownPermissions(content.id ?? "", user);

return result;
}
Expand Down
5 changes: 5 additions & 0 deletions src/publish/confluence/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ export type User = {
accountId: string;
accountType: "atlassian" | "app";
email: string;
operations: Operation[];
};

export type Operation = {
operation: string;
};

export type Space = {
Expand Down
25 changes: 24 additions & 1 deletion src/publish/confluence/confluence-verify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { AccountToken } from "../provider.ts";
import { ConfluenceClient } from "./api/index.ts";
import { getMessageFromAPIError } from "./confluence-helper.ts";
import { withSpinner } from "../../core/console.ts";
import { ConfluenceParent } from "./api/types.ts";
import { Confirm } from "cliffy/prompt/mod.ts";
import { ConfluenceParent, Space, User } from "./api/types.ts";

const verifyWithSpinner = async (
verifyCommand: () => Promise<void>,
Expand Down Expand Up @@ -58,3 +59,25 @@ export const verifyConfluenceParent = async (
}
await verifyLocation(parentUrl);
};

export const verifyOrWarnManagePermissions = async (
client: ConfluenceClient,
space: Space,
parent: ConfluenceParent,
user: User
) => {
const canManagePermissions = await client.canSetPermissions(
parent,
space,
user
);

if (!canManagePermissions) {
const confirmed: boolean = await Confirm.prompt(
"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?"
);
if (!confirmed) {
throw new Error("");
}
}
};
5 changes: 4 additions & 1 deletion src/publish/confluence/confluence.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { join } from "path/mod.ts";
import { Input, Secret } from "cliffy/prompt/mod.ts";
import { Input, Secret, Confirm } from "cliffy/prompt/mod.ts";
import { RenderFlags } from "../../command/render/types.ts";
import { pathWithForwardSlashes } from "../../core/path.ts";

Expand Down Expand Up @@ -82,6 +82,7 @@ import {
verifyAccountToken,
verifyConfluenceParent,
verifyLocation,
verifyOrWarnManagePermissions,
} from "./confluence-verify.ts";
import {
DELETE_DISABLED,
Expand Down Expand Up @@ -259,6 +260,8 @@ async function publish(

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

await verifyOrWarnManagePermissions(client, space, parent, user);

const uniquifyTitle = async (title: string, idToIgnore: string = "") => {
trace("uniquifyTitle", title);

Expand Down
Loading

0 comments on commit 94a815f

Please sign in to comment.