Skip to content

Commit

Permalink
feat(aws-iam): add user operations (#89)
Browse files Browse the repository at this point in the history
## Describe your changes

- Add AWS-IAM user operations

## Issue ticket number and link

-
[EXT-203](https://linear.app/nango/issue/EXT-203/user-operations-for-aws)

## Checklist before requesting a review (skip if just adding/editing
APIs & templates)
- [ ] I added tests, otherwise the reason is:
- [x] External API requests have `retries`
- [ ] Pagination is used where appropriate
- [ ] The built in `nango.paginate` call is used instead of a `while
(true)` loop
- [x] Third party requests are NOT parallelized (this can cause issues
with rate limits)
- [x] If a sync requires metadata the `nango.yaml` has `auto_start:
false`
- [x] If the sync is a `full` sync then `track_deletes: true` is set

---------

Co-authored-by: Khaliq <khaliqgant@gmail.com>
Co-authored-by: Khaliq <khaliq@nango.dev>
  • Loading branch information
3 people authored Nov 6, 2024
1 parent 4dea518 commit 3277a82
Show file tree
Hide file tree
Showing 20 changed files with 626 additions and 0 deletions.
44 changes: 44 additions & 0 deletions flows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -632,6 +632,50 @@ integrations:
NoteObject:
value: string
type: string
aws-iam:
actions:
create-user:
description: Creates a user in AWS IAM.
output: User
endpoint: POST /users
input: AWSCreateUser
delete-user:
description: >
Delete an existing user in AWS IAM. When you delete a user, you must
delete the items attached to the user manually, or the deletion fails.
endpoint: DELETE /users
output: SuccessResponse
input: UserNamEntity
syncs:
users:
runs: every day
description: |
Fetches a list of users from AWS IAM
output: User
sync_type: full
track_deletes: true
endpoint: GET /users
models:
UserNamEntity:
userName: string
SuccessResponse:
success: boolean
ActionResponseError:
message: string
User:
id: string
firstName: string
lastName: string
email: string
CreateUser:
firstName: string
lastName: string
email: string
AWSCreateUser:
firstName: string
lastName: string
email: string
userName?: string
bamboohr-basic:
syncs:
employees:
Expand Down
100 changes: 100 additions & 0 deletions integrations/aws-iam/actions/create-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import type { NangoAction, User, AWSCreateUser, ActionResponseError, ProxyConfiguration } from '../../models';
import type { AWSIAMRequestParams, CreateUserResponse } from '../types';
import { aWSCreateUserSchema } from '../schema.zod.js';
import { getAWSAuthHeader } from '../helper/utils.js';

export default async function runAction(nango: NangoAction, input: AWSCreateUser): Promise<User> {
const parsedInput = aWSCreateUserSchema.safeParse(input);

if (!parsedInput.success) {
for (const error of parsedInput.error.errors) {
await nango.log(`Invalid input provided to create a user: ${error.message} at path ${error.path.join('.')}`, { level: 'error' });
}
throw new nango.ActionError<ActionResponseError>({
message: 'Invalid input provided to create a user'
});
}

const { firstName, lastName, email } = parsedInput.data;

const awsIAMParams: AWSIAMRequestParams = {
method: 'POST',
service: 'iam',
path: '/',
params: {
Action: 'CreateUser',
UserName: parsedInput.data.userName || `${firstName}.${lastName}`,
Version: '2010-05-08'
}
};

const tags = [
{ Key: 'firstName', Value: firstName },
{ Key: 'lastName', Value: lastName },
{ Key: 'email', Value: email }
];
const tagsParams = new URLSearchParams();
tags.forEach((tag, index) => {
tagsParams.append(`Tags.member.${index + 1}.Key`, tag.Key);
tagsParams.append(`Tags.member.${index + 1}.Value`, tag.Value);
});

const queryParams = new URLSearchParams({
...awsIAMParams.params
});

tagsParams.forEach((value, key) => {
queryParams.append(key, value);
});

// Sort and construct query string
const sortedQueryParams = new URLSearchParams(Array.from(queryParams.entries()).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)));

