-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
29 changed files
with
955 additions
and
0 deletions.
There are no files selected for viewing
81 changes: 81 additions & 0 deletions
81
examples/aws-lambda-salesforce-webhook/.github/workflows/terraform.yml
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,81 @@ | ||
name: deploy.indent-salesforce-webhook | ||
|
||
on: | ||
push: | ||
branches: | ||
- main | ||
pull_request: | ||
|
||
jobs: | ||
terraform: | ||
name: 'Terraform' | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Checkout | ||
uses: actions/checkout@v2 | ||
|
||
- name: Setup Terraform | ||
uses: hashicorp/setup-terraform@v1 | ||
|
||
- name: Configure AWS Credentials | ||
uses: aws-actions/configure-aws-credentials@v1 | ||
with: | ||
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} | ||
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | ||
aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} # if you have/need it | ||
aws-region: ${{ secrets.AWS_REGION }} | ||
|
||
- name: Terraform Format | ||
id: fmt | ||
run: terraform fmt -check -diff | ||
|
||
- name: Build Webhook (terraform-aws-salesforce-webhook) | ||
run: cd terraform-aws-salesforce-webhook && npm run deploy:prepare && npm install && npm run build | ||
|
||
- name: Terraform Init | ||
id: init | ||
run: terraform init | ||
|
||
- name: Terraform Plan | ||
id: plan | ||
if: github.event_name == 'pull_request' | ||
run: terraform plan -input=false -no-color | ||
continue-on-error: true | ||
env: | ||
TF_VAR_indent_webhook_secret: ${{ secrets.SALESFORCE_WEBHOOK_SECRET }} | ||
TF_VAR_indent_pull_webhook_secret: ${{ secrets.SALESFORCE_PULL_WEBHOOK_SECRET }} | ||
TF_VAR_okta_domain: ${{ secrets.SALESFORCE_ACCOUNT}} | ||
TF_VAR_okta_token: ${{ secrets.SALESFORCE_ACCESS_TOKEN }} | ||
|
||
- uses: actions/[email protected] | ||
if: github.event_name == 'pull_request' | ||
env: | ||
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}" | ||
with: | ||
github-token: ${{ secrets.GITHUB_TOKEN }} | ||
script: | | ||
const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\` | ||
#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\` | ||
#### Terraform Plan 📖\`${{ steps.plan.outcome }}\` | ||
<details><summary>Show Plan</summary> | ||
\`\`\`${process.env.PLAN}\`\`\` | ||
</details> | ||
*Actor: @${{ github.actor }}, Event: \`${{ github.event_name }}\`*`; | ||
github.issues.createComment({ | ||
issue_number: context.issue.number, | ||
owner: context.repo.owner, | ||
repo: context.repo.repo, | ||
body: output | ||
}) | ||
- name: Terraform Plan Status | ||
if: steps.plan.outcome == 'failure' | ||
run: exit 1 | ||
|
||
- name: Terraform Apply | ||
if: github.ref == 'refs/heads/main' && github.event_name == 'push' | ||
run: terraform apply -input=false -auto-approve | ||
env: | ||
TF_VAR_indent_webhook_secret: ${{ secrets.SALESFORCE_WEBHOOK_SECRET }} | ||
TF_VAR_indent_pull_webhook_secret: ${{ secrets.SALESFORCE_PULL_WEBHOOK_SECRET }} | ||
TF_VAR_salesforce_instance_url: ${{ secrets.SALESFORCE_INSTANCE_URL }} | ||
TF_VAR_salesforce_access_token: ${{ secrets.SALESFORCE_ACCESS_TOKEN }} |
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,12 @@ | ||
data | ||
dist | ||
lib | ||
.env | ||
node_modules | ||
*.tfstate | ||
.terraform* | ||
*.tfstate.* | ||
terraform/config/*.tfvars | ||
!terraform/config/example.tfvars | ||
yarn.lock | ||
package-lock.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 @@ | ||
# Indent + Salesforce |
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,24 @@ | ||
# terraform { | ||
# backend "s3" { | ||
# encrypt = true | ||
# bucket = "" | ||
# region = "us-west-2" | ||
# key = "indent/terraform.tfstate" | ||
# } | ||
# } | ||
|
||
module "salesforce-pull-webhook" { | ||
source = "./terraform-aws-salesforce-webhook/terraform" | ||
|
||
indent_webhook_secret = var.salesforce_pull_webhook_secret | ||
salesforce_instance_url = var.salesforce_instance_url | ||
salesforce_access_token = var.salesforce_access_token | ||
} | ||
|
||
module "salesforce-change-webhook" { | ||
source = "./terraform-aws-salesforce-webhook/terraform" | ||
|
||
indent_webhook_secret = var.salesforce_webhook_secret | ||
salesforce_instance_url = var.salesforce_instance_url | ||
salesforce_access_token = var.salesforce_access_token | ||
} |
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,9 @@ | ||
output "pull_api_base_url" { | ||
value = module.salesforce-pull-webhook.api_base_url | ||
description = "The URL of the deployed Lambda" | ||
} | ||
|
||
output "api_base_url" { | ||
value = module.salesforce-change-webhook.api_base_url | ||
description = "The URL of the deployed Lambda" | ||
} |
10 changes: 10 additions & 0 deletions
10
examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/.gitignore
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 @@ | ||
data | ||
dist | ||
lib | ||
.env | ||
node_modules | ||
*.tfstate | ||
.terraform | ||
*.tfstate.* | ||
terraform/config/*.tfvars | ||
!terraform/config/example.tfvars |
40 changes: 40 additions & 0 deletions
40
examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/package.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,40 @@ | ||
{ | ||
"name": "@indent/terraform-aws-salesforce-webhook", | ||
"version": "0.0.0", | ||
"description": "A Node.js starter for Terraform on AWS with Indent and Okta.", | ||
"main": "index.js", | ||
"private": true, | ||
"scripts": { | ||
"build": "tsc", | ||
"clean:dist": "rm -rf dist", | ||
"clean:modules": "rm -rf node_modules", | ||
"clean:tf": "rm -rf terraform/.terraform && rm -rf terraform/terraform.tfstate*", | ||
"clean:all": "npm run clean:dist; npm run clean:tf; npm run clean:modules", | ||
"create:all": "npm run deploy:init; npm run deploy:prepare; npm run deploy:all", | ||
"deploy:init": "cd terraform; terraform init", | ||
"deploy:prepare": "npm install --production && ./scripts/build-layers.sh", | ||
"deploy:all": "npm run build && npm run tf:apply -auto-approve", | ||
"destroy:all": "npm run tf:destroy -auto-approve", | ||
"tf:plan": "cd terraform && terraform plan -var-file ./config/terraform.tfvars", | ||
"tf:apply": "cd terraform && terraform apply -compact-warnings -var-file ./config/terraform.tfvars", | ||
"tf:destroy": "cd terraform && terraform destroy -auto-approve -var-file ./config/terraform.tfvars" | ||
}, | ||
"author": "Indent Inc <[email protected]>", | ||
"license": "Apache-2.0", | ||
"repository": { | ||
"type": "git", | ||
"url": "https://github.com/indentapis/integrations.git" | ||
}, | ||
"devDependencies": { | ||
"@types/aws-lambda": "^8.10.39", | ||
"@types/node": "^13.9.8", | ||
"@types/node-fetch": "^2.5.5", | ||
"typescript": "^3.8.3" | ||
}, | ||
"dependencies": { | ||
"@indent/runtime-aws-lambda": "canary", | ||
"@indent/webhook": "latest", | ||
"@indent/types": "latest", | ||
"ts-node": "^8.5.4" | ||
} | ||
} |
1 change: 1 addition & 0 deletions
1
examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/readme.md
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 @@ | ||
# Terraform AWS + Salesforce Webhook |
17 changes: 17 additions & 0 deletions
17
...es/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/scripts/build-layers.sh
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,17 @@ | ||
#!/usr/bin/env bash | ||
set -x | ||
set -e | ||
|
||
ROOT_DIR="$(pwd)" | ||
|
||
OUTPUT_DIR="$(pwd)/dist" | ||
|
||
LAYER_DIR=$OUTPUT_DIR/layers/nodejs | ||
|
||
mkdir -p $LAYER_DIR | ||
|
||
cp -LR node_modules $LAYER_DIR | ||
|
||
cd $OUTPUT_DIR/layers | ||
|
||
zip -q -r layers.zip nodejs |
6 changes: 6 additions & 0 deletions
6
examples/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/src/index.ts
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 @@ | ||
import { getLambdaHandler } from '@indent/runtime-aws' | ||
import { SalesforceIntegration } from './integration' | ||
|
||
export const handle = getLambdaHandler({ | ||
integrations: [new SalesforceIntegration()], | ||
}) |
177 changes: 177 additions & 0 deletions
177
...s/aws-lambda-salesforce-webhook/terraform-aws-salesforce-webhook/src/integration/index.ts
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,177 @@ | ||
import { | ||
ApplyUpdateRequest, | ||
BaseHttpIntegration, | ||
BaseHttpIntegrationOpts, | ||
FullIntegration, | ||
HealthCheckResponse, | ||
IntegrationInfoResponse, | ||
PullUpdateRequest, | ||
StatusCode, | ||
WriteRequest, | ||
} from '@indent/base-integration' | ||
import { | ||
ApplyUpdateResponse, | ||
PullUpdateResponse, | ||
Resource, | ||
} from '@indent/types' | ||
import jsforce from 'jsforce' | ||
import { SalesforceUsers } from './salesforce-types' | ||
|
||
const pkg = require('../package.json') | ||
const SALESFORCE_INSTANCE_URL = process.env.SALESFORCE_INSTANCE_URL | ||
const SALESFORCE_ACCESS_TOKEN = process.env.SALESFORCE_ACCESS_TOKEN | ||
|
||
export class SalesforceIntegration | ||
extends BaseHttpIntegration | ||
implements FullIntegration | ||
{ | ||
conn | ||
constructor(opts?: BaseHttpIntegrationOpts) { | ||
super(opts) | ||
if (opts) { | ||
this._name = opts.name | ||
} | ||
} | ||
|
||
HealthCheck(): HealthCheckResponse { | ||
return { status: { code: 0 } } | ||
} | ||
|
||
GetInfo(): IntegrationInfoResponse { | ||
return { | ||
name: ['indent-salesforce-webhook', this._name].filter(Boolean).join('#'), | ||
capabilities: ['ApplyUpdate', 'PullUpdate'], | ||
version: pkg.version, | ||
} | ||
} | ||
|
||
MatchApply(req: WriteRequest): boolean { | ||
return ( | ||
req.events.filter((e) => | ||
Boolean( | ||
e.resources?.filter((r) => | ||
r.kind?.toLowerCase().includes('salesforce.v1.userlicense') | ||
).length | ||
) | ||
).length > 0 | ||
) | ||
} | ||
|
||
async ConnectSalesforce(): Promise<void> { | ||
this.conn = new jsforce.Connection({ | ||
instanceUrl: SALESFORCE_INSTANCE_URL, | ||
accessToken: SALESFORCE_ACCESS_TOKEN, | ||
}) | ||
} | ||
|
||
MatchPull(req) { | ||
return req.kinds | ||
.map((k) => k.toLowerCase()) | ||
.includes('salesforce.v1.userlicense') | ||
} | ||
|
||
async PullUpdate(_req: PullUpdateRequest): Promise<PullUpdateResponse> { | ||
if (!this.conn) { | ||
this.ConnectSalesforce() | ||
} | ||
const userLicenses = await this.conn.query( | ||
'SELECT Id, Name, CreatedDate, Status FROM UserLicense' | ||
) | ||
console.log(`debug userLicenses: ${JSON.stringify(userLicenses, null, 1)}`) | ||
|
||
const kind = 'salesforce.v1.userLicense' | ||
|
||
const userIds = userLicenses.records.map((license) => license.Id) | ||
const users: SalesforceUsers = await this.conn.query( | ||
`SELECT Id, Name, UserRole.Id, UserRole.Name, Profile.Id, Profile.Name,Profile.UserLicense.Id, Profile.UserLicense.Name FROM User WHERE Profile.UserLicense.Id IN ('${userIds.join( | ||
"','" | ||
)}')` | ||
) | ||
console.log(`debug users: ${JSON.stringify(users, null, 1)}`) | ||
|
||
const userMap = {} | ||
users.records.forEach((user) => { | ||
const licenseId = user.Profile?.UserLicense?.Id | ||
if (licenseId) { | ||
userMap[licenseId] = user.Profile.Id // Mapping UserLicenseId to Profile record | ||
} | ||
}) | ||
const timestamp = new Date().toISOString() | ||
const resources: Resource[] = userLicenses.records.map((license) => ({ | ||
id: license.Id, | ||
displayName: license.Name, | ||
kind, | ||
labels: { | ||
description: license.Name, | ||
timestamp, | ||
'salesforce/licenseStatus': license.Status, | ||
'salesforce/profileId': userMap[license.Id] || 'N/A', | ||
}, | ||
})) as Resource[] | ||
console.log(`debug resources: ${JSON.stringify(resources, null, 1)}`) | ||
|
||
return { | ||
resources, | ||
} | ||
} | ||
|
||
async ApplyUpdate(req: ApplyUpdateRequest): Promise<ApplyUpdateResponse> { | ||
if (!this.conn) { | ||
this.ConnectSalesforce() | ||
} | ||
const auditEvent = req.events.find((e) => /grant|revoke/.test(e.event)) | ||
const { event, resources } = auditEvent | ||
const grantee = getResourceByKind(resources, 'user') | ||
const granted = getResourceByKind(resources, 'salesforce.v1.userLicense') | ||
const grantedUserProfile = granted.labels['salesforce/profileId'] | ||
|
||
const allUsers: SalesforceUsers = await this.conn.query( | ||
`SELECT Id, Name, UserRole.Id, UserRole.Name, Profile.Id, Profile.Name,Profile.UserLicense.Id, Profile.UserLicense.Name FROM User` | ||
) | ||
|
||
const user = allUsers.records.find((u) => u.Id === grantee.id) | ||
let res = { status: { code: StatusCode.UNKNOWN, message: '' } } | ||
|
||
try { | ||
if (event === 'access/grant') { | ||
const existingLicense = user.Profile.UserLicense.Id === granted.id | ||
if (!existingLicense) { | ||
console.log('no existing license') | ||
const updateData: any = { | ||
Id: grantee.id, | ||
ProfileId: grantedUserProfile, | ||
} | ||
|
||
if (grantedUserProfile) { | ||
updateData.ProfileId = grantedUserProfile | ||
} | ||
|
||
await this.conn.sobject('User').update(updateData) | ||
} | ||
|
||
res.status.code = StatusCode.OK | ||
} else if (event === 'access/revoke') { | ||
const profilesWithoutLicense = allUsers.records.filter( | ||
(user) => !user.Profile.UserLicense.Id | ||
) | ||
await this.conn.sobject('User').update({ | ||
Id: grantee.id, | ||
ProfileId: profilesWithoutLicense?.[0].Profile.Id || null, | ||
}) | ||
res.status.code = StatusCode.OK | ||
} | ||
} catch (err) { | ||
res.status.code = StatusCode.INTERNAL | ||
res.status.message = err.message | ||
console.error('failed to update role and license') | ||
console.error(res.status.message) | ||
} | ||
return res | ||
} | ||
} | ||
|
||
function getResourceByKind(resources, kind) { | ||
return resources.find( | ||
(r) => r.kind && r.kind.toLowerCase().includes(kind.toLowerCase()) | ||
) | ||
} |
Oops, something went wrong.