Skip to content

Commit

Permalink
feat: Added support for managing BitBucket subscriptions through the …
Browse files Browse the repository at this point in the history
…Mgmt APIs

Signed-off-by: Yoriyasu Yano <430092+yorinasub17@users.noreply.github.com>
  • Loading branch information
yorinasub17 committed Nov 2, 2023
1 parent 27483c4 commit 3661454
Show file tree
Hide file tree
Showing 11 changed files with 585 additions and 242 deletions.
8 changes: 6 additions & 2 deletions config/custom-environment-variables.json5
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@
__name: "FENSAK_PLAN_REPO_LIMITS",
__format: "json",
},
plansAllowedMultipleOrgs: {
__name: "FENSAK_PLANS_ALLOWED_MULTIPLE_ORGS",
plansAllowedMultipleAccounts: {
__name: "FENSAK_PLANS_ALLOWED_MULTIPLE_ACCOUNTS",
__format: "json",
},
managementAPI: {
Expand All @@ -31,6 +31,10 @@
__format: "boolean",
},
eventSecretKey: "FENSAK_MANAGEMENT_API_EVENT_SECRET_KEY",
sharedCryptoEncryptionKeys: {
__name: "FENSAK_MANAGEMENT_API_SHARED_CRYPTO_ENCRYPTION_KEYS",
__format: "json",
},
allowedCORSOrigins: {
__name: "FENSAK_MANAGEMENT_API_ALLOWED_CORS_ORIGINS",
__format: "json",
Expand Down
7 changes: 5 additions & 2 deletions config/default.json5
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@
"": 5,
},

// A list of subscription plan names that are allowed to link multiple Orgs.
plansAllowedMultipleOrgs: [],
// A list of subscription plan names that are allowed to link multiple Accounts.
plansAllowedMultipleAccounts: [],

/**
* Settings related to the management API.
Expand All @@ -41,6 +41,9 @@
// The secret key for signing webhook events.
eventSecretKey: "",

// A set of shared secret keys to use for encrypting secrets that can be decrypted by multiple Fensak services.
sharedCryptoEncryptionKeys: [],

// Allowed CORS origins.
allowedCORSOrigins: [],
},
Expand Down
1 change: 1 addition & 0 deletions deployments/dev/app.docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ services:
- "FENSAK_GITHUB_OAUTH_APP_CLIENT_ID"
- "FENSAK_GITHUB_OAUTH_APP_CLIENT_SECRET"
- "FENSAK_MANAGEMENT_API_EVENT_SECRET_KEY"
- "FENSAK_MANAGEMENT_API_SHARED_CRYPTO_ENCRYPTION_KEYS"
- "FENSAK_MANAGEMENT_API_ALLOWED_CORS_ORIGINS"
- "FENSAK_PLAN_REPO_LIMITS"
- "FENSAK_PLANS_ALLOWED_MULTIPLE_ORGS"
Expand Down
10 changes: 6 additions & 4 deletions logging/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,12 @@ export function logConfig(): void {
logger.info(`\t- ${key}: ${repoLimits[key]}`);
}

const plansAllowedMultipleOrgs = config.get("plansAllowedMultipleOrgs");
if (plansAllowedMultipleOrgs.length > 0) {
logger.info("plansAllowedMultipleOrgs:");
for (const plan of plansAllowedMultipleOrgs) {
const plansAllowedMultipleAccounts = config.get(
"plansAllowedMultipleAccounts",
);
if (plansAllowedMultipleAccounts.length > 0) {
logger.info("plansAllowedMultipleAccounts:");
for (const plan of plansAllowedMultipleAccounts) {
logger.info(`\t- ${plan}`);
}
} else {
Expand Down
140 changes: 140 additions & 0 deletions mgmt/accounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// Copyright (c) Fensak, LLC.
// SPDX-License-Identifier: AGPL-3.0-or-later OR BUSL-1.1

import { Octokit } from "../deps.ts";

import {
FensakConfigSource,
getBitBucketWorkspace,
getComputedFensakConfig,
getGitHubOrgRecord,
} from "../svcdata/mod.ts";
import { isOrgManager } from "../ghstd/mod.ts";

export interface Account {
source: "github" | "bitbucket";
slug: string;
app_is_installed: boolean;
dotfensak_ready: boolean;
subscription_id: string | null;
}

/**
* Filters down the given GitHub Orgs (identified by slug) based on whether the authenticated user of the Octokit client
* is an admin of the org.
*/
export async function filterAllowedGitHubOrgsForAuthenticatedUser(
octokit: Octokit,
slugs: string[],
): Promise<Account[]> {
const orgData = await Promise.all(slugs.map((sl) => getGitHubOrgRecord(sl)));
const allowedOrgs = await Promise.all(
orgData.map(async (od): Promise<Account | null> => {
if (od.value == null) {
return null;
}

const isAllowed = await isOrgManager(octokit, od.value.name);
if (!isAllowed) {
return null;
}

const maybeCfg = await getComputedFensakConfig(
FensakConfigSource.GitHub,
od.value.name,
);

return {
source: "github",
slug: od.value.name,
app_is_installed: od.value.installationID != null,
dotfensak_ready: maybeCfg != null,
subscription_id: od.value.subscriptionID,
};
}),
);
const out: Account[] = [];
for (const o of allowedOrgs) {
if (!o) {
continue;
}
out.push(o);
}
return out;
}

/**
* Filters down the given BitBucket Workspaces (identified by slug) based on whether the authenticated user
* is an admin of the workspace.
*/
export async function filterAllowedBitBucketWorkspacesForAuthenticatedUser(
token: string,
slugs: string[],
): Promise<Account[]> {
const wsLookup = await getWorkspacePermissionLookup(token);
const wsData = await Promise.all(
slugs.map((sl) => getBitBucketWorkspace(sl)),
);
const allowedWS = await Promise.all(
wsData.map(async (ws): Promise<Account | null> => {
if (ws.value == null) {
return null;
}

const isAllowed = wsLookup[ws.value.name] === "owner";
if (!isAllowed) {
return null;
}

const maybeCfg = await getComputedFensakConfig(
FensakConfigSource.BitBucket,
ws.value.name,
);

return {
source: "bitbucket",
slug: ws.value.name,
app_is_installed: ws.value.securityContext != null,
dotfensak_ready: maybeCfg != null,
subscription_id: ws.value.subscriptionID,
};
}),
);
const out: Account[] = [];
for (const w of allowedWS) {
if (!w) {
continue;
}
out.push(w);
}
return out;
}

export async function getWorkspacePermissionLookup(
token: string,
): Promise<Record<string, "owner" | "member">> {
const wsUPath = "/user/permissions/workspaces";
const wsResp = await fetch(
`https://api.bitbucket.org/2.0${wsUPath}`,
{
method: "GET",
headers: {
Authorization: `Bearer ${token}`,
Accept: "application/json",
},
},
);
if (!wsResp.ok) {
const rtext = await wsResp.text();
throw new Error(
`BitBucket API Error for url path ${wsUPath} (${wsResp.status}): ${rtext}`,
);
}
const data = await wsResp.json();

const wsPermissionLookup: Record<string, "owner" | "member"> = {};
for (const wm of data.values) {
wsPermissionLookup[wm.workspace.slug] = wm.permission as "owner" | "member";
}
return wsPermissionLookup;
}
2 changes: 1 addition & 1 deletion mgmt/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@
* - Subscription Event hooks
*/
export * from "./events.ts";
export * from "./organization.ts";
export * from "./accounts.ts";
export * from "./subscription_events.ts";
61 changes: 0 additions & 61 deletions mgmt/organization.ts

This file was deleted.

Loading

0 comments on commit 3661454

Please sign in to comment.