const paramsObject = Object.fromEntries(sortedQueryParams.entries());

// Get AWS authorization header
const { authorizationHeader, date } = await getAWSAuthHeader(
nango,
awsIAMParams.method,
awsIAMParams.service,
awsIAMParams.path,
sortedQueryParams.toString()
);

const config: ProxyConfiguration = {
endpoint: awsIAMParams.path,
params: paramsObject,
retries: 10
};

// Make the Create User request
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateUser.html
const resp = await nango.post({
...config,
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'x-amz-date': date,
Authorization: authorizationHeader
},
retries: 10
});

return mapCreateUserResponse(resp.data.CreateUserResponse);
}

function mapCreateUserResponse(response: CreateUserResponse): User {
const user = response.CreateUserResult?.User;
const tags = user.Tags || [];

// Map tags to variables
const firstName = tags.find((tag) => tag.Key === 'firstName')?.Value || '';
const lastName = tags.find((tag) => tag.Key === 'lastName')?.Value || '';
const email = tags.find((tag) => tag.Key === 'email')?.Value || '';

return {
id: user.UserId,
firstName,
lastName,
email
};
}
49 changes: 49 additions & 0 deletions integrations/aws-iam/actions/delete-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { NangoAction, ProxyConfiguration, SuccessResponse, UserNamEntity } from '../../models';
import { getAWSAuthHeader } from '../helper/utils.js';
import type { AWSIAMRequestParams } from '../types';

export default async function runAction(nango: NangoAction, input: UserNamEntity): Promise<SuccessResponse> {
if (!input || !input.userName) {
throw new nango.ActionError({
message: 'userName is required'
});
}

// Set AWS IAM parameters
const awsIAMParams: AWSIAMRequestParams = {
method: 'GET',
service: 'iam',
path: '/',
params: {
Action: 'DeleteUser',
UserName: input.userName,
Version: '2010-05-08'
}
};

const querystring = new URLSearchParams(awsIAMParams.params).toString();

// Get AWS authorization header
const { authorizationHeader, date } = await getAWSAuthHeader(nango, awsIAMParams.method, awsIAMParams.service, awsIAMParams.path, querystring);

const config: ProxyConfiguration = {
endpoint: awsIAMParams.path,
params: awsIAMParams.params,
retries: 10
};

// Make the delete user request
// https://docs.aws.amazon.com/IAM/latest/APIReference/API_DeleteUser.html
await nango.get({
...config,
headers: {
'x-amz-date': date,
Authorization: authorizationHeader
},
retries: 10
});

return {
success: true
};
}
6 changes: 6 additions & 0 deletions integrations/aws-iam/fixtures/create-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"userName": "john_doe_15840"
}
3 changes: 3 additions & 0 deletions integrations/aws-iam/fixtures/delete-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"userName": "john_doe"
}
51 changes: 51 additions & 0 deletions integrations/aws-iam/helper/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import crypto from 'crypto';
import type { NangoSync, NangoAction } from '../../models';

interface AWSAuthHeader {
authorizationHeader: string;
date: string;
}

