Skip to content

Commit

Permalink
add(integrations): salesforce
Browse files Browse the repository at this point in the history
  • Loading branch information
akshat-76 committed Dec 12, 2023
1 parent 17108bd commit e69d025
Show file tree
Hide file tree
Showing 29 changed files with 955 additions and 0 deletions.
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 }}
12 changes: 12 additions & 0 deletions examples/aws-lambda-salesforce-webhook/.gitignore
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
1 change: 1 addition & 0 deletions examples/aws-lambda-salesforce-webhook/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Indent + Salesforce
24 changes: 24 additions & 0 deletions examples/aws-lambda-salesforce-webhook/main.tf
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
}
9 changes: 9 additions & 0 deletions examples/aws-lambda-salesforce-webhook/outputs.tf
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"
}
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
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"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Terraform AWS + Salesforce Webhook
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
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()],
})
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())
)
}
Loading

0 comments on commit e69d025

Please sign in to comment.