Skip to content

Commit 001a55b

Browse files
authored
Merge pull request #152 from fensak-io/main
Release
2 parents d390ac9 + 0a8409b commit 001a55b

File tree

12 files changed

+595
-244
lines changed

12 files changed

+595
-244
lines changed

config/custom-environment-variables.json5

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{
66
env: "FENSAK_ENV",
77
appURL: "FENSAK_APP_URL",
8+
dashboardAppURL: "FENSAK_DASHBOARD_APP_URL",
89
configFileSizeLimit: {
910
__name: "FENSAK_CONFIG_FILE_SIZE_LIMIT",
1011
__format: "number",
@@ -21,8 +22,8 @@
2122
__name: "FENSAK_PLAN_REPO_LIMITS",
2223
__format: "json",
2324
},
24-
plansAllowedMultipleOrgs: {
25-
__name: "FENSAK_PLANS_ALLOWED_MULTIPLE_ORGS",
25+
plansAllowedMultipleAccounts: {
26+
__name: "FENSAK_PLANS_ALLOWED_MULTIPLE_ACCOUNTS",
2627
__format: "json",
2728
},
2829
managementAPI: {
@@ -31,6 +32,10 @@
3132
__format: "boolean",
3233
},
3334
eventSecretKey: "FENSAK_MANAGEMENT_API_EVENT_SECRET_KEY",
35+
sharedCryptoEncryptionKeys: {
36+
__name: "FENSAK_MANAGEMENT_API_SHARED_CRYPTO_ENCRYPTION_KEYS",
37+
__format: "json",
38+
},
3439
allowedCORSOrigins: {
3540
__name: "FENSAK_MANAGEMENT_API_ALLOWED_CORS_ORIGINS",
3641
__format: "json",

config/default.json5

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
// The URL where the app is hosted. Used for constructing self-referencing URLs for the app.
1313
appURL: "",
1414

15+
// The URL where the dashboard app is hosted. Used for constructing redirecting URLs for certain workflows
16+
// (e.g., post install redirect for BitBucket).
17+
dashboardAppURL: "",
18+
1519
// The maximum size of config files that can be accepted (in bytes).
1620
configFileSizeLimit: 1024000,
1721

@@ -28,8 +32,8 @@
2832
"": 5,
2933
},
3034

31-
// A list of subscription plan names that are allowed to link multiple Orgs.
32-
plansAllowedMultipleOrgs: [],
35+
// A list of subscription plan names that are allowed to link multiple Accounts.
36+
plansAllowedMultipleAccounts: [],
3337

3438
/**
3539
* Settings related to the management API.
@@ -41,6 +45,9 @@
4145
// The secret key for signing webhook events.
4246
eventSecretKey: "",
4347

48+
// A set of shared secret keys to use for encrypting secrets that can be decrypted by multiple Fensak services.
49+
sharedCryptoEncryptionKeys: [],
50+
4451
// Allowed CORS origins.
4552
allowedCORSOrigins: [],
4653
},

deployments/dev/app.docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ services:
3434
- "FENSAK_GITHUB_OAUTH_APP_CLIENT_ID"
3535
- "FENSAK_GITHUB_OAUTH_APP_CLIENT_SECRET"
3636
- "FENSAK_MANAGEMENT_API_EVENT_SECRET_KEY"
37+
- "FENSAK_MANAGEMENT_API_SHARED_CRYPTO_ENCRYPTION_KEYS"
3738
- "FENSAK_MANAGEMENT_API_ALLOWED_CORS_ORIGINS"
3839
- "FENSAK_PLAN_REPO_LIMITS"
3940
- "FENSAK_PLANS_ALLOWED_MULTIPLE_ORGS"

logging/logger.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,10 +43,12 @@ export function logConfig(): void {
4343
logger.info(`\t- ${key}: ${repoLimits[key]}`);
4444
}
4545

46-
const plansAllowedMultipleOrgs = config.get("plansAllowedMultipleOrgs");
47-
if (plansAllowedMultipleOrgs.length > 0) {
48-
logger.info("plansAllowedMultipleOrgs:");
49-
for (const plan of plansAllowedMultipleOrgs) {
46+
const plansAllowedMultipleAccounts = config.get(
47+
"plansAllowedMultipleAccounts",
48+
);
49+
if (plansAllowedMultipleAccounts.length > 0) {
50+
logger.info("plansAllowedMultipleAccounts:");
51+
for (const plan of plansAllowedMultipleAccounts) {
5052
logger.info(`\t- ${plan}`);
5153
}
5254
} else {

mgmt/accounts.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) Fensak, LLC.
2+
// SPDX-License-Identifier: AGPL-3.0-or-later OR BUSL-1.1
3+
4+
import { Octokit } from "../deps.ts";
5+
6+
import {
7+
FensakConfigSource,
8+
getBitBucketWorkspace,
9+
getComputedFensakConfig,
10+
getGitHubOrgRecord,
11+
} from "../svcdata/mod.ts";
12+
import { isOrgManager } from "../ghstd/mod.ts";
13+
14+
export interface Account {
15+
source: "github" | "bitbucket";
16+
slug: string;
17+
app_is_installed: boolean;
18+
dotfensak_ready: boolean;
19+
subscription_id: string | null;
20+
}
21+
22+
/**
23+
* Filters down the given GitHub Orgs (identified by slug) based on whether the authenticated user of the Octokit client
24+
* is an admin of the org.
25+
*/
26+
export async function filterAllowedGitHubOrgsForAuthenticatedUser(
27+
octokit: Octokit,
28+
slugs: string[],
29+
): Promise<Account[]> {
30+
const orgData = await Promise.all(slugs.map((sl) => getGitHubOrgRecord(sl)));
31+
const allowedOrgs = await Promise.all(
32+
orgData.map(async (od): Promise<Account | null> => {
33+
if (od.value == null) {
34+
return null;
35+
}
36+
37+
const isAllowed = await isOrgManager(octokit, od.value.name);
38+
if (!isAllowed) {
39+
return null;
40+
}
41+
42+
const maybeCfg = await getComputedFensakConfig(
43+
FensakConfigSource.GitHub,
44+
od.value.name,
45+
);
46+
47+
return {
48+
source: "github",
49+
slug: od.value.name,
50+
app_is_installed: od.value.installationID != null,
51+
dotfensak_ready: maybeCfg != null,
52+
subscription_id: od.value.subscriptionID,
53+
};
54+
}),
55+
);
56+
const out: Account[] = [];
57+
for (const o of allowedOrgs) {
58+
if (!o) {
59+
continue;
60+
}
61+
out.push(o);
62+
}
63+
return out;
64+
}
65+
66+
/**
67+
* Filters down the given BitBucket Workspaces (identified by slug) based on whether the authenticated user
68+
* is an admin of the workspace.
69+
*/
70+
export async function filterAllowedBitBucketWorkspacesForAuthenticatedUser(
71+
token: string,
72+
slugs: string[],
73+
): Promise<Account[]> {
74+
const wsLookup = await getWorkspacePermissionLookup(token);
75+
const wsData = await Promise.all(
76+
slugs.map((sl) => getBitBucketWorkspace(sl)),
77+
);
78+
const allowedWS = await Promise.all(
79+
wsData.map(async (ws): Promise<Account | null> => {
80+
if (ws.value == null) {
81+
return null;
82+
}
83+
84+
const isAllowed = wsLookup[ws.value.name] === "owner";
85+
if (!isAllowed) {
86+
return null;
87+
}
88+
89+
const maybeCfg = await getComputedFensakConfig(
90+
FensakConfigSource.BitBucket,
91+
ws.value.name,
92+
);
93+
94+
return {
95+
source: "bitbucket",
96+
slug: ws.value.name,
97+
app_is_installed: ws.value.securityContext != null,
98+
dotfensak_ready: maybeCfg != null,
99+
subscription_id: ws.value.subscriptionID,
100+
};
101+
}),
102+
);
103+
const out: Account[] = [];
104+
for (const w of allowedWS) {
105+
if (!w) {
106+
continue;
107+
}
108+
out.push(w);
109+
}
110+
return out;
111+
}
112+
113+
export async function getWorkspacePermissionLookup(
114+
token: string,
115+
): Promise<Record<string, "owner" | "member">> {
116+
const wsUPath = "/user/permissions/workspaces";
117+
const wsResp = await fetch(
118+
`https://api.bitbucket.org/2.0${wsUPath}`,
119+
{
120+
method: "GET",
121+
headers: {
122+
Authorization: `Bearer ${token}`,
123+
Accept: "application/json",
124+
},
125+
},
126+
);
127+
if (!wsResp.ok) {
128+
const rtext = await wsResp.text();
129+
throw new Error(
130+
`BitBucket API Error for url path ${wsUPath} (${wsResp.status}): ${rtext}`,
131+
);
132+
}
133+
const data = await wsResp.json();
134+
135+
const wsPermissionLookup: Record<string, "owner" | "member"> = {};
136+
for (const wm of data.values) {
137+
wsPermissionLookup[wm.workspace.slug] = wm.permission as "owner" | "member";
138+
}
139+
return wsPermissionLookup;
140+
}

mgmt/mod.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88
* - Subscription Event hooks
99
*/
1010
export * from "./events.ts";
11-
export * from "./organization.ts";
11+
export * from "./accounts.ts";
1212
export * from "./subscription_events.ts";

mgmt/organization.ts

Lines changed: 0 additions & 61 deletions
This file was deleted.

0 commit comments

Comments
 (0)