export async function getAWSAuthHeader(
nango: NangoSync | NangoAction,
method: string,
service: string,
path: string,
querystring: string
): Promise<AWSAuthHeader> {
const connection = await nango.getConnection();

if ('username' in connection.credentials && 'password' in connection.credentials && 'region' in connection.connection_config) {
const accessKeyId = connection.credentials['username'];
const secretAccessKey = connection.credentials['password'];
const region = connection.connection_config['region'];
const host = 'iam.amazonaws.com';

const date = new Date().toISOString().replace(/[:-]|\.\d{3}/g, '');
const payloadHash = crypto.createHash('sha256').update('').digest('hex');
const canonicalHeaders = `host:${host}\nx-amz-date:${date}\n`;
const signedHeaders = 'host;x-amz-date';

const canonicalRequest = `${method}\n${path}\n${querystring}\n${canonicalHeaders}\n${signedHeaders}\n${payloadHash}`;
const credentialScope = `${date.substr(0, 8)}/${region}/${service}/aws4_request`;
const stringToSign = `AWS4-HMAC-SHA256\n${date}\n${credentialScope}\n${crypto.createHash('sha256').update(canonicalRequest).digest('hex')}`;

const getSignatureKey = (key: string, dateStamp: string, regionName: string, serviceName: string) => {
const kDate = crypto.createHmac('sha256', `AWS4${key}`).update(dateStamp).digest();
const kRegion = crypto.createHmac('sha256', kDate).update(regionName).digest();
const kService = crypto.createHmac('sha256', kRegion).update(serviceName).digest();
return crypto.createHmac('sha256', kService).update('aws4_request').digest();
};

const signingKey = getSignatureKey(secretAccessKey, date.substr(0, 8), region, service);
const signature = crypto.createHmac('sha256', signingKey).update(stringToSign).digest('hex');

const authorizationHeader = `AWS4-HMAC-SHA256 Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;

return { authorizationHeader, date };
} else {
throw new nango.ActionError({
message: `AWS credentials (username, password, region) are incomplete`
});
}
}
6 changes: 6 additions & 0 deletions integrations/aws-iam/mocks/create-user/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"userName": "john_doe_15840"
}
6 changes: 6 additions & 0 deletions integrations/aws-iam/mocks/create-user/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"id": "AIDAQ3EGRVT6DWWNFXQ5B",
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com"
}
3 changes: 3 additions & 0 deletions integrations/aws-iam/mocks/delete-user/input.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"userName": "john_doe_1"
}
3 changes: 3 additions & 0 deletions integrations/aws-iam/mocks/delete-user/output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"success": true
}
7 changes: 7 additions & 0 deletions integrations/aws-iam/mocks/nango/get/proxy/delete-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"DeleteUserResponse": {
"ResponseMetadata": {
"RequestId": "bbda2ca3-fbd0-4022-8891-854b831e5fd4"
}
}
}
10 changes: 10 additions & 0 deletions integrations/aws-iam/mocks/nango/getConnection.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"metadata": {},
"connection_config": {
"region": "us-east-1"
},
"credentials": {
"username": "foo",
"password": "bar"
}
}
1 change: 1 addition & 0 deletions integrations/aws-iam/mocks/nango/getMetadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
32 changes: 32 additions & 0 deletions integrations/aws-iam/mocks/nango/post/proxy/create-user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"CreateUserResponse": {
"CreateUserResult": {
"User": {
"Arn": "arn:aws:iam::058264235260:user/john_doe_15840",
"CreateDate": 1730364497,
"PasswordLastUsed": null,
"Path": "/",
"PermissionsBoundary": null,
"Tags": [
{
"Key": "firstName",
"Value": "John"
},
{
"Key": "lastName",
"Value": "Doe"
},
{
"Key": "email",
"Value": "john.doe@example.com"
}
],
"UserId": "AIDAQ3EGRVT6DWWNFXQ5B",
"UserName": "john_doe_15840"
}
},
"ResponseMetadata": {
"RequestId": "97fdacbe-c30d-4800-bf91-eb0a249b803e"
}
}
}
45 changes: 45 additions & 0 deletions integrations/aws-iam/nango.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
integrations:
aws-iam:
actions:
create-user:
description: Creates a user in AWS IAM.
output: User
endpoint: POST /users
input: AWSCreateUser
delete-user:
description: |
Delete an existing user in AWS IAM. When you delete a user, you must delete the items attached to the user manually, or the deletion fails.
endpoint: DELETE /users
output: SuccessResponse
input: UserNamEntity
syncs:
users:
runs: every day
description: |
Fetches a list of users from AWS IAM
output: User
sync_type: full
track_deletes: true
endpoint: GET /users

models:
# Generic
UserNamEntity:
userName: string
SuccessResponse:
success: boolean
ActionResponseError:
message: string

User:
id: string
firstName: string
lastName: string
email: string
CreateUser:
firstName: string
lastName: string
email: string
AWSCreateUser:
__extends: CreateUser
userName?: string
Loading

0 comments on commit 3277a82

Please sign in to comment.