Skip to content

Commit

Permalink
prepping for reuse?
Browse files Browse the repository at this point in the history
  • Loading branch information
battis committed Jun 16, 2023
1 parent 04ee106 commit 28f3fd3
Show file tree
Hide file tree
Showing 16 changed files with 477 additions and 416 deletions.
30 changes: 30 additions & 0 deletions scripts/actions/createAppEngineInstance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { select } from '@inquirer/prompts';
import fs from 'fs';
import gcloud from '../lib/gcloud.js';
import { options } from '../lib/options.js';

export default async function createAppEngineInstance({ region = undefined }) {
gcloud.invoke('services enable appengine.googleapis.com');
let app = gcloud.invoke('app describe');
if (typeof instance === 'string') {
region =
region ||
(await select({
message: options.region.description,
choices: gcloud
.invoke(`app regions list`)
.map((region) => ({ value: region.region }))
}));
gcloud.invoke(`app create --region=${region}`);
app = gcloud.invoke('app describe');
}

const url = `https://${app.defaultHostname}`;
fs.writeFileSync(
'.env',
`PROJECT=${gcloud.getProjectId()}
URL=${url}`
);

return app;
}
28 changes: 28 additions & 0 deletions scripts/actions/createProject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { confirm } from '@inquirer/prompts';
import cli from '../lib/cli.js';
import gcloud from '../lib/gcloud.js';

export default async function createProject({ projectName }) {
const [project] = gcloud.invoke(
`projects list --filter=projectId=${gcloud.getProjectId()}`
);
if (project) {
if (
!(await confirm({
message: `(Re)configure existing project ${cli.value(
project.projectId
)}?`
}))
) {
throw new Error('must create or reuse project');
}
} else {
let response = gcloud.invoke(
`projects create --name="${projectName}" ${gcloud.getProjectId()}`,
false
);
if (/error/i.test(response)) {
throw new Error(response);
}
}
}
44 changes: 44 additions & 0 deletions scripts/actions/enableBilling.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { confirm, select } from '@inquirer/prompts';
import open from 'open';
import path from 'path';
import cli from '../lib/cli.js';
import gcloud from '../lib/gcloud.js';

export default async function enableBilling({ accountId = undefined }) {
if (!accountId) {
const choices = gcloud
.invokeBeta(`billing accounts list --filter=open=true`)
.map((account) => ({
name: account.displayName,
value: path.basename(account.name)
}));
if (choices.length > 1) {
accountId = await select({
message: 'Select a billing account for this project',
choices
});
} else if (
choices.length === 1 &&
(await confirm({
message: `Use billing account ${cli.value(choices[0].name)}?`
}))
) {
accountId = choices[0].value;
}
}

if (accountId) {
gcloud.invokeBeta(
`billing projects link ${gcloud.getProjectId()} --billing-account="${accountId}"`,
false
);
} else {
await open(
`https://console.cloud.google.com/billing/?project=${gcloud.getProjectId()}`
);
await confirm({
message:
'Confirm that you have created a billing account for this project'
});
}
}
68 changes: 68 additions & 0 deletions scripts/actions/enableIdentityAwareProxy.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { input } from '@inquirer/prompts';
import path from 'path';
import gcloud from '../lib/gcloud.js';
import { options } from '../lib/options.js';
import validators from '../lib/validators.js';

