@dataroadinc/setup-auth
Version:
CLI tool and programmatic API for automated OAuth setup across cloud platforms
231 lines (230 loc) • 12.2 kB
JavaScript
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}.`);
}
}
}