Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 121 additions & 7 deletions packages/openops/src/lib/aws/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { BlockAuth, Property } from '@openops/blocks-framework';
import { SharedSystemProp, system } from '@openops/server-shared';
import { parseArn } from './arn-handler';
import { assumeRole } from './sts-common';
import { assumeRole, getAccountId } from './sts-common';

const isImplicitRoleEnabled = system.getBoolean(
SharedSystemProp.AWS_ENABLE_IMPLICIT_ROLE,
Expand Down Expand Up @@ -45,6 +45,7 @@ export async function getCredentialsFromAuth(
auth.defaultRegion,
auth.assumeRoleArn,
auth.assumeRoleExternalId,
auth.endpoint,
);

return {
Expand Down Expand Up @@ -94,6 +95,7 @@ export async function getCredentialsListFromAuth(
auth.defaultRegion,
role.assumeRoleArn,
role.assumeRoleExternalId,
auth.endpoint,
),
);

Expand Down Expand Up @@ -166,6 +168,115 @@ export function getAwsAccountsSingleSelectDropdown() {
return createAwsAccountsDropdown(false);
}

const ROLE_VALIDATION_BATCH_SIZE = 5;

type ValidationResult = { valid: true } | { valid: false; error: string };

function extractErrorMessage(error: unknown): string {
return error instanceof Error ? error.message : 'Unknown error';
}

function formatRoleValidationError(role: Role, errorMessage: string): string {
return `role "${role.assumeRoleArn}" (${role.accountName}): ${errorMessage}`;
}

async function validateRoleBatch(
roles: Role[],
accessKeyId: string,
secretAccessKey: string,
defaultRegion: string,
endpoint?: string | undefined | null,
): Promise<ValidationResult> {
const results = await Promise.allSettled(
roles.map((role) =>
assumeRole(
accessKeyId,
secretAccessKey,
defaultRegion,
role.assumeRoleArn,
role.assumeRoleExternalId,
endpoint,
),
),
);

for (let i = 0; i < results.length; i++) {
const result = results[i];
if (result.status === 'rejected') {
const role = roles[i];
const errorMessage = extractErrorMessage(result.reason);
return {
valid: false,
error: formatRoleValidationError(role, errorMessage),
};
}
}

return { valid: true };
}

async function validateRequiredFields(auth: any): Promise<ValidationResult> {
if (!auth.defaultRegion) {
return { valid: false, error: 'Default region is required' };
}

const hasCredentials = auth.accessKeyId && auth.secretAccessKey;
if (!hasCredentials && !isImplicitRoleEnabled) {
return {
valid: false,
error: 'Access Key ID and Secret Access Key are required',
};
}

return { valid: true };
}

async function validateBaseCredentials(auth: any): Promise<ValidationResult> {
try {
const credentials = {
accessKeyId: auth.accessKeyId || '',
secretAccessKey: auth.secretAccessKey || '',
endpoint: auth.endpoint,
};
await getAccountId(credentials, auth.defaultRegion);
return { valid: true };
} catch (error) {
const errorMessage = extractErrorMessage(error);
return {
valid: false,
error: errorMessage,
};
}
}

async function validateRoleAssumptions(auth: any): Promise<ValidationResult> {
if (!auth.roles || auth.roles.length === 0) {
return { valid: true };
}

const accessKeyId = auth.accessKeyId || '';
const secretAccessKey = auth.secretAccessKey || '';
const roles = auth.roles as Role[];

for (let i = 0; i < roles.length; i += ROLE_VALIDATION_BATCH_SIZE) {
const batch = roles.slice(i, i + ROLE_VALIDATION_BATCH_SIZE);

const result = await validateRoleBatch(
batch,
accessKeyId,
secretAccessKey,
auth.defaultRegion,
auth.endpoint,
);

if (!result.valid) {
return result;
}
}

return { valid: true };
}

export const amazonAuth = BlockAuth.CustomAuth({
authProviderKey: 'AWS',
authProviderDisplayName: 'AWS',
Expand Down Expand Up @@ -229,16 +340,19 @@ For large or complex setups, enhanced features are available, including:
},
required: true,
validate: async ({ auth }) => {
if (!auth.defaultRegion) {
return { valid: false, error: 'Default region is required' };
const fieldValidation = await validateRequiredFields(auth);
if (!fieldValidation.valid) {
return fieldValidation;
}

if (!auth.accessKeyId && !isImplicitRoleEnabled) {
return { valid: false, error: 'Access Key ID is required' };
const baseCredentialsValidation = await validateBaseCredentials(auth);
if (!baseCredentialsValidation.valid) {
return baseCredentialsValidation;
}

if (!auth.secretAccessKey && !isImplicitRoleEnabled) {
return { valid: false, error: 'Secret Access Key is required' };
const roleValidation = await validateRoleAssumptions(auth);
if (!roleValidation.valid) {
return roleValidation;
}

return { valid: true };
Expand Down
3 changes: 2 additions & 1 deletion packages/openops/src/lib/aws/sts-common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ export async function assumeRole(
defaultRegion: string,
roleArn: string,
externalId?: string,
endpoint?: string | undefined | null,
): Promise<Credentials | undefined> {
const client = getAwsClient(
STSClient,
{ accessKeyId, secretAccessKey },
{ accessKeyId, secretAccessKey, endpoint },
defaultRegion,
);
const command = new AssumeRoleCommand({
Expand Down
3 changes: 3 additions & 0 deletions packages/openops/test/auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('getCredentialsFromAuth tests', () => {
'some region',
'some role',
'some external id',
undefined,
);
});

Expand Down Expand Up @@ -225,6 +226,7 @@ describe('getCredentialsListFromAuth tests', () => {
'region',
'arn:aws:iam::2:user/roleName2',
'externalId2',
undefined,
);
Comment thread
cezudas marked this conversation as resolved.
});

Expand Down Expand Up @@ -334,6 +336,7 @@ describe('getCredentialsForAccount tests', () => {
'region',
'arn:aws:iam::2:user/roleName2',
'externalId2',
undefined,
);
Comment thread
cezudas marked this conversation as resolved.
});

Expand Down
Loading
Loading