export default async function enableIdentityAwareProxy({
projectName,
supportEmail = undefined,
users = undefined
}) {
gcloud.invoke(`services enable iap.googleapis.com`);
const project = gcloud.invoke(
`projects list --filter=projectId=${gcloud.getProjectId()}`,
false
)[0];
let brand = gcloud.invoke(
`iap oauth-brands list --filter=name=projects/${project.projectNumber}/brands/${project.projectNumber}`
);
brand = brand && brand.shift();
if (!brand) {
supportEmail =
supportEmail ||
(await input({
message: options.supportEmail.description,
validate: validators.email,
default: supportEmail
}));
brand = gcloud.invoke(
`iap oauth-brands create --application_title="${projectName}" --support_email=${supportEmail}`
).name;
}
let oauth = gcloud.invoke(`iap oauth-clients list ${brand.name}`);
oauth = oauth && oauth.shift();
if (!oauth) {
oauth = gcloud.invoke(
`iap oauth-clients create ${brand} --display_name=IAP-App-Engine-app`
);
}
gcloud.invoke(
`iap web enable --resource-type=app-engine --oauth2-client-id=${path.basename(
oauth.name
)} --oauth2-client-secret=${oauth.secret}`
);
users = (
await input({
message: options.users.description,
validate: (value) =>
value
.split(',')
.map((val) => val.trim())
.reduce(
(cond, val) =>
cond && validators.nonEmpty(val) && validators.email(val),
true
) || 'all entries must be email addresses',
default: users
})
)
.split(',')
.map((value) => value.trim());
users.forEach((user) =>
gcloud.invoke(
`projects add-iam-policy-binding ${gcloud.getProjectId()} --member="user:${user}" --role="roles/iap.httpsResourceAccessor"`,
false
)
);
}
10 changes: 10 additions & 0 deletions scripts/actions/guideAuthorizeApp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { confirm } from '@inquirer/prompts';
import open from 'open';
import cli from '../lib/cli.js';

export default async function guideAuthorizeApp({ url }) {
await open(url);
await confirm({
message: `Confirm that you have authorized the app at ${cli.url(url)}`
});
}
44 changes: 44 additions & 0 deletions scripts/actions/guideBlackbaudAppCreation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { confirm, input } from '@inquirer/prompts';
import open from 'open';
import cli from '../lib/cli.js';
import { options } from '../lib/options.js';
import validators from '../lib/validators.js';

export default async function guideBlackbaudAppCreation({
url,
accessKey = undefined,
clientId = undefined,
clientSecret = undefined
}) {
!accessKey && open(options.accessKey.url);
accessKey = await input({
message: `${options.accessKey.description} from ${cli.url(
options.accessKey.url
)}`,
validate: validators.nonEmpty,
default: accessKey
});
!clientId && open(options.clientId.url);
cli.log(`Create a new app at ${cli.url(options.clientId.url)}`);
clientId = await input({
message: options.clientId.description,
validate: validators.nonEmpty,
default: clientId
});
clientSecret = await input({
message: options.clientSecret.description,
validate: validators.nonEmpty,
default: clientSecret
});
const redirectUrl = `${url}/redirect`;
await confirm({
message: `Configure ${cli.value(redirectUrl)} as the app's redirect URL`
});
const scope =
'https://github.com/groton-school/blackbaud-to-google-group-sync/blob/main/docs/blackbaud-api-scope.md';
open(scope);
await confirm({
message: `Limit the SKY API scopes as described at ${cli.url(scope)}`
});
return { accessKey, clientId, clientSecret, redirectUrl };
}
67 changes: 67 additions & 0 deletions scripts/actions/guideWorkspaceAdminDelegation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { confirm, input } from '@inquirer/prompts';
import open from 'open';
import cli from '../lib/cli.js';
import gcloud from '../lib/gcloud.js';
import { options } from '../lib/options.js';
import validators from '../lib/validators.js';

