From 3130cc744864f4207ef157b9647bb2314389e99e Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 5 Oct 2025 13:32:52 -0500 Subject: [PATCH 1/4] Fix build config --- .gitignore | 3 ++- package.json | 4 ++-- tsconfig.json | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 855250a..dbda2d0 100644 --- a/.gitignore +++ b/.gitignore @@ -139,4 +139,5 @@ dist vite.config.js.timestamp-* vite.config.ts.timestamp-* .vite/ -dist/ \ No newline at end of file +dist/ +.keep/ \ No newline at end of file diff --git a/package.json b/package.json index 46324bc..8ac1495 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "description": "Syncs EntraID directory to GSuite", "type": "module", - "main": "index.js", + "main": "dist/sync.js", "author": "ACM@UIUC", "license": "BSD-3-Clause", "scripts": { @@ -29,4 +29,4 @@ "prettier": "^3.6.2", "zod": "^4.1.11" } -} +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index 38487f0..5afdfb0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,9 +2,9 @@ "extends": "@tsconfig/node22/tsconfig.json", "compilerOptions": { "module": "Node16", - "rootDir": "../", - "outDir": "../../dist", - "baseUrl": "../" + "rootDir": "./", + "outDir": "./dist", + "baseUrl": "./" }, "ts-node": { "esm": true From a23ade60937ab74c4abb8d9323c194891a985c58 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 5 Oct 2025 13:33:47 -0500 Subject: [PATCH 2/4] Setup unit tests on PR --- .github/workflows/test.yml | 73 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..31a5bdc --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,73 @@ +name: Run unit tests +run-name: Unit tests - @${{ github.actor }} + +on: + pull_request: + branches: + - main + +jobs: + test: + permissions: + contents: read + runs-on: ubuntu-latest + timeout-minutes: 15 + name: Run Unit Tests + steps: + - uses: actions/checkout@v4 + env: + HUSKY: "0" + + - name: Setup Terraform + uses: hashicorp/setup-terraform@v3 + with: + terraform_version: 1.12.2 + + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: "yarn" + + - name: Restore Yarn Cache + uses: actions/cache@v4 + with: + path: node_modules + key: yarn-modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-dev + restore-keys: | + yarn-modules-${{ runner.arch }}-${{ runner.os }}- + + - name: Run unit testing + run: make test_unit + + build: + permissions: + contents: read + runs-on: ubuntu-24.04-arm + timeout-minutes: 15 + name: Build Application + steps: + - uses: actions/checkout@v4 + env: + HUSKY: "0" + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22.x + cache: "yarn" + + - name: Restore Yarn Cache + uses: actions/cache@v4 + with: + path: node_modules + key: yarn-modules-${{ runner.arch }}-${{ runner.os }}-${{ hashFiles('**/yarn.lock') }}-dev + restore-keys: | + yarn-modules-${{ runner.arch }}-${{ runner.os }}- + + - name: Run build + run: make build + env: + HUSKY: "0" + RunEnvironment: prod \ No newline at end of file From 57b1fa6f4cd18ce6ee61c9ca2a94e549b3515a65 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 5 Oct 2025 13:43:27 -0500 Subject: [PATCH 3/4] Don't add UPN as secondary email --- src/config.ts | 2 +- src/gsuite.ts | 9 --------- 2 files changed, 1 insertion(+), 10 deletions(-) diff --git a/src/config.ts b/src/config.ts index 5bef62a..4025ed6 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,7 +17,7 @@ const SecretsConfigSchema = z.object({ googleServiceAccountJson: z .string() .min(1, "googleServiceAccountJson is required"), - deleteRemovedContacts: z.boolean().default(false), + deleteRemovedContacts: z.boolean().default(true), }); const EnvironmentSchema = z.enum(["dev", "prod"]); diff --git a/src/gsuite.ts b/src/gsuite.ts index b9d2ecf..b8443f7 100644 --- a/src/gsuite.ts +++ b/src/gsuite.ts @@ -303,15 +303,6 @@ const contactToAtomXml = (contact: GoogleContact): string => { ); } - if ( - contact.upn && - contact.upn.toLowerCase() !== contact.email.toLowerCase() - ) { - emails.push( - ` `, - ); - } - return ` From f6923d199027c04ebe1f5e7387fb97a9a9cc3112 Mon Sep 17 00:00:00 2001 From: Dev Singh Date: Sun, 5 Oct 2025 13:43:31 -0500 Subject: [PATCH 4/4] Add docs --- README.md | 64 +++++++++++++++++++++++++++++++++++++++---- docs/setup.md | 76 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 docs/setup.md diff --git a/README.md b/README.md index 6f977b3..85ad761 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,65 @@ # Entra ID to Google Workspace External Contact Sync +## Purpose -# Creating certificates +This Lambda function syncs members from our Entra ID (Azure AD) tenant to Google Workspace as external domain shared contacts. + +### Why This Exists + +In the `acm.illinois.edu` Google Workspace tenant, we cannot use people chips or autocomplete for `@illinois.edu` email addresses because they're in a separate identity system (GSuite for UIUC). This creates friction when trying to email or mention Illinois users. + +**This sync solves that problem** by: +- Automatically pulling all active users from the University of Illinois Entra ID tenant +- Creating them as external contacts in Google Workspace's domain shared contacts +- Making Illinois email addresses searchable and autocomplete-able in Gmail, Calendar, Drive, etc. +- Providing a seamless experience where ACM members can easily find and contact other Illinois users + +Users will now see Illinois emails appear in autocomplete suggestions and people chips work correctly across all Google Workspace apps. + +## Architecture + +- **Source**: ACM @ UIUC Entra ID tenant +- **Destination**: Google Workspace Domain Shared Contacts for `acm.illinois.edu` +- **Sync Frequency**: Configurable via EventBridge schedule (default: every hour) +- **Fields Synced**: First name, last name, display name, email (primary) + +## Configuration + +Configuration is stored in AWS Secrets Manager under the secret `gsuite-dirsync-config`: + +## How It Works + +1. **Fetch**: Retrieves all enabled users from Entra ID using certificate authentication +2. **Parse**: Extracts name and email fields, intelligently parsing various display name formats +3. **Compare**: Fetches existing domain shared contacts from Google Workspace +4. **Sync**: Creates, updates, or deletes contacts as needed +5. **Log**: Reports statistics on synced contacts + +## Contact Format + +Contacts are created with: +- **Primary email**: The user's mail field from Entra ID +- **Name fields**: Given name, family name, and display name +- **Smart parsing**: Automatically parses display names like "First Last", "Last, First", etc. when individual name fields are missing + +## Deployment + +The Lambda is deployed via Terraform. Set the Makefile. + +## Monitoring + +View logs in CloudWatch Logs: +- Log group: `/aws/lambda/gsuite-dirsync-engine` +- Structured JSON logging via Pino +- Contains detailed sync statistics and any errors + +## Development + +Run locally: ```bash -openssl req -x509 -newkey rsa:2048 -keyout private-key.pem -out certificate.pem -days 7350 -nodes -subj "/CN=DirSync" -cat private-key.pem certificate.pem > combined.pem -base64 -i combined.pem -o combined-base64.txt +yarn -D +make local ``` +--- -Upload `certificate.pem` to Azure, and the contents of `combined-base64.txt` to Secrets Manager. +For detailed setup instructions, see the setup documentation. \ No newline at end of file diff --git a/docs/setup.md b/docs/setup.md new file mode 100644 index 0000000..ff0b535 --- /dev/null +++ b/docs/setup.md @@ -0,0 +1,76 @@ + +## Setup + +### 1. Entra ID Certificate Authentication + +Generate a certificate for authenticating with Entra ID: + +```bash +# Generate certificate and private key +openssl req -x509 -newkey rsa:2048 -keyout private-key.pem -out certificate.pem -days 7350 -nodes -subj "/CN=DirSync" + +# Combine them +cat private-key.pem certificate.pem > combined.pem + +# Base64 encode for storage +base64 -i combined.pem -o combined-base64.txt +``` + +**Upload to Azure:** +1. Go to Azure Portal → App Registrations +2. Select your app registration +3. Navigate to **Certificates & secrets** → **Certificates** +4. Upload `certificate.pem` + +**Upload to AWS Secrets Manager:** +- Store the contents of `combined-base64.txt` as the `entraClientCertificate` field in your secret + +### 2. Azure App Registration Setup + +1. Create an app registration in Azure Portal +2. Grant API permissions: + - **Microsoft Graph** → `User.Read.All` (Application permission) +3. Grant admin consent for the tenant +4. Note the **Tenant ID** and **Application (client) ID** + +### 3. Google Workspace Setup + +#### Create Service Account +1. Go to [Google Cloud Console](https://console.cloud.google.com) +2. Create a new project (or use existing) +3. Enable the **Contacts API** +4. Create a service account +5. Download the service account JSON key + +#### Enable Domain-Wide Delegation +1. In the service account details, enable **Domain-wide delegation** +2. Note the **Client ID** (numeric) + +#### Authorize in Google Workspace +1. Go to [admin.google.com](https://admin.google.com) +2. Navigate to **Security** → **Access and data control** → **API controls** +3. Click **Manage Domain Wide Delegation** +4. Add new: + - **Client ID**: The numeric client ID from your service account + - **OAuth Scopes**: `https://www.google.com/m8/feeds` +5. Click **Authorize** + +#### Set Delegated User +The `googleDelegatedUser` must be a **Super Admin** in your Google Workspace domain. + +## Troubleshooting + +### "Not Authorized to access this resource/api" +- Verify the delegated user is a Super Admin +- Check domain-wide delegation is properly configured +- Ensure the Client ID and scopes are correct + +### "Failed to fetch contacts: Forbidden" +- Verify the scope `https://www.google.com/m8/feeds` is authorized +- Check that domain-wide delegation is enabled for the service account +- Wait 5-10 minutes after authorization for changes to propagate + +### Contacts not appearing +- Domain Shared Contacts sync can take a few minutes to propagate +- Verify contacts are being created (check CloudWatch logs) +- Try signing out and back in to Google Workspace \ No newline at end of file