Update version to 0.2.1 and add create-pca script for Azure app management
This commit is contained in:
267
src/create-pca.ts
Normal file
267
src/create-pca.ts
Normal file
@@ -0,0 +1,267 @@
|
||||
#!/usr/bin/env node
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import readline from "node:readline";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { parseArgs } from "node:util";
|
||||
|
||||
type RunAzOptions = {
|
||||
quiet?: boolean;
|
||||
allowFailure?: boolean;
|
||||
};
|
||||
|
||||
type RunAzResult = {
|
||||
status: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
};
|
||||
|
||||
function runAz(args: string[], options: RunAzOptions = {}): RunAzResult {
|
||||
const result = spawnSync("az", args, {
|
||||
encoding: "utf8",
|
||||
stdio: options.quiet
|
||||
? ["ignore", "ignore", "ignore"]
|
||||
: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
if (result.status !== 0 && options.allowFailure !== true) {
|
||||
throw new Error(
|
||||
(result.stderr || "").trim() || `az ${args.join(" ")} failed`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
status: result.status ?? 1,
|
||||
stdout: (result.stdout || "").trim(),
|
||||
stderr: (result.stderr || "").trim(),
|
||||
};
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
const usageText = `Usage: ${path.basename(process.argv[1])} [options] <app-name>
|
||||
Options:
|
||||
-c, --config <path> Write JSON config to file (optional)
|
||||
-h, --help Show this help message and exit`;
|
||||
|
||||
let values: Record<string, string | boolean | undefined>;
|
||||
let positionals: string[];
|
||||
try {
|
||||
({ values, positionals } = parseArgs({
|
||||
args: process.argv.slice(2),
|
||||
options: {
|
||||
help: { type: "boolean", short: "h" },
|
||||
config: { type: "string", short: "c" },
|
||||
},
|
||||
strict: true,
|
||||
allowPositionals: true,
|
||||
}));
|
||||
} catch (err) {
|
||||
console.error(`Error: ${(err as Error).message}`);
|
||||
console.error(usageText);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (values.help) {
|
||||
console.log(usageText);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (positionals.length > 1) {
|
||||
console.error(
|
||||
"Error: Too many positional arguments. Only one app name positional argument is allowed.",
|
||||
);
|
||||
console.error(usageText);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const appName = positionals[0] || "";
|
||||
const configPath = typeof values.config === "string" ? values.config : "";
|
||||
|
||||
if (!appName) {
|
||||
console.error("Error: Application name is required.");
|
||||
console.error(usageText);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let appId = runAz([
|
||||
"ad",
|
||||
"app",
|
||||
"list",
|
||||
"--display-name",
|
||||
appName,
|
||||
"--query",
|
||||
"[0].appId",
|
||||
"-o",
|
||||
"tsv",
|
||||
]).stdout;
|
||||
|
||||
if (appId) {
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stderr,
|
||||
});
|
||||
const answer = await new Promise<string>((resolve) => {
|
||||
rl.question(
|
||||
`Application '${appName}' already exists. Update it? [y/N]: `,
|
||||
(answerValue) => {
|
||||
rl.close();
|
||||
resolve(answerValue.trim());
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
if (!/^(yes|y)$/i.test(answer)) {
|
||||
console.error("Canceled.");
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if (!appId) {
|
||||
appId = runAz([
|
||||
"ad",
|
||||
"app",
|
||||
"create",
|
||||
"--display-name",
|
||||
appName,
|
||||
"--query",
|
||||
"appId",
|
||||
"-o",
|
||||
"tsv",
|
||||
]).stdout;
|
||||
|
||||
if (!appId) {
|
||||
console.error(`Error: Failed to create application '${appName}'.`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
const requiredResourceAccess = [
|
||||
{
|
||||
resourceAppId: "00000003-0000-0000-c000-000000000000",
|
||||
resourceAccess: [
|
||||
{ id: "0e263e50-5827-48a4-b97c-d940288653c7", type: "Scope" },
|
||||
],
|
||||
},
|
||||
{
|
||||
resourceAppId: "499b84ac-1321-427f-aa17-267ca6975798",
|
||||
resourceAccess: [
|
||||
{ id: "ee69721e-6c3a-468f-a9ec-302d16a4c599", type: "Scope" },
|
||||
],
|
||||
},
|
||||
{
|
||||
resourceAppId: "797f4846-ba00-4fd7-ba43-dac1f8f63013",
|
||||
resourceAccess: [
|
||||
{ id: "41094075-9dad-400e-a0bd-54e686782033", type: "Scope" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "create-pca-"));
|
||||
const requiredResourceAccessFile = path.join(
|
||||
tempDir,
|
||||
"required-resource-accesses.json",
|
||||
);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(
|
||||
requiredResourceAccessFile,
|
||||
JSON.stringify(requiredResourceAccess, null, 2),
|
||||
"utf8",
|
||||
);
|
||||
|
||||
try {
|
||||
runAz(
|
||||
[
|
||||
"ad",
|
||||
"app",
|
||||
"update",
|
||||
"--id",
|
||||
appId,
|
||||
"--sign-in-audience",
|
||||
"AzureADMyOrg",
|
||||
"--is-fallback-public-client",
|
||||
"true",
|
||||
"--required-resource-accesses",
|
||||
`@${requiredResourceAccessFile}`,
|
||||
"--public-client-redirect-uris",
|
||||
"http://localhost",
|
||||
`msal${appId}://auth`,
|
||||
"--enable-access-token-issuance",
|
||||
"true",
|
||||
"--enable-id-token-issuance",
|
||||
"true",
|
||||
],
|
||||
{ quiet: true },
|
||||
);
|
||||
} catch {
|
||||
console.error(
|
||||
`Error: Failed to configure application '${appName}' (${appId}).`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
} finally {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
runAz(["ad", "sp", "create", "--id", appId], {
|
||||
quiet: true,
|
||||
allowFailure: true,
|
||||
});
|
||||
|
||||
const adminConsentResult = runAz(
|
||||
["ad", "app", "permission", "admin-consent", "--id", appId],
|
||||
{ quiet: true, allowFailure: true },
|
||||
);
|
||||
if (adminConsentResult.status !== 0) {
|
||||
console.warn(
|
||||
`Warning: Failed to grant admin consent for '${appName}' (${appId}). Continuing without failing.`,
|
||||
);
|
||||
if (adminConsentResult.stderr) {
|
||||
console.warn(adminConsentResult.stderr);
|
||||
}
|
||||
}
|
||||
|
||||
const tenantId = runAz([
|
||||
"account",
|
||||
"show",
|
||||
"--query",
|
||||
"tenantId",
|
||||
"-o",
|
||||
"tsv",
|
||||
]).stdout;
|
||||
if (!tenantId) {
|
||||
console.error(
|
||||
"Error: Failed to resolve tenantId from current Azure CLI context.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const configTemplate = JSON.stringify(
|
||||
{
|
||||
tenantId,
|
||||
clientId: appId,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
if (configPath) {
|
||||
const targetPath = path.resolve(configPath);
|
||||
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
||||
fs.writeFileSync(targetPath, `${configTemplate}\n`, "utf8");
|
||||
}
|
||||
|
||||
console.log(configTemplate);
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(`Error: ${(err as Error).message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user