UNPKG

@dataroadinc/setup-auth

Version:

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

231 lines (230 loc) 12.2 kB
import { SetupAuthError } from "../../../utils/error.js"; import { superJoin } from "../../../utils/string.js"; import { OrgPolicyClient } from "@google-cloud/org-policy"; import { backOff } from "exponential-backoff"; import { BACKOFF_OPTIONS } from "../iam/base-iam.js"; import { ORGANIZATION_PERMISSIONS } from "../iam/constants.js"; const SERVICE_USAGE_CONSTRAINT = "constraints/serviceuser.services"; export class GcpOrgPolicyManager { constructor(identity, organizationId, organizationsClient) { this.hasGetPermission = undefined; this.hasSetPermission = undefined; this.constraintsListed = false; this.identity = identity; this.organizationsClient = organizationsClient; this.orgResourceName = `organizations/${organizationId}`; } async initialize() { if (!this.orgPolicyClient) { const auth = await this.identity.getGaxAuthClient(); this.orgPolicyClient = new OrgPolicyClient({ auth }); } if (!this.userEmail) { this.userEmail = await this.identity.getCurrentUserEmail(); if (!this.userEmail) { throw new SetupAuthError("Could not retrieve user email from identity."); } } if (this.orgPolicyClient && !this.constraintsListed) { try { console.log(`DEBUG: Listing constraints for ${this.orgResourceName}...`); const [constraintsList] = await this.orgPolicyClient.listConstraints({ parent: this.orgResourceName, pageSize: 200, }); console.log(`DEBUG: Found ${constraintsList.length} constraints.`); console.log("DEBUG: Sample constraint names:"); constraintsList.slice(0, 10).forEach(c => console.log(` - ${c.name}`)); const targetConstraint = `${this.orgResourceName}/constraints/serviceuser.services`; const foundTarget = constraintsList.some(c => c.name === targetConstraint); console.log(`DEBUG: Does list contain '${targetConstraint}'? ${foundTarget}`); this.constraintsListed = true; } catch (listError) { console.error(`DEBUG: Failed to list constraints for ${this.orgResourceName}:`, listError); } } } async checkOrgPolicyPermissions() { await this.initialize(); if (this.hasGetPermission !== undefined && this.hasSetPermission !== undefined) { return { canGet: this.hasGetPermission, canSet: this.hasSetPermission }; } if (!this.userEmail) { throw new SetupAuthError("User email is required for permission check."); } const permissionsToCheck = [ ORGANIZATION_PERMISSIONS.ORG_POLICY_GET, ORGANIZATION_PERMISSIONS.ORG_POLICY_SET, ]; console.log(`Checking if user ${this.userEmail} has the following Org Policy permissions on ${this.orgResourceName}:${superJoin(permissionsToCheck)}`); try { const [response] = await backOff(() => this.organizationsClient.testIamPermissions({ resource: this.orgResourceName, permissions: permissionsToCheck, }), BACKOFF_OPTIONS); const granted = new Set(response.permissions || []); this.hasGetPermission = granted.has(ORGANIZATION_PERMISSIONS.ORG_POLICY_GET); this.hasSetPermission = granted.has(ORGANIZATION_PERMISSIONS.ORG_POLICY_SET); console.log(`Permission check results: GET=${this.hasGetPermission}, SET=${this.hasSetPermission}`); return { canGet: this.hasGetPermission, canSet: this.hasSetPermission }; } catch (error) { console.error(`Failed to check Org Policy permissions [${permissionsToCheck.join(", ")}]:`, error); this.hasGetPermission = false; this.hasSetPermission = false; return { canGet: false, canSet: false }; } } async getOrgLevelPolicy(constraint) { const { canGet } = await this.checkOrgPolicyPermissions(); if (!canGet) { throw new SetupAuthError(`User ${this.userEmail} lacks permission '${ORGANIZATION_PERMISSIONS.ORG_POLICY_GET}' on ${this.orgResourceName}. Cannot fetch Org Policy.`); } await this.initialize(); if (!this.orgPolicyClient) throw new SetupAuthError("OrgPolicyClient not initialized"); const name = `${this.orgResourceName}/policies/${constraint}`; console.log(`DEBUG: Attempting to fetch Org Policy with resource name: "${name}"`); try { const [policy] = await backOff(() => this.orgPolicyClient.getPolicy({ name }), BACKOFF_OPTIONS); return policy; } catch (error) { let code; let message; if (error && typeof error === "object") { if ("code" in error) code = error.code; if ("message" in error) message = error.message; } if (code === 5) { console.log(`No policy found directly on ${this.orgResourceName} for constraint ${constraint}.`); return null; } if (code === 3 && constraint === SERVICE_USAGE_CONSTRAINT) { console.warn(`WORKAROUND: Received INVALID_ARGUMENT (Code 3) fetching policy for ${SERVICE_USAGE_CONSTRAINT}, ` + `but proceeding as GET permission exists and effective policy is likely allowAll. Check GCP console if issues persist.`); return null; } if (code === 3) { console.error(`Received INVALID_ARGUMENT (Code 3) fetching policy ${name}, despite having GET permission. This is unexpected.`); throw new SetupAuthError(`Failed to fetch organization policy for ${constraint} due to unexpected INVALID_ARGUMENT (Code: 3), even though GET permission exists. ` + `This might indicate an issue with the constraint configuration or the API/client library. Please check the constraint '${constraint}' in the GCP console for potential issues. Original error: ${message || String(error)}`, { cause: error instanceof Error ? error : new Error(message || String(error)), }); } console.error(`Error fetching policy ${name}:`, error); const errorCodeMessage = code ? ` (Code: ${code})` : ""; throw new SetupAuthError(`Failed to fetch organization policy for ${constraint}${errorCodeMessage}`, { cause: error instanceof Error ? error : new Error(message || String(error)), }); } } async ensureServiceAllowedAtOrgLevel(serviceName) { const policy = await this.getOrgLevelPolicy(SERVICE_USAGE_CONSTRAINT); let isAllowed = true; let policyNeedsUpdate = false; let modifiedPolicy = policy ? JSON.parse(JSON.stringify(policy)) : null; if (modifiedPolicy && !modifiedPolicy.spec) modifiedPolicy.spec = {}; if (modifiedPolicy?.spec && !modifiedPolicy.spec.rules) modifiedPolicy.spec.rules = []; if (modifiedPolicy?.spec?.rules && modifiedPolicy.spec.rules.length > 0) { let relevantRuleFound = false; for (let i = 0; i < modifiedPolicy.spec.rules.length; i++) { const rule = modifiedPolicy.spec.rules[i]; if (!rule.values) rule.values = {}; const ruleValues = rule.values; if (ruleValues.deniedValues?.includes(serviceName)) { isAllowed = false; relevantRuleFound = true; ruleValues.deniedValues = ruleValues.deniedValues.filter((v) => v !== serviceName); policyNeedsUpdate = true; console.log(`Removed ${serviceName} from deny list.`); } else if (ruleValues.allowedValues) { relevantRuleFound = true; const isAllowListEnforced = !rule.allowAll && !rule.denyAll; if (isAllowListEnforced && !ruleValues.allowedValues.includes(serviceName)) { isAllowed = false; ruleValues.allowedValues.push(serviceName); policyNeedsUpdate = true; isAllowed = true; console.log(`Added ${serviceName} to allow list.`); } else if (ruleValues.allowedValues.includes(serviceName)) { isAllowed = true; policyNeedsUpdate = false; break; } } else if (rule.denyAll) { isAllowed = false; relevantRuleFound = true; policyNeedsUpdate = false; console.log(`Service blocked by denyAll rule.`); break; } else if (rule.allowAll) { isAllowed = true; relevantRuleFound = true; policyNeedsUpdate = false; break; } } if (!relevantRuleFound) isAllowed = true; } else { isAllowed = true; } if (!isAllowed && !policyNeedsUpdate) { throw new SetupAuthError(`Service ${serviceName} is blocked by Organization Policy (${SERVICE_USAGE_CONSTRAINT}), and automatic modification was not possible or safe. Please review the policy on ${this.orgResourceName}.`); } if (policyNeedsUpdate) { console.log(`Organization policy for ${SERVICE_USAGE_CONSTRAINT} needs update to allow ${serviceName}.`); const { canSet } = await this.checkOrgPolicyPermissions(); if (!canSet) { throw new SetupAuthError(`Executing user ${this.userEmail} lacks permission '${ORGANIZATION_PERMISSIONS.ORG_POLICY_SET}' on ${this.orgResourceName}. ` + `Cannot automatically modify Organization Policy to allow service '${serviceName}'. Please grant the permission or modify the policy manually.`); } if (!modifiedPolicy?.name || !modifiedPolicy?.etag || !modifiedPolicy?.spec) { throw new SetupAuthError("Cannot update policy: missing name, etag, or spec."); } console.log(`Attempting to update policy ${modifiedPolicy.name} (etag: ${modifiedPolicy.etag})...`); try { const updateRequest = { policy: modifiedPolicy, updateMask: { paths: ["spec"] }, }; await backOff(() => this.orgPolicyClient.updatePolicy(updateRequest), BACKOFF_OPTIONS); console.log(`Successfully updated Org Policy ${modifiedPolicy.name} to allow ${serviceName}.`); } catch (error) { console.error(`Error updating policy ${modifiedPolicy.name}:`, error); throw new SetupAuthError(`Failed to update Organization Policy for ${SERVICE_USAGE_CONSTRAINT} to allow ${serviceName}.`, { cause: error instanceof Error ? error : new Error(String(error)), }); } } else if (!isAllowed) { throw new SetupAuthError(`Service ${serviceName} appears blocked by Organization Policy (${SERVICE_USAGE_CONSTRAINT}), but no modification was attempted. Please review the policy on ${this.orgResourceName}.`); } else { console.log(`Service ${serviceName} appears to be allowed by Organization Policy ${SERVICE_USAGE_CONSTRAINT}.`); } } }