@dataroadinc/setup-auth
Version:
CLI tool and programmatic API for automated OAuth setup across cloud platforms
374 lines (373 loc) • 18.1 kB
JavaScript
import { GcpIdentityFactory, } from "../../../providers/gcp/creds/identity.js";
import { GLOBAL_PERMISSIONS, ORGANIZATION_PERMISSIONS, PROJECT_PERMISSIONS, } from "../../../providers/gcp/iam/constants.js";
import { GcpGlobalIamManager } from "../../../providers/gcp/iam/global-iam.js";
import { GcpOrganizationIamManager } from "../../../providers/gcp/iam/organization-iam.js";
import { GcpProjectIamManager } from "../../../providers/gcp/iam/project-iam.js";
import { GcpOrganizationManager } from "../../../providers/gcp/organization.js";
import { GcpProjectManager } from "../../../providers/gcp/project/index.js";
import { enforceUserDomainOrFail, getAdcEmailOrNull, printGcloudAndAdcAccounts, } from "../../../utils/env-handler.js";
import { SetupAuthError } from "../../../utils/error.js";
import { superJoin } from "../../../utils/string.js";
import { Command } from "commander";
function displayTable(data) {
if (data.length === 0) {
console.log("No data to display");
return;
}
console.table(data);
}
export class GcpOrganizationViewer {
constructor(identity, organizationId, enableAutoGrant = false) {
this.initialized = false;
this.userEmail = "";
this.identity = identity;
this.organizationId = organizationId;
this.enableAutoGrant = enableAutoGrant;
this.identity
.getCurrentUserEmail()
.then(email => {
if (email) {
this.userEmail = email;
}
})
.catch(() => {
});
this.organizationManager = new GcpOrganizationManager(identity, organizationId);
this.projectManager = new GcpProjectManager(identity, organizationId);
}
async initialize() {
if (this.initialized) {
return;
}
if (!this.userEmail) {
try {
const email = await this.identity.getCurrentUserEmail();
if (!email) {
throw new SetupAuthError("User email could not be retrieved via identity method and was empty.");
}
this.userEmail = email;
}
catch (error) {
if (error instanceof SetupAuthError)
throw error;
throw new SetupAuthError("Failed to initialize GcpOrganizationViewer due to email retrieval failure", { cause: error });
}
}
await this.organizationManager.initialize();
await this.projectManager.initialize();
if (!this.organizationIamManager) {
this.organizationIamManager = new GcpOrganizationIamManager(this.identity, this.organizationId);
}
if (!this.globalIamManager) {
this.globalIamManager = new GcpGlobalIamManager(this.identity);
}
await this.organizationIamManager.initialize();
await this.globalIamManager.initialize();
this.initialized = true;
}
async checkPermissions(retryCount = 0) {
const MAX_RETRIES = 3;
const permissionChecks = [];
let missingPermissions = {
global: [],
organization: [],
};
let failureReasons = {
global: [],
organization: [],
};
try {
const globalPermissionStatus = await this.globalIamManager.checkPermissions();
for (const permission of Object.values(GLOBAL_PERMISSIONS)) {
const isMissing = globalPermissionStatus.missingPermissions.includes(permission);
if (isMissing) {
missingPermissions.global.push(permission);
}
permissionChecks.push({
Level: "Global",
Scope: "User",
Permission: permission,
Status: isMissing ? "❌ Missing" : "✅ Granted",
});
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.warn(`Failed to check global permissions: ${errorMessage}`);
for (const permission of Object.values(GLOBAL_PERMISSIONS)) {
missingPermissions.global.push(permission);
permissionChecks.push({
Level: "Global",
Scope: "User",
Permission: permission,
Status: "❌ Missing",
});
}
}
try {
const orgPermissionStatus = await this.organizationIamManager.checkPermissions();
for (const permission of Object.values(ORGANIZATION_PERMISSIONS)) {
const isMissing = orgPermissionStatus.missingPermissions.includes(permission);
if (isMissing) {
missingPermissions.organization.push(permission);
}
permissionChecks.push({
Level: "Organization",
Scope: this.organizationId,
Permission: permission,
Status: isMissing ? "❌ Missing" : "✅ Granted",
});
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.warn(`Failed to check organization permissions: ${errorMessage}`);
for (const permission of Object.values(ORGANIZATION_PERMISSIONS)) {
missingPermissions.organization.push(permission);
permissionChecks.push({
Level: "Organization",
Scope: this.organizationId,
Permission: permission,
Status: "❌ Missing",
});
}
}
if (this.enableAutoGrant &&
(missingPermissions.global.length > 0 ||
missingPermissions.organization.length > 0)) {
if (retryCount >= MAX_RETRIES) {
const errorMessage = `Maximum retry attempts reached. The following permissions could not be granted:\n` +
`- Global permissions: ${superJoin(missingPermissions.global)}\n` +
`- Organization permissions: ${superJoin(missingPermissions.organization)}\n\n` +
`Reasons:\n` +
`- Global: ${superJoin(failureReasons.global)} \n` +
`- Organization: ${superJoin(failureReasons.organization)} `;
throw new SetupAuthError(errorMessage);
}
console.log(`\nAttempting to grant missing permissions(attempt ${retryCount + 1} of ${MAX_RETRIES})...`);
if (missingPermissions.global.length > 0) {
try {
console.log("Granting global permissions...");
await this.globalIamManager.ensurePermissions();
const updatedGlobalStatus = await this.globalIamManager.checkPermissions();
missingPermissions.global = updatedGlobalStatus.missingPermissions;
if (missingPermissions.global.length === 0) {
console.log("✅ Successfully granted all global permissions.");
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.warn(`Failed to grant global permissions: ${errorMessage} `);
failureReasons.global.push(`Global permissions: ${errorMessage} `);
}
}
if (missingPermissions.organization.length > 0) {
try {
console.log("Granting organization permissions...");
await this.organizationIamManager.ensurePermissions();
const updatedOrgStatus = await this.organizationIamManager.checkPermissions();
missingPermissions.organization = updatedOrgStatus.missingPermissions;
if (missingPermissions.organization.length === 0) {
console.log("✅ Successfully granted all organization permissions.");
}
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.warn(`Failed to grant organization permissions: ${errorMessage} `);
failureReasons.organization.push(`Organization permissions: ${errorMessage} `);
}
}
if (missingPermissions.global.length === 0 &&
missingPermissions.organization.length === 0) {
return permissionChecks;
}
return this.checkPermissions(retryCount + 1);
}
return permissionChecks;
}
async view() {
try {
await this.initialize();
console.log("\nOrganization Details:");
const organization = await this.organizationManager.getOrganization();
console.log(organization);
console.log("\nChecking Required Permissions:");
const permissionChecks = await this.checkPermissions();
displayTable(permissionChecks);
const missingPermissions = permissionChecks.filter(check => check.Status.includes("❌"));
if (missingPermissions.length > 0) {
console.log("\n⚠️ Missing required permissions. Please grant the following permissions:");
for (const { Permission, Level, Scope } of missingPermissions) {
console.log(`- ${Level} permission: ${Permission} for ${Scope}`);
}
if (!this.enableAutoGrant) {
console.log("\nTip: Run this command with --enable to attempt to grant these permissions automatically.");
}
console.log("\nUnable to proceed with project and OAuth client listing due to missing permissions.");
return;
}
console.log("\nProjects in Organization:");
let projects = [];
try {
projects = await this.projectManager.listProjects();
if (projects.length === 0) {
console.log("No projects found");
}
else {
console.log(`Found ${projects.length} projects: `);
projects.forEach((project, index) => {
console.log(`${index + 1}. ${project} `);
});
}
}
catch (error) {
if (error instanceof Error) {
console.log("\n❌ Unable to list projects:", error.message);
}
else {
console.log("\n❌ Unable to list projects: Unknown error");
}
console.log("This requires the resourcemanager.projects.list permission.");
console.log("Please ensure you have the necessary permissions to list projects in this organization.");
return;
}
}
catch (error) {
if (error instanceof SetupAuthError) {
throw error;
}
throw new SetupAuthError("An error occurred while viewing organization details", { cause: error });
}
}
}
async function checkPermissions(identity, organizationId, projectId) {
const tableData = [];
const missingPermissions = {
global: [],
organization: [],
project: [],
};
console.log("\nChecking Global Permissions...");
const globalIamManager = new GcpGlobalIamManager(identity);
await globalIamManager.initialize();
const globalPermissionStatus = await globalIamManager.checkPermissions();
for (const permission of Object.values(GLOBAL_PERMISSIONS)) {
const isMissing = globalPermissionStatus.missingPermissions.includes(permission);
if (isMissing)
missingPermissions.global.push(permission);
tableData.push({
Level: "Global",
Scope: "User",
Permission: permission,
Status: isMissing ? "❌ Missing" : "✅ Granted",
});
}
console.log("\nChecking Organization Permissions...");
const orgIamManager = new GcpOrganizationIamManager(identity, organizationId);
await orgIamManager.initialize();
const orgPermissionStatus = await orgIamManager.checkPermissions();
for (const permission of Object.values(ORGANIZATION_PERMISSIONS)) {
const isMissing = orgPermissionStatus.missingPermissions.includes(permission);
if (isMissing)
missingPermissions.organization.push(permission);
tableData.push({
Level: "Organization",
Scope: organizationId,
Permission: permission,
Status: isMissing ? "❌ Missing" : "✅ Granted",
});
}
if (projectId) {
console.log(`\nChecking Project Permissions for ${projectId}...`);
const projectIamManager = new GcpProjectIamManager(identity, organizationId, projectId);
await projectIamManager.initialize();
const projectPermissionStatus = await projectIamManager.checkPermissions();
for (const permission of Object.values(PROJECT_PERMISSIONS)) {
const isMissing = projectPermissionStatus.missingPermissions.includes(permission);
if (isMissing)
missingPermissions.project.push(permission);
tableData.push({
Level: "Project",
Scope: projectId,
Permission: permission,
Status: isMissing ? "❌ Missing" : "✅ Granted",
});
}
}
return { tableData, missingPermissions };
}
new Command("view-organization")
.description("View organization details, permissions, and OAuth configuration")
.option("-o, --organization-id <id>", "GCP Organization ID (optional, uses env var GCP_ORGANIZATION_ID if not set)")
.option("-p, --project-id <id>", "GCP Project ID (optional, checks project permissions if provided, uses env var GCP_PROJECT_ID if not set)")
.option("--check-permissions", "Check required permissions for the current user", true)
.option("--check-oauth", "Check OAuth brand and client configuration", true)
.action(async (options) => {
console.log("Starting view-organization command...");
await printGcloudAndAdcAccounts();
const expectedDomain = process.env.EKG_ORG_PRIMARY_DOMAIN;
const adcEmail = await getAdcEmailOrNull();
if (!adcEmail) {
throw new SetupAuthError("Could not determine Application Default Credentials (ADC) email. Please run 'gcloud auth application-default login' and try again.");
}
const adcDomain = adcEmail.split("@")[1] || "";
if (!expectedDomain) {
throw new SetupAuthError("Missing required environment variable: EKG_ORG_PRIMARY_DOMAIN. This tool enforces a fail-fast approach and requires this variable to be set in your .env.local (e.g., EKG_ORG_PRIMARY_DOMAIN=your-domain.com).");
}
if (adcDomain.toLowerCase() !== expectedDomain.toLowerCase()) {
throw new SetupAuthError(`Application Default Credentials (ADC) are for '${adcEmail}', which does not match the required organization domain (${expectedDomain}).\n` +
`Please run 'gcloud auth application-default login' and select your <user>@${expectedDomain} account.`);
}
const identity = GcpIdentityFactory.createAdcIdentity();
try {
const email = await identity.getCurrentUserEmail();
enforceUserDomainOrFail(email);
console.log(`Identity validated for ${email}.`);
}
catch (error) {
console.error("Failed to validate identity:", error);
process.exit(1);
}
const organizationId = options.organizationId || process.env.GCP_ORGANIZATION_ID;
const projectId = options.projectId || process.env.GCP_PROJECT_ID;
if (!organizationId) {
console.error("Organization ID must be provided via --organization-id or GCP_ORGANIZATION_ID env var.");
process.exit(1);
}
if (options.checkPermissions) {
try {
const { tableData, missingPermissions } = await checkPermissions(identity, organizationId, projectId);
console.log("\n--- Permission Check Summary ---");
displayTable(tableData);
const totalMissing = missingPermissions.global.length +
missingPermissions.organization.length +
missingPermissions.project.length;
if (totalMissing > 0) {
console.log("\n⚠️ Missing required permissions:");
if (missingPermissions.global.length > 0)
console.log(" Global:", missingPermissions.global.join(", "));
if (missingPermissions.organization.length > 0)
console.log(" Organization:", missingPermissions.organization.join(", "));
if (missingPermissions.project.length > 0)
console.log(" Project:", missingPermissions.project.join(", "));
}
else {
console.log("\n✅ All checked permissions appear to be granted.");
}
}
catch (error) {
console.error("\n❌ Error checking permissions:", error instanceof Error ? error.message : error);
}
}
if (options.checkOauth && projectId) {
console.log(`\nChecking OAuth configuration for project ${projectId}...`);
try {
const viewer = new GcpOrganizationViewer(identity, organizationId);
await viewer.initialize();
}
catch (error) {
console.error("\n❌ Error checking OAuth configuration:", error instanceof Error ? error.message : error);
}
}
console.log("\nView command finished.");
});