Skip to content

Commit 9a4c75d

Browse files
committed
add(integrations): salesforce
1 parent 17108bd commit 9a4c75d

File tree

29 files changed

+886
-0
lines changed

29 files changed

+886
-0
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: deploy.indent-salesforce-webhook
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
jobs:
10+
terraform:
11+
name: 'Terraform'
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@v2
16+
17+
- name: Setup Terraform
18+
uses: hashicorp/setup-terraform@v1
19+
20+
- name: Configure AWS Credentials
21+
uses: aws-actions/configure-aws-credentials@v1
22+
with:
23+
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
24+
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
25+
aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }} # if you have/need it
26+
aws-region: ${{ secrets.AWS_REGION }}
27+
28+
- name: Terraform Format
29+
id: fmt
30+
run: terraform fmt -check -diff
31+
32+
- name: Build Webhook (terraform-aws-salesforce-webhook)
33+
run: cd terraform-aws-salesforce-webhook && npm run deploy:prepare && npm install && npm run build
34+
35+
- name: Terraform Init
36+
id: init
37+
run: terraform init
38+
39+
- name: Terraform Plan
40+
id: plan
41+
if: github.event_name == 'pull_request'
42+
run: terraform plan -input=false -no-color
43+
continue-on-error: true
44+
env:
45+
TF_VAR_indent_webhook_secret: ${{ secrets.SALESFORCE_WEBHOOK_SECRET }}
46+
TF_VAR_indent_pull_webhook_secret: ${{ secrets.SALESFORCE_PULL_WEBHOOK_SECRET }}
47+
TF_VAR_okta_domain: ${{ secrets.SALESFORCE_ACCOUNT}}
48+
TF_VAR_okta_token: ${{ secrets.SALESFORCE_ACCESS_TOKEN }}
49+
50+
- uses: actions/[email protected]
51+
if: github.event_name == 'pull_request'
52+
env:
53+
PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
54+
with:
55+
github-token: ${{ secrets.GITHUB_TOKEN }}
56+
script: |
57+
const output = `#### Terraform Format and Style 🖌\`${{ steps.fmt.outcome }}\`
58+
#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
59+
#### Terraform Plan 📖\`${{ steps.plan.outcome }}\`
60+
<details><summary>Show Plan</summary>
61+
\`\`\`${process.env.PLAN}\`\`\`
62+
</details>
63+
*Actor: @${{ github.actor }}, Event: \`${{ github.event_name }}\`*`;
64+
github.issues.createComment({
65+
issue_number: context.issue.number,
66+
owner: context.repo.owner,
67+
repo: context.repo.repo,
68+
body: output
69+
})
70+
- name: Terraform Plan Status
71+
if: steps.plan.outcome == 'failure'
72+
run: exit 1
73+
74+
- name: Terraform Apply
75+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
76+
run: terraform apply -input=false -auto-approve
77+
env:
78+
TF_VAR_indent_webhook_secret: ${{ secrets.SALESFORCE_WEBHOOK_SECRET }}
79+
TF_VAR_indent_pull_webhook_secret: ${{ secrets.SALESFORCE_PULL_WEBHOOK_SECRET }}
80+
TF_VAR_salesforce_instance_url: ${{ secrets.SALESFORCE_INSTANCE_URL }}
81+
TF_VAR_salesforce_access_token: ${{ secrets.SALESFORCE_ACCESS_TOKEN }}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
data
2+
dist
3+
lib
4+
.env
5+
node_modules
6+
*.tfstate
7+
.terraform*
8+
*.tfstate.*
9+
terraform/config/*.tfvars
10+
!terraform/config/example.tfvars
11+
yarn.lock
12+
package-lock.json
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Indent + Salesforce
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# terraform {
2+
# backend "s3" {
3+
# encrypt = true
4+
# bucket = ""
5+
# region = "us-west-2"
6+
# key = "indent/terraform.tfstate"
7+
# }
8+
# }
9+
10+
module "salesforce-pull-webhook" {
11+
source = "./terraform-aws-salesforce-webhook/terraform"
12+
13+
indent_webhook_secret = var.salesforce_pull_webhook_secret
14+
salesforce_instance_url = var.salesforce_instance_url
15+
salesforce_access_token = var.salesforce_access_token
16+
}
17+
18+
module "salesforce-change-webhook" {
19+
source = "./terraform-aws-salesforce-webhook/terraform"
20+
21+
indent_webhook_secret = var.salesforce_webhook_secret
22+
salesforce_instance_url = var.salesforce_instance_url
23+
salesforce_access_token = var.salesforce_access_token
24+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
output "pull_api_base_url" {
2+
value = module.salesforce-pull-webhook.api_base_url
3+
description = "The URL of the deployed Lambda"
4+
}
5+
6+
output "api_base_url" {
7+
value = module.salesforce-change-webhook.api_base_url
8+
description = "The URL of the deployed Lambda"
9+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
data
2+
dist
3+
lib
4+
.env
5+
node_modules
6+
*.tfstate
7+
.terraform
8+
*.tfstate.*
9+
terraform/config/*.tfvars
10+
!terraform/config/example.tfvars
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"name": "@indent/terraform-aws-salesforce-webhook",
3+
"version": "0.0.0",
4+
"description": "A Node.js starter for Terraform on AWS with Indent and Okta.",
5+
"main": "index.js",
6+
"private": true,
7+
"scripts": {
8+
"build": "tsc",
9+
"clean:dist": "rm -rf dist",
10+
"clean:modules": "rm -rf node_modules",
11+
"clean:tf": "rm -rf terraform/.terraform && rm -rf terraform/terraform.tfstate*",
12+
"clean:all": "npm run clean:dist; npm run clean:tf; npm run clean:modules",
13+
"create:all": "npm run deploy:init; npm run deploy:prepare; npm run deploy:all",
14+
"deploy:init": "cd terraform; terraform init",
15+
"deploy:prepare": "npm install --production && ./scripts/build-layers.sh",
16+
"deploy:all": "npm run build && npm run tf:apply -auto-approve",
17+
"destroy:all": "npm run tf:destroy -auto-approve",
18+
"tf:plan": "cd terraform && terraform plan -var-file ./config/terraform.tfvars",
19+
"tf:apply": "cd terraform && terraform apply -compact-warnings -var-file ./config/terraform.tfvars",
20+
"tf:destroy": "cd terraform && terraform destroy -auto-approve -var-file ./config/terraform.tfvars"
21+
},
22+
"author": "Indent Inc <[email protected]>",
23+
"license": "Apache-2.0",
24+
"repository": {
25+
"type": "git",
26+
"url": "https://github.com/indentapis/integrations.git"
27+
},
28+
"devDependencies": {
29+
"@types/aws-lambda": "^8.10.39",
30+
"@types/node": "^13.9.8",
31+
"@types/node-fetch": "^2.5.5",
32+
"typescript": "^3.8.3"
33+
},
34+
"dependencies": {
35+
"@indent/runtime-aws-lambda": "canary",
36+
"@indent/webhook": "latest",
37+
"@indent/types": "latest",
38+
"ts-node": "^8.5.4"
39+
}
40+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Terraform AWS + Salesforce Webhook
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#!/usr/bin/env bash
2+
set -x
3+
set -e
4+
5+
ROOT_DIR="$(pwd)"
6+
7+
OUTPUT_DIR="$(pwd)/dist"
8+
9+
LAYER_DIR=$OUTPUT_DIR/layers/nodejs
10+
11+
mkdir -p $LAYER_DIR
12+
13+
cp -LR node_modules $LAYER_DIR
14+
15+
cd $OUTPUT_DIR/layers
16+
17+
zip -q -r layers.zip nodejs
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { getLambdaHandler } from '@indent/runtime-aws'
2+
import { SalesforceIntegration } from './integration'
3+
4+
export const handle = getLambdaHandler({
5+
integrations: [new SalesforceIntegration()],
6+
})
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import {
2+
ApplyUpdateRequest,
3+
BaseHttpIntegration,
4+
BaseHttpIntegrationOpts,
5+
FullIntegration,
6+
HealthCheckResponse,
7+
IntegrationInfoResponse,
8+
PullUpdateRequest,
9+
StatusCode,
10+
WriteRequest,
11+
} from '@indent/base-integration'
12+
import {
13+
ApplyUpdateResponse,
14+
PullUpdateResponse,
15+
Resource,
16+
} from '@indent/types'
17+
import jsforce from 'jsforce'
18+
import {
19+
SalesforceMembersResponse,
20+
SalesforceUserRolesResponse,
21+
} from './salesforce-types'
22+
23+
const pkg = require('../package.json')
24+
const SALESFORCE_INSTANCE_URL = process.env.SALESFORCE_INSTANCE_URL
25+
const SALESFORCE_ACCESS_TOKEN = process.env.SALESFORCE_ACCESS_TOKEN
26+
27+
export class SalesforceIntegration
28+
extends BaseHttpIntegration
29+
implements FullIntegration
30+
{
31+
conn
32+
constructor(opts?: BaseHttpIntegrationOpts) {
33+
super(opts)
34+
if (opts) {
35+
this._name = opts.name
36+
}
37+
}
38+
39+
HealthCheck(): HealthCheckResponse {
40+
return { status: { code: 0 } }
41+
}
42+
43+
GetInfo(): IntegrationInfoResponse {
44+
return {
45+
name: ['indent-salesforce-webhook', this._name].filter(Boolean).join('#'),
46+
capabilities: ['ApplyUpdate', 'PullUpdate'],
47+
version: pkg.version,
48+
}
49+
}
50+
51+
MatchApply(req: WriteRequest): boolean {
52+
return (
53+
req.events.filter((e) =>
54+
Boolean(
55+
e.resources?.filter((r) =>
56+
r.kind?.toLowerCase().includes('salesforce.v1.userrole')
57+
).length
58+
)
59+
).length > 0
60+
)
61+
}
62+
63+
async ConnectSalesforce(): Promise<void> {
64+
this.conn = new jsforce.Connection({
65+
instanceUrl: SALESFORCE_INSTANCE_URL,
66+
accessToken: SALESFORCE_ACCESS_TOKEN,
67+
})
68+
}
69+
70+
MatchPull(req) {
71+
return req.kinds
72+
.map((k) => k.toLowerCase())
73+
.includes('salesforce.v1.userrole')
74+
}
75+
76+
async PullUpdate(_req: PullUpdateRequest): Promise<PullUpdateResponse> {
77+
if (!this.conn) {
78+
this.ConnectSalesforce()
79+
}
80+
const userRole: SalesforceUserRolesResponse = await this.conn.query(
81+
'SELECT Id, Name FROM UserRole'
82+
)
83+
console.log(`debug userRole: ${JSON.stringify(userRole, null, 1)}`)
84+
85+
const kind = 'salesforce.v1.UserRole'
86+
const timestamp = new Date().toISOString()
87+
const resources: Resource[] = userRole.records.map((r) => ({
88+
id: r.Id,
89+
displayName: r.Name,
90+
kind,
91+
labels: {
92+
description: r.Name,
93+
timestamp,
94+
},
95+
})) as Resource[]
96+
console.log(`debug resources: ${JSON.stringify(resources, null, 1)}`)
97+
98+
return {
99+
resources,
100+
}
101+
}
102+
103+
async ApplyUpdate(req: ApplyUpdateRequest): Promise<ApplyUpdateResponse> {
104+
if (!this.conn) {
105+
this.ConnectSalesforce()
106+
}
107+
const auditEvent = req.events.find((e) => /grant|revoke/.test(e.event))
108+
const { event, resources } = auditEvent
109+
const grantee = getResourceByKind(resources, 'user')
110+
const granted = getResourceByKind(resources, 'salesforce.v1.userrole')
111+
let res = { status: { code: StatusCode.UNKNOWN, message: '' } }
112+
113+
try {
114+
if (event === 'access/grant') {
115+
const result: SalesforceMembersResponse = await this.conn.query(
116+
`SELECT Id, Name, UserRole.Id, UserRole.Name FROM User where Id = '${grantee.id}'`
117+
)
118+
const role = result.records.map((u) => u.UserRole?.Id)
119+
if (!role || role[0] !== granted.id) {
120+
await this.conn.sobject('User').update({
121+
Id: grantee.id,
122+
UserRoleId: granted.id,
123+
})
124+
}
125+
126+
res.status.code = StatusCode.OK
127+
} else if (event === 'access/revoke') {
128+
await this.conn.sobject('User').update({
129+
Id: grantee.id,
130+
UserRoleId: null,
131+
})
132+
res.status.code = StatusCode.OK
133+
}
134+
} catch (err) {
135+
res.status.code = StatusCode.INTERNAL
136+
res.status.message = err.message
137+
console.error('failed to update role and license')
138+
console.error(res.status.message)
139+
}
140+
return res
141+
}
142+
}
143+
144+
function getResourceByKind(resources, kind) {
145+
return resources.find(
146+
(r) => r.kind && r.kind.toLowerCase().includes(kind.toLowerCase())
147+
)
148+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
export type SalesforceMembersResponse = {
2+
totalSize: boolean
3+
done: boolean
4+
records: SalesforceMember[]
5+
}
6+
7+
export type SalesforceUserRolesResponse = {
8+
totalSize: boolean
9+
done: boolean
10+
records: SalesforceRole[]
11+
}
12+
13+
export type SalesforceMember = {
14+
attributes: {
15+
type: string
16+
url: string
17+
}
18+
Id: string
19+
Name: string
20+
UserRole: SalesforceRole | null
21+
}
22+
23+
export type SalesforceRole = {
24+
attributes: {
25+
type: string
26+
url: string
27+
}
28+
Name: string
29+
Id: string
30+
}

0 commit comments

Comments
 (0)