
# saif-api-service

This is a module for deploying an API to an App Service Plan and add that API to an API Management Service.

## ⚠️ Authentication Provider Changes

This module now supports dual authentication:

- **Corporate Tenant**: Uses **Entra ID (Microsoft)** for internal/corporate authentication
- **External Tenant**: Uses **Okta** for external user authentication

The API Management policies are configured to validate JWT tokens from both providers, allowing the API to serve both corporate and external users.

## Feature flags

Optional capabilities such as Cosmos DB NoSQL, Blob Storage, and Service Bus queues are members of the single `feature_flags` object input, not top-level module arguments. In a Forge v3 project, the templates generate this module call as `module "saif-appservices"` in `infra/api/app.generated.tf`.

A module block accepts only one `feature_flags` argument, and Terraform rejects a second one as a duplicate. When you enable another feature and the module call already defines `feature_flags` (for example, a project scaffolded with `database_type=cosmosdb` already has the Cosmos DB members generated into it), add the new feature's members to that existing object instead of adding a new `feature_flags = { ... }` block:

```hcl
module "saif-appservices" {
  # ... existing generated module arguments ...

  feature_flags = {
    # Existing members, e.g. from Cosmos DB
    cosmosdb_nosql_serverless = true

    # Members added for another feature, e.g. Blob Storage
    enable_blob_storage = true
    blob_storage_settings = {
      containers           = ["documents"]
      contributor_group_id = "12345678-1234-1234-1234-123456789abc"
    }
  }
}
```

The `feature_flags` row in the Inputs table below lists every member and its default. Each feature's development guide documents the members it needs.

<!-- BEGIN_TF_DOCS -->
## Providers

