diff --git a/bin/create-app-and-sp.mjs b/bin/create-app-and-sp.mjs new file mode 100644 index 0000000..e1d1a65 --- /dev/null +++ b/bin/create-app-and-sp.mjs @@ -0,0 +1,60 @@ +#!/usr/bin/env node + +import { config } from "../public-config.js"; +import { + createApp, + createSp, + getApp, + getGraphClient, + getServicePrincipal, +} from "../src/graph.js"; +import { parseArgs } from "node:util"; + +async function usage() { + console.log("Usage: create-app-and-sp.mjs --app-name "); +} + +async function main() { + const { client } = await getGraphClient({ + tenantId: config.tenantId, + clientId: config.clientId, + }); + + const args = parseArgs({ + options: { + "app-name": { + type: "string", + short: "n", + }, + }, + }); + + if (!args.values["app-name"]) { + await usage(); + return; + } + + console.log("Will create app with name:", args.values["app-name"]); + + let app = await getApp(client, args.values["app-name"]); + if (!app) { + app = await createApp(client, args.values["app-name"]); + console.log("Created app:", app.appId); + } + + let sp = await getServicePrincipal(client, app.appId); + if (!sp) { + sp = await createSp(client, app.appId); + console.log("Created service principal:", sp.id); + } + + console.log(`The application and associated service principal are ready. +App ID: ${app.appId} +Service Principal ID: ${sp.id}`); +} + +await main().catch((e) => { + console.error("Error in main:", e); + console.error(e.stack); + process.exit(1); +}); diff --git a/bin/delete-app-and-sp.mjs b/bin/delete-app-and-sp.mjs new file mode 100644 index 0000000..5094e15 --- /dev/null +++ b/bin/delete-app-and-sp.mjs @@ -0,0 +1,65 @@ +#!/usr/bin/env node + +import { config } from "../public-config.js"; +import { + deleteApp, + deleteSp, + getApp, + getGraphClient, + getServicePrincipal, +} from "../src/graph.js"; +import { parseArgs } from "node:util"; + +async function usage() { + console.log("Usage: delete-app-and-sp.mjs --app-name "); +} + +async function main() { + const { client } = await getGraphClient({ + tenantId: config.tenantId, + clientId: config.clientId, + }); + + const args = parseArgs({ + options: { + "app-name": { + type: "string", + short: "n", + }, + }, + }); + + if (!args.values["app-name"]) { + await usage(); + return; + } + + console.log("Will delete app with name:", args.values["app-name"]); + + const app = await getApp(client, args.values["app-name"]); + if (!app) { + console.log("No app found with name:", args.values["app-name"]); + return; + } + + const sp = await getServicePrincipal(client, app.appId); + if (sp && sp.id) { + await deleteSp(client, sp.id); + console.log("Deleted service principal:", sp.id); + } else { + console.log("No service principal found for appId:", app.appId); + } + + if (app && app.id) { + await deleteApp(client, app.id); + console.log("Deleted app:", app.appId); + } else { + console.log("App object id missing; cannot delete application"); + } +} + +await main().catch((e) => { + console.error("Error in main:", e); + console.error(e.stack); + process.exit(1); +}); diff --git a/src/graph.js b/src/graph.js new file mode 100644 index 0000000..7195abe --- /dev/null +++ b/src/graph.js @@ -0,0 +1,70 @@ +import { Client } from "@microsoft/microsoft-graph-client"; +import { loginInteractive } from "./azure.js"; + +export async function getGraphClient({ tenantId, clientId }) { + const graphApiToken = await loginInteractive({ + tenantId, + clientId, + scopes: ["https://graph.microsoft.com/.default"], + }); + + const client = Client.init({ + authProvider: (done) => { + done(null, graphApiToken.accessToken); + }, + }); + + return { graphApiToken, client }; +} + +export async function getApp(client, appName) { + const result = await client + .api("/applications") + .filter(`displayName eq '${appName}'`) + .get(); + + // Return the first application found or null if none exists + return result.value.length > 0 ? result.value[0] : null; +} + +export async function getServicePrincipal(client, appId) { + const result = await client + .api("/servicePrincipals") + .filter(`appId eq '${appId}'`) + .get(); + + // Return the first service principal found or null if none exists + return result.value.length > 0 ? result.value[0] : null; +} + +export async function createApp(client, appName) { + const app = await client.api("/applications").post({ + displayName: appName, + }); + + if (!app || !app.appId) { + throw new Error("Failed to create application"); + } + + return app; +} + +export async function createSp(client, appId) { + const sp = await client.api("/servicePrincipals").post({ + appId, + }); + + if (!sp || !sp.id) { + throw new Error("Failed to create service principal"); + } + + return sp; +} + +export async function deleteSp(client, spId) { + await client.api(`/servicePrincipals/${spId}`).delete(); +} + +export async function deleteApp(client, appObjectId) { + await client.api(`/applications/${appObjectId}`).delete(); +}