UNPKG

@dataroadinc/setup-auth

Version:

CLI tool and programmatic API for automated OAuth setup across cloud platforms

374 lines (373 loc) 18.1 kB
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."); });