| Name | Version |
|------|---------|
| <a name="provider_azurerm.shared-services"></a> [azurerm.shared-services](#provider\_azurerm.shared-services) | >= 4.0, < 5.0 |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_api"></a> [api](#input\_api) | n/a | <pre>object({<br/>    name          = string<br/>    type          = string<br/>    open_api_file = string<br/>  })</pre> | n/a | yes |
| <a name="input_api_policy_type"></a> [api\_policy\_type](#input\_api\_policy\_type) | APIM policy type. 'standard' uses Okta/Entra tenant-based routing. 'filevine' validates Filevine Identity Server JWTs for webhook endpoints. 'subscription\_key' allows legacy apps to authenticate via APIM subscription keys (Experience APIs only). | `string` | `"standard"` | no |
| <a name="input_application_secrets"></a> [application\_secrets](#input\_application\_secrets) | Secrets for your application from Azure Devops Library | `map(string)` | `{}` | no |
| <a name="input_application_settings"></a> [application\_settings](#input\_application\_settings) | Settings for your application | `map(string)` | `{}` | no |
| <a name="input_azure_logging_level"></a> [azure\_logging\_level](#input\_azure\_logging\_level) | The logging level for the Azure Infrastructure | `string` | `"Error"` | no |
| <a name="input_create_staging_slot"></a> [create\_staging\_slot](#input\_create\_staging\_slot) | Whether to create a staging slot for the web app | `bool` | `false` | no |
| <a name="input_deployed_by"></a> [deployed\_by](#input\_deployed\_by) | Identifier for the deployment mechanism (e.g. Terraform, GitHub Actions) | `string` | `"Terraform"` | no |
| <a name="input_enable_health_check"></a> [enable\_health\_check](#input\_enable\_health\_check) | Whether to enable the App Service health check. Defaults to true. Set to false to opt out. | `bool` | `true` | no |
| <a name="input_environment"></a> [environment](#input\_environment) | The environment in which the resources are deployed | `string` | n/a | yes |
| <a name="input_environment_short_name"></a> [environment\_short\_name](#input\_environment\_short\_name) | The short name of the environment | `string` | `""` | no |
| <a name="input_feature_flags"></a> [feature\_flags](#input\_feature\_flags) | Feature Flags for this module | <pre>object({<br/>    cosmosdb_nosql_serverless = optional(bool, false)<br/>    cosmosdb_nosql_serverless_settings = optional(object({<br/>      containers = map(object({<br/>        partition_key_path = string<br/>        ttl_in_days        = optional(number)                 # TTL in days; null = TTL disabled, -1 = TTL enabled with no expiration, positive whole number = TTL in days<br/>        unique_keys        = optional(list(list(string)), []) # each inner list = one unique key constraint (single-path or composite)<br/>      }))<br/>      }), {<br/>      containers = {}<br/>    })<br/>    cosmosdb_nosql_serverless_data_readers = optional(map(object({<br/>      object_id  = optional(string)<br/>      group_name = optional(string)<br/>    })), {})<br/>    enable_blob_storage = optional(bool, false)<br/>    blob_storage_settings = optional(object({<br/>      containers           = list(string)<br/>      contributor_group_id = string<br/>      }), {<br/>      containers           = []<br/>      contributor_group_id = ""<br/>    })<br/>    enable_document_intelligence = optional(bool, false)<br/>    enable_servicebus_queue      = optional(bool, false)<br/>    # Keyed by a stable queue identity name (e.g. "intake", "retries") so an app can provision<br/>    # multiple, independent Service Bus queues. Defaults to a single "default" queue. Mirrors the<br/>    # full servicebus-queue module's per-queue settings so every module option is configurable here.<br/>    servicebus_queues = optional(map(object({<br/>      max_delivery_count                      = optional(number, 10)<br/>      lock_duration                           = optional(string, "PT30S")<br/>      default_message_ttl                     = optional(string, "P14D")<br/>      dead_lettering_on_message_expiration    = optional(bool, true)<br/>      requires_duplicate_detection            = optional(bool, false)<br/>      duplicate_detection_history_time_window = optional(string, "PT10M")<br/>      max_size_in_megabytes                   = optional(number, 1024)<br/>      dlq_reader_identities                   = optional(map(string), {})<br/>      dlq_manager_identities                  = optional(map(string), {})<br/>      })), {<br/>      default = {<br/>        max_delivery_count                   = 10<br/>        dead_lettering_on_message_expiration = true<br/>        requires_duplicate_detection         = false<br/>        dlq_reader_identities                = {}<br/>        dlq_manager_identities               = {}<br/>      }<br/>    })<br/>  })</pre> | <pre>{<br/>  "blob_storage_settings": {<br/>    "containers": [],<br/>    "contributor_group_id": ""<br/>  },<br/>  "cosmosdb_nosql_serverless": false,<br/>  "cosmosdb_nosql_serverless_data_readers": {},<br/>  "cosmosdb_nosql_serverless_settings": {<br/>    "containers": {}<br/>  },<br/>  "enable_blob_storage": false,<br/>  "enable_document_intelligence": false,<br/>  "enable_servicebus_queue": false,<br/>  "servicebus_queues": {<br/>    "default": {<br/>      "dead_lettering_on_message_expiration": true,<br/>      "dlq_manager_identities": {},<br/>      "dlq_reader_identities": {},<br/>      "max_delivery_count": 10,<br/>      "requires_duplicate_detection": false<br/>    }<br/>  }<br/>}</pre> | no |
| <a name="input_is_experience_api"></a> [is\_experience\_api](#input\_is\_experience\_api) | Is this an Experience API | `bool` | `false` | no |
| <a name="input_is_external_app"></a> [is\_external\_app](#input\_is\_external\_app) | Is this an external app | `bool` | `false` | no |
| <a name="input_is_production"></a> [is\_production](#input\_is\_production) | Is this a production environment | `bool` | n/a | yes |
| <a name="input_owner"></a> [owner](#input\_owner) | The name of the team that owns the resources | `string` | n/a | yes |
| <a name="input_project_id"></a> [project\_id](#input\_project\_id) | The id of the project. This will be used to name the resources. | `string` | n/a | yes |
| <a name="input_resource_location"></a> [resource\_location](#input\_resource\_location) | The location the resources will be deployed to. | `string` | `"westus2"` | no |
| <a name="input_tags"></a> [tags](#input\_tags) | A map of tags to be applied to resources in the module | `map(string)` | n/a | yes |
| <a name="input_tenant"></a> [tenant](#input\_tenant) | Deprecated — the Azure infra tenant is fixed to Corporate (see local.azure\_tenant). Retained for backward compatibility with existing workspace variable sets (e.g. the Okta credentials variable set that injects tenant = "ext"), but no longer read by this module. | `string` | `"Corporate"` | no |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_application_client_id"></a> [application\_client\_id](#output\_application\_client\_id) | n/a |
| <a name="output_application_id"></a> [application\_id](#output\_application\_id) | n/a |
| <a name="output_application_object_id"></a> [application\_object\_id](#output\_application\_object\_id) | n/a |
| <a name="output_application_principal_client_id"></a> [application\_principal\_client\_id](#output\_application\_principal\_client\_id) | n/a |
| <a name="output_application_principal_id"></a> [application\_principal\_id](#output\_application\_principal\_id) | n/a |
| <a name="output_application_principal_object_id"></a> [application\_principal\_object\_id](#output\_application\_principal\_object\_id) | n/a |
| <a name="output_cosmosdb_account_endpoint"></a> [cosmosdb\_account\_endpoint](#output\_cosmosdb\_account\_endpoint) | n/a |
| <a name="output_cosmosdb_account_id"></a> [cosmosdb\_account\_id](#output\_cosmosdb\_account\_id) | n/a |
| <a name="output_cosmosdb_account_name"></a> [cosmosdb\_account\_name](#output\_cosmosdb\_account\_name) | n/a |
| <a name="output_organization_name"></a> [organization\_name](#output\_organization\_name) | n/a |
| <a name="output_resource_group_name"></a> [resource\_group\_name](#output\_resource\_group\_name) | n/a |
| <a name="output_subscription_primary_key"></a> [subscription\_primary\_key](#output\_subscription\_primary\_key) | Primary key of the APIM subscription scoped to this API. Only populated when api\_policy\_type = "subscription\_key". |
| <a name="output_subscription_secondary_key"></a> [subscription\_secondary\_key](#output\_subscription\_secondary\_key) | Secondary key of the APIM subscription scoped to this API. Only populated when api\_policy\_type = "subscription\_key". |
| <a name="output_user_assigned_identity_clientid"></a> [user\_assigned\_identity\_clientid](#output\_user\_assigned\_identity\_clientid) | n/a |
| <a name="output_user_assigned_identity_id"></a> [user\_assigned\_identity\_id](#output\_user\_assigned\_identity\_id) | n/a |
| <a name="output_user_assigned_identity_principalid"></a> [user\_assigned\_identity\_principalid](#output\_user\_assigned\_identity\_principalid) | n/a |

## Resources


- resource.azurerm_app_configuration_key.application_secrets (/terraform-docs/main.tf#310)
- resource.azurerm_app_configuration_key.application_settings (/terraform-docs/main.tf#298)
- resource.azurerm_key_vault_secret.Application_Secrets (/terraform-docs/main.tf#219)
- resource.azurerm_role_assignment.Application_Secret_Permissions (/terraform-docs/main.tf#283)    
<!-- END_TF_DOCS -->

---

[View source on GitHub](https://github.com/saif-corp/forge/blob/main/src/terraform/saif-api-service/README.md)
