-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(aws-iam): add user operations (#89)
## 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
1 parent
4dea518
commit 3277a82
Showing
20 changed files
with
626 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"userName": "john_doe" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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` | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"userName": "john_doe_1" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"success": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
{ | ||
"DeleteUserResponse": { | ||
"ResponseMetadata": { | ||
"RequestId": "bbda2ca3-fbd0-4022-8891-854b831e5fd4" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
{} |
32 changes: 32 additions & 0 deletions
32
integrations/aws-iam/mocks/nango/post/proxy/create-user.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.