diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
new file mode 100644
index 0000000..3e2d427
--- /dev/null
+++ b/.github/workflows/run-tests.yml
@@ -0,0 +1,63 @@
+name: CI - Unit and Integration Tests
+
+on:
+ push:
+ workflow_dispatch:
+
+permissions:
+ contents: read # Allow read access to repository contents
+ actions: write # Allow write access to actions to upload artifacts
+
+jobs:
+ tests:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out the repository
+ uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4
+
+ - name: Set up Python 3.11
+ uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 # v5.2.0
+ with:
+ python-version: "3.11"
+
+ - name: Set environment variables
+ run: |
+ {
+ echo "AZURE_TENANT_ID=dummy_value";
+ echo "AZURE_CLIENT_ID=dummy_value";
+ echo "AZURE_CLIENT_SECRET=dummy_value";
+ echo "LOG_LEVEL=INFO";
+ } >> "$GITHUB_ENV"
+
+ - name: Install dependencies
+ run: |
+ set -e # Exit immediately if a command exits with a non-zero status
+ python -m pip install --upgrade pip
+ pip install -r function/requirements.txt
+ pip install -r function/requirements-dev.txt
+
+ - name: Run unit tests and log output
+ run: |
+ set -e # Ensure the script fails if the tests fail
+ python -m unittest discover -s "function/tests/unit" 2>&1 | tee "unit-test-results.log"
+ continue-on-error: false # Ensure the job fails if the tests do not run
+
+ - name: Upload unit test logs
+ if: always()
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
+ with:
+ name: Unit Test Logs
+ path: "unit-test-results.log"
+
+ - name: Run integration tests and log output
+ run: |
+ set -e # Ensure the script fails if the tests fail
+ python -m unittest discover -s "function/tests/integration" 2>&1 | tee "integration-test-results.log"
+ continue-on-error: false # Ensure the job fails if the tests do not run
+
+ - name: Upload integration test logs
+ if: always()
+ uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
+ with:
+ name: Integration Test Logs
+ path: "integration-test-results.log"
diff --git a/.gitignore b/.gitignore
index 455a0f4..92622b4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,7 @@ scrap.txt
**/**__pycache__
venv/
entra-id-scim/
+
+# function code
+
+**/function.zip
diff --git a/README.md b/README.md
index 5641b41..1923510 100644
--- a/README.md
+++ b/README.md
@@ -1,66 +1,77 @@
-# Ministry of Justice Template Repository
-
-[![repo standards badge](https://img.shields.io/endpoint?labelColor=231f20&color=005ea5&style=for-the-badge&label=MoJ%20Compliant&url=https%3A%2F%2Foperations-engineering-reports.cloud-platform.service.justice.gov.uk%2Fapi%2Fv1%2Fcompliant_public_repositories%2Fendpoint%2Ftemplate-repository&logo=data:image/png;base64,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)](https://operations-engineering-reports.cloud-platform.service.justice.gov.uk/public-report/template-repository)
-
-This template repository equips you with the default initial files required for a Ministry of Justice GitHub repository.
-
-## Included Files
-
-The repository comes with the following preset files:
-
-- LICENSE
-- .gitignore
-- CODEOWNERS
-- dependabot.yml
-- GitHub Actions example files
-- Ministry of Justice Compliance Badge (public repositories only)
-
-## Setup Instructions
-
-Once you've created your repository using this template, ensure the following steps:
-
-### Update readme
-
-Edit this README.md file to document your project accurately. Take the time to create a clear, engaging, and informative README.md file. Include information like what your project does, how to install and run it, how to contribute, and any other pertinent details.
-
-### Update repository description
-
-After you've created your repository, GitHub provides a brief description field that appears on the top of your repository's main page. This is a summary that gives visitors quick insight into the project. Using this field to provide a succinct overview of your repository is highly recommended.
-
-This description and your README.md will be one of the first things people see when they visit your repository. It's a good place to make a strong, concise first impression. Remember, this is often visible in search results on GitHub and search engines, so it's also an opportunity to help people discover your project.
-
-### Grant Team Permissions
-
-Assign permissions to the appropriate Ministry of Justice teams. Ensure at least one team is granted Admin permissions. Whenever possible, assign permissions to teams rather than individual users.
-
-### Read about the GitHub repository standards
-
-Familiarise yourself with the Ministry of Justice GitHub Repository Standards. These standards ensure consistency, maintainability, and best practices across all our repositories.
-
-You can find the standards [here](https://user-guide.operations-engineering.service.justice.gov.uk/documentation/information/mojrepostandards.html).
-
-Please read and understand these standards thoroughly and enable them when you feel comfortable.
-
-### Modify the GitHub Standards Badge
-
-Once you've ensured that all the [GitHub Repository Standards](https://user-guide.operations-engineering.service.justice.gov.uk/documentation/information/mojrepostandards.html) have been applied to your repository, it's time to update the Ministry of Justice (MoJ) Compliance Badge located in the readme file.
-
-The badge demonstrates that your repository is compliant with MoJ's standards. Please follow these [instructions](https://user-guide.operations-engineering.service.justice.gov.uk/documentation/information/add-repo-badge.html) to modify the badge URL to reflect the status of your repository correctly.
-
-**Please note** the badge will not function correctly if your repository is internal or private. In this case, you may remove the badge from your readme.
-
-### Manage Outside Collaborators
-
-To add an Outside Collaborator to the repository, follow the guidelines detailed [here](https://github.com/ministryofjustice/github-collaborators).
-
-### Update CODEOWNERS
-
-(Optional) Modify the CODEOWNERS file to specify the teams or users authorized to approve pull requests.
-
-### Configure Dependabot
-
-Adapt the dependabot.yml file to match your project's [dependency manager](https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#package-ecosystem) and to enable [automated pull requests for package updates](https://docs.github.com/en/code-security/supply-chain-security).
-
-### Dependency Review
-
-If your repository is private with no GitHub Advanced Security license, remove the `.github/workflows/dependency-review.yml` file.
+# moj-terraform-scim-entra-id
+
+[![repo standards badge](https://img.shields.io/endpoint?labelColor=231f20&color=005ea5&style=for-the-badge&label=MoJ%20Compliant&url=https%3A%2F%2Foperations-engineering-reports.cloud-platform.service.justice.gov.uk%2Fapi%2Fv1%2Fcompliant_public_repositories%2Fendpoint%2Fmoj-terraform-scim-entra-id&logo=data:image/png;base64,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)](https://operations-engineering-reports.cloud-platform.service.justice.gov.uk/public-github-repositories.html#moj-terraform-scim-entra-id)
+
+This Terraform module configures a Lambda function for provisioning (and deprovisioning) AWS SSO Identity Store users and groups from EntraID.
+
+The Lambda function used to use the SCIM endpoints (hence its name, _moj-terraform-scim-github_), but now uses the direct [Identity Store API](https://docs.aws.amazon.com/singlesignon/latest/IdentityStoreAPIReference/API_Operations.html).
+The SCIM API has limitations such as not being able to list more than 50 groups or members (and doesn't support startIndex, so you can't paginate them), whereas the Identity Store API does allow pagination.
+This allows us to deprovision users and groups using the Identity Store API, which you cannot do easily with the SCIM API.
+
+This function only syncs EntraID groups that begin with `azure-aws-sso-`
+
+## Usage
+
+```hcl
+module "scim" {
+ source = "github.com/ministryofjustice/moj-terraform-scim-entra-id"
+ # Required variables for the module
+ azure_tenant_id = "your-tenant-id"
+ azure_client_id = "your-client-id"
+ azure_client_secret = "your-client-secret"
+}
+```
+
+
+## Requirements
+
+| Name | Version |
+|------|---------|
+| [terraform](#requirement\_terraform) | >= 1.0 |
+| [archive](#requirement\_archive) | >= 2.4.0 |
+| [aws](#requirement\_aws) | >= 5.0.0 |
+
+## Providers
+
+| Name | Version |
+|------|---------|
+| [archive](#provider\_archive) | >= 2.4.0 |
+| [aws](#provider\_aws) | >= 5.0.0 |
+
+## Modules
+
+No modules.
+
+## Resources
+
+| Name | Type |
+|------|------|
+| [aws_cloudwatch_event_rule.lambda_schedule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource |
+| [aws_cloudwatch_event_target.lambda_target](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource |
+| [aws_cloudwatch_log_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource |
+| [aws_iam_policy.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy) | resource |
+| [aws_iam_role.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
+| [aws_iam_role_policy_attachment.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource |
+| [aws_lambda_function.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource |
+| [aws_lambda_permission.allow_eventbridge](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource |
+| [archive_file.function](https://registry.terraform.io/providers/hashicorp/archive/latest/docs/data-sources/file) | data source |
+| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source |
+| [aws_iam_policy_document.assume_role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+| [aws_iam_policy_document.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+| [aws_kms_alias.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/kms_alias) | data source |
+
+## Inputs
+
+| Name | Description | Type | Default | Required |
+|------|-------------|------|---------|:--------:|
+| [azure\_client\_id](#input\_azure\_client\_id) | Client ID for AzureAD application | `string` | n/a | yes |
+| [azure\_client\_secret](#input\_azure\_client\_secret) | Client Secret for AzureAD application | `string` | n/a | yes |
+| [azure\_tenant\_id](#input\_azure\_tenant\_id) | Tenant ID for to use for user sync | `string` | n/a | yes |
+| [tags](#input\_tags) | Tags to apply to resources | `map(any)` | `{}` | no |
+
+## Outputs
+
+| Name | Description |
+|------|-------------|
+| [lambda\_function\_name](#output\_lambda\_function\_name) | Name of the deployed Lambda function |
+
\ No newline at end of file
diff --git a/example/.terraform.lock.hcl b/example/.terraform.lock.hcl
new file mode 100644
index 0000000..9061ea4
--- /dev/null
+++ b/example/.terraform.lock.hcl
@@ -0,0 +1,45 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/archive" {
+ version = "2.5.0"
+ constraints = ">= 2.4.0"
+ hashes = [
+ "h1:GyV//bFbFWEll/6XafMvSUCmJL+r7wDDz222dMEmV3c=",
+ "zh:3b5774d20e87058d6d67d9ad4ce3fc4a5f7ea7748d345fa6721e24a0cbb0a3d4",
+ "zh:3b94e706ac0f5151880ccc9e63d33c4113361f27e64224a942caa04a5a19cd44",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:7d7201858fa9376029818c9d017b4b53a933cea75480306b1122663d1e8eea2b",
+ "zh:8c8c7537978adf12271fe143f93b3587bb5dbabf8202ff49d0e3955b7bddc24b",
+ "zh:a5942584665a2689e73f3a3c43296adeaeb7e8698631d157419aa931ff856907",
+ "zh:a63673abdba624d60c84b819184fe86422bdbdf6bc73f68d903a7191aed32c00",
+ "zh:bcd1586cc32b263265e09e78f56dba3a6b6b19f5371c099a9d7a1bfe0b0667cc",
+ "zh:cc9e70e186e4dcef60208b4a64b42e6813b197e21ea106a96bb4eb23b54c3e44",
+ "zh:d4c8a0f69412892507a2c9ec0e334bcc2812a54b81212420d4f2c96ef58f713a",
+ "zh:e91e6d90bbc15252310eca6400d4188b29260aab0539480a3fc7b45e4d19c446",
+ "zh:fc468449c0dbda56aae6cb924e4a67578d18504b5b06e8989783182c6b4a5f73",
+ ]
+}
+
+provider "registry.terraform.io/hashicorp/aws" {
+ version = "5.66.0"
+ constraints = ">= 5.0.0"
+ hashes = [
+ "h1:q04VHjxAyH71dKTfMvrUuap88czr8vpiS8MsN7mDn9A=",
+ "zh:071c908eb18627f4becdaf0a9fe95d7a61f69be365080aba2ef5e24f6314392b",
+ "zh:3dea2a474c6ad4be5b508de4e90064ec485e3fbcebb264cb6c4dec660e3ea8b5",
+ "zh:56c0b81e3bbf4e9ccb2efb984f8758e2bc563ce179ff3aecc1145df268b046d1",
+ "zh:5f34b75a9ef69cad8c79115ecc0697427d7f673143b81a28c3cf8d5decfd7f93",
+ "zh:65632bc2c408775ee44cb32a72e7c48376001a9a7b3adbc2c9b4d088a7d58650",
+ "zh:6d0550459941dfb39582fadd20bfad8816255a827bfaafb932d51d66030fcdd5",
+ "zh:7f1811ef179e507fdcc9776eb8dc3d650339f8b84dd084642cf7314c5ca26745",
+ "zh:8a793d816d7ef57e71758fe95bf830cfca70d121df70778b65cc11065ad004fd",
+ "zh:8c7cda08adba01b5ae8cc4e5fbf16761451f0fab01327e5f44fc47b7248ba653",
+ "zh:96d855f1771342771855c0fb2d47ff6a731e8f2fa5d242b18037c751fd63e6c3",
+ "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
+ "zh:b2a62669b72c2471820410b58d764102b11c24e326831ddcfae85c7d20795acf",
+ "zh:b4a6b251ac24c8f5522581f8d55238d249d0008d36f64475beefc3791f229e1d",
+ "zh:ca519fa7ee1cac30439c7e2d311a0ecea6a5dae2d175fe8440f30133688b6272",
+ "zh:fbcd54e7d65806b0038fc8a0fbdc717e1284298ff66e22aac39dcc5a22cc99e5",
+ ]
+}
diff --git a/example/main.tf b/example/main.tf
new file mode 100644
index 0000000..9fefcc2
--- /dev/null
+++ b/example/main.tf
@@ -0,0 +1,26 @@
+terraform {
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 5.0.0"
+ }
+ archive = {
+ source = "hashicorp/archive"
+ version = ">= 2.4.0"
+ }
+ }
+ required_version = ">= 1.0"
+}
+
+provider "aws" {
+ region = "eu-west-2"
+}
+
+module "entra_id_scim_lambda" {
+ source = "../."
+
+ # Required variables for the module
+ azure_tenant_id = "your-tenant-id" # Replace with your Azure Tenant ID
+ azure_client_id = "your-client-id" # Replace with your Azure Client ID
+ azure_client_secret = "your-client-secret" # Replace with your Azure Client Secret
+}
diff --git a/requirements-dev.txt b/function/requirements-dev.txt
similarity index 82%
rename from requirements-dev.txt
rename to function/requirements-dev.txt
index 5867047..c45b8d6 100644
--- a/requirements-dev.txt
+++ b/function/requirements-dev.txt
@@ -4,9 +4,10 @@ flake8==7.1.0
isort==5.13.2
mypy==1.11.0
pylint==3.2.6
-flake8==6.1.0
unittest2==1.1.0
mock==5.1.0
+requests_mock==1.12.1
+moto==5.0.13
boto3==1.28.57
botocore==1.31.57
diff --git a/requirements.txt b/function/requirements.txt
similarity index 100%
rename from requirements.txt
rename to function/requirements.txt
diff --git a/main.tf b/main.tf
new file mode 100644
index 0000000..bdab82a
--- /dev/null
+++ b/main.tf
@@ -0,0 +1,142 @@
+data "aws_caller_identity" "current" {}
+
+locals {
+ name = "entra-id-scim-lambda"
+}
+data "aws_kms_alias" "lambda" {
+ name = "alias/aws/lambda"
+}
+
+data "aws_iam_policy_document" "assume_role" {
+ statement {
+ effect = "Allow"
+ actions = ["sts:AssumeRole"]
+
+ principals {
+ type = "Service"
+ identifiers = ["lambda.amazonaws.com"]
+ }
+ }
+}
+
+data "aws_iam_policy_document" "default" {
+ statement {
+ effect = "Allow"
+ actions = [
+ "logs:CreateLogGroup",
+ "logs:CreateLogStream",
+ "logs:PutLogEvents"
+ ]
+ resources = ["${aws_cloudwatch_log_group.default.arn}:*"]
+ }
+
+ statement {
+ effect = "Allow"
+ actions = [
+ "identitystore:CreateGroup",
+ "identitystore:CreateGroupMembership",
+ "identitystore:CreateUser",
+ "identitystore:DeleteGroup",
+ "identitystore:DeleteGroupMembership",
+ "identitystore:DeleteUser",
+ "identitystore:DescribeGroup",
+ "identitystore:DescribeGroupMembership",
+ "identitystore:ListGroupMemberships",
+ "identitystore:ListGroups",
+ "identitystore:ListUsers",
+ ]
+ resources = [
+ "arn:aws:identitystore::${data.aws_caller_identity.current.account_id}:identitystore/*",
+ "arn:aws:identitystore:::user/*",
+ "arn:aws:identitystore:::group/*",
+ "arn:aws:identitystore:::membership/*"
+ ]
+ }
+}
+
+resource "aws_iam_policy" "default" {
+ name = local.name
+ policy = data.aws_iam_policy_document.default.json
+}
+
+resource "aws_iam_role" "default" {
+ name = "${local.name}-role"
+ assume_role_policy = data.aws_iam_policy_document.assume_role.json
+}
+
+resource "aws_iam_role_policy_attachment" "default" {
+ role = aws_iam_role.default.name
+ policy_arn = aws_iam_policy.default.arn
+}
+
+resource "aws_cloudwatch_log_group" "default" {
+ name = "/aws/lambda/${local.name}"
+ retention_in_days = 365
+ kms_key_id = data.aws_kms_alias.lambda.arn
+}
+
+data "archive_file" "function" {
+ type = "zip"
+ source_dir = "${path.module}/function"
+ output_path = "${path.module}/function.zip"
+}
+
+resource "aws_lambda_function" "default" {
+ #checkov:skip=CKV_AWS_116:No DLQ needed for this function
+ #checkov:skip=CKV_AWS_115:No function-level concurrency limit required
+ #checkov:skip=CKV_AWS_272:No code-signing configuration required
+ #checkov:skip=CKV_AWS_117:Not configuring a VPC for this Lambda
+ #ts:skip=AWS.LambdaFunction.Logging.0472:No VPC configuration needed for this Lambda function
+
+ function_name = local.name
+ role = aws_iam_role.default.arn
+ handler = "app.lambda_handler"
+ runtime = "python3.11"
+
+ filename = data.archive_file.function.output_path
+ source_code_hash = data.archive_file.function.output_base64sha256
+
+ kms_key_arn = data.aws_kms_alias.lambda.arn
+
+ environment {
+ variables = {
+ AZURE_TENANT_ID = var.azure_tenant_id
+ AZURE_CLIENT_ID = var.azure_client_id
+ AZURE_CLIENT_SECRET = var.azure_client_secret
+ }
+ }
+
+ # Enable X-Ray tracing
+ tracing_config {
+ mode = "Active" # Enables active tracing for Lambda function
+ }
+
+ tags = var.tags
+}
+
+# Schedule rule to trigger the Lambda function every 2 hours
+resource "aws_cloudwatch_event_rule" "lambda_schedule" {
+ name = "${local.name}-schedule"
+ description = "Scheduled rule to trigger the EntraID SCIM Lambda function"
+ schedule_expression = "rate(2 hours)" # Triggers the function every 2 hours
+}
+
+# Target for the CloudWatch event rule to invoke the Lambda function
+resource "aws_cloudwatch_event_target" "lambda_target" {
+ rule = aws_cloudwatch_event_rule.lambda_schedule.name
+ target_id = local.name
+ arn = aws_lambda_function.default.arn
+
+ input = jsonencode({
+ "dry_run" = "False"
+ })
+}
+
+# Permission for CloudWatch Events to invoke the Lambda function
+resource "aws_lambda_permission" "allow_eventbridge" {
+ statement_id = "AllowExecutionFromCloudWatch"
+ action = "lambda:InvokeFunction"
+ function_name = aws_lambda_function.default.function_name
+ principal = "events.amazonaws.com"
+ source_arn = aws_cloudwatch_event_rule.lambda_schedule.arn
+}
diff --git a/outputs.tf b/outputs.tf
new file mode 100644
index 0000000..c48ff1d
--- /dev/null
+++ b/outputs.tf
@@ -0,0 +1,4 @@
+output "lambda_function_name" {
+ description = "Name of the deployed Lambda function"
+ value = aws_lambda_function.default.function_name
+}
diff --git a/variables.tf b/variables.tf
new file mode 100644
index 0000000..1d4780c
--- /dev/null
+++ b/variables.tf
@@ -0,0 +1,24 @@
+variable "azure_tenant_id" {
+ type = string
+ description = "Tenant ID for to use for user sync"
+ sensitive = true
+}
+
+variable "azure_client_id" {
+ type = string
+ description = "Client ID for AzureAD application"
+ sensitive = true
+}
+
+variable "azure_client_secret" {
+ type = string
+ description = "Client Secret for AzureAD application"
+ sensitive = true
+}
+
+
+variable "tags" {
+ type = map(any)
+ description = "Tags to apply to resources"
+ default = {}
+}
diff --git a/versions.tf b/versions.tf
new file mode 100644
index 0000000..6e9121d
--- /dev/null
+++ b/versions.tf
@@ -0,0 +1,13 @@
+terraform {
+ required_providers {
+ aws = {
+ source = "hashicorp/aws"
+ version = ">= 5.0.0"
+ }
+ archive = {
+ source = "hashicorp/archive"
+ version = ">= 2.4.0"
+ }
+ }
+ required_version = ">= 1.0"
+}