export default async function guideGoogleWorkspaceAdminDelegation({
projectName,
delegatedAdmin = undefined,
delegated = false
}) {
delegatedAdmin = await input({
message: options.delegatedAdmin.description,
validate: validators.email,
default: delegatedAdmin
});
gcloud.invoke(
`projects add-iam-policy-binding ${gcloud.getProjectId()} --member="user:${delegatedAdmin}" --role="roles/owner"`,
false
);
gcloud.invoke('services enable admin.googleapis.com');
const name = projectName
.toLowerCase()
.replace(/[^a-z0-9]/g, '-')
.replace(/--/g, '-');
let serviceAccount = gcloud.invoke(
`iam service-accounts list --filter=email=${name}@${gcloud.getProjectId()}.iam.gserviceaccount.com`
)[0];
if (!serviceAccount) {
serviceAccount = gcloud.invoke(
`iam service-accounts create ${name} --display-name="Google Delegated Admin"`
);
}
/*
* FIXME use Workload Identity Federation
* Service account keys could pose a security risk if compromised. We
* recommend you avoid downloading service account keys and instead use the
* Workload Identity Federation . You can learn more about the best way to
* authenticate service accounts on Google Cloud here.
* https://cloud.google.com/iam/docs/workload-identity-federation
* https://cloud.google.com/blog/products/identity-security/how-to-authenticate-service-accounts-to-help-keep-applications-secure
*/
/*
* FIXME max 10 keys available
* Need to delete one to create one
*/
const credentialsPath = `.cache/${serviceAccount.uniqueId}.json`;
gcloud.invoke(
`iam service-accounts keys create ${credentialsPath} --iam-account=${serviceAccount.email}`
);
if (!delegated) {
const url =
'https://github.com/groton-school/blackbaud-to-google-group-sync/blob/main/docs/google-workspace-admin.md';
open(url);
await confirm({
message: `The Service Account Unique ID is ${cli.value(
serviceAccount.uniqueId
)}
Confirm that ${cli.value(
delegatedAdmin
)} has followed the directions at ${cli.url(url)}`
});
}

return { delegatedAdmin, credentialsPath };
}
23 changes: 23 additions & 0 deletions scripts/actions/initializeProject.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { input } from '@inquirer/prompts';
import gcloud from '../lib/gcloud.js';
import { options } from '../lib/options.js';
import validators from '../lib/validators.js';

export default async function initializeProject({
projectName,
projectId = undefined
}) {
projectName = await input({
message: options.name.description,
validate: validators.maxLength.bind(null, 30),
default: projectName
});
gcloud.setProjectId(
await input({
message: options.project.description,
validate: validators.maxLength.bind(null, 30),
default: projectId || gcloud.getProjectId()
})
);
return projectName;
}
58 changes: 58 additions & 0 deletions scripts/actions/initializeSecretManager.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import fs from 'fs';
import path from 'path';
import cli from '../lib/cli.js';
import gcloud from '../lib/gcloud.js';

export default async function initializeSecretManager({
blackbaud,
googleWorkspace,
serviceAccount
}) {
gcloud.invoke(`services enable secretmanager.googleapis.com`);
const secrets = Array.from(gcloud.invoke('secrets list'));
const values = {
BLACKBAUD_ACCESS_KEY: blackbaud.accessKey,
BLACKBAUD_API_TOKEN: 'null',
BLACKBAUD_CLIENT_ID: blackbaud.clientId,
BLACKBAUD_CLIENT_SECRET: blackbaud.clientSecret,
BLACKBAUD_REDIRECT_URL: blackbaud.redirectUrl,
GOOGLE_DELEGATED_ADMIN: googleWorkspace.delegatedAdmin,
GOOGLE_CREDENTIALS: googleWorkspace.credentialsPath
};
for (const key in values) {
const secret = secrets.reduce((result, secret) => {
if (path.basename(secret.name) === key) {
return secret.name;
}
return result;
}, undefined);
if (secret) {
if (key === 'GOOGLE_CREDENTIALS') {
gcloud.invoke(
`secrets versions add ${secret} --data-file="${values[key]}"`
);
} else {
cli.exec(
`printf "${values[key]
}" | gcloud secrets versions add ${secret} --data-file=- ${gcloud.getFlagsWithProject()}`
);
}
} else {
if (key === 'GOOGLE_CREDENTIALS') {
gcloud.invoke(`secrets create ${key} --data-file="${values[key]}"`);
} else {
cli.exec(
`printf "${values[key]
}" | gcloud secrets create ${key} --data-file=- ${gcloud.getFlagsWithProject()}`
);
}
}
}

fs.unlinkSync(googleWorkspace.credentialsPath);

gcloud.invoke(
`projects add-iam-policy-binding ${gcloud.getProjectId()} --member="serviceAccount:${serviceAccount}" --role="roles/secretmanager.secretAccessor"`,
false
);
}
Loading

0 comments on commit 28f3fd3

Please sign in to comment.