# Aspire Security Configuration

Define security configuration in C# code and generate YAML files during publish.

| Property          | Value                                                                      |
| ----------------- | -------------------------------------------------------------------------- |
| **Goal**          | Configure authentication and authorization using Aspire hosting extensions |
| **Prerequisites** | Forge API project with Aspire AppHost                                      |
| **Time Estimate** | 15-30 minutes                                                              |
| **Difficulty**    | Intermediate                                                               |

!!! warning "Experimental Feature (SAIFSECURITY001)"
    The Aspire security configuration APIs are **experimental** and subject to change in future releases. The `[Experimental]` attribute will generate compiler warning `SAIFSECURITY001` when using these APIs.

    To suppress the warning, add the following to your project file:

    ```xml
    <PropertyGroup>
      <NoWarn>$(NoWarn);SAIFSECURITY001</NoWarn>
    </PropertyGroup>
    ```

---

## 📋 Overview

Instead of manually editing YAML configuration files, you can define security configuration in your AppHost using strongly-typed C# APIs. During `aspire publish`, the configuration is automatically generated into the correct YAML files consumed by Terraform.

### Benefits

- **Compile-time safety** - Invalid configurations fail at build time
- **IntelliSense support** - Discover available permissions and roles
- **Single source of truth** - Configuration lives alongside your application code
- **Automatic generation** - YAML files are created during publish pipeline

---

## Step 1: Define Permissions

Create static classes to define the permissions your API exposes and consumes.

### App Roles (Application Permissions)

App roles are application-level permissions assigned to users via Business Roles or to other applications for service-to-service access:

```csharp
// Auth/Permissions.cs
public static class Permissions
{
    // App roles this API exposes
    public static readonly AppRole ClaimsRead = new("Claims.Read", "Read claims data");
    public static readonly AppRole ClaimsWrite = new("Claims.Write", "Write claims data");
    public static readonly AppRole PolicyRead = new("Policy.Read", "Read policy data");
}
```

### Scopes (Delegated Permissions)

Scopes are delegated permissions where user context flows through for app-to-app calls:

```csharp
// Auth/Permissions.cs
public static class Permissions
{
    // Scopes for upstream API calls (user context flows through)
    public static readonly Scope OrdersRead = new("Orders.Read", "Read orders");
    public static readonly Scope OrdersWrite = new("Orders.Write", "Write orders");

    // App roles for upstream API calls (service-to-service)
    public static readonly AppRole OrdersAppRead = new("Orders.App.Read", "App-level read");
}
```

---

## Step 2: Define Business Roles

Business roles are groups of users defined in your identity system (Entra ID security groups or Okta groups). These are created in dedicated business roles repositories and deployed org-wide for any application to use.

!!! info "Business Roles Are Org-Wide"
    Business Roles are **created** in team-specific repos (e.g., `{Project}-okta-business-roles-corp`, `{Project}-okta-business-roles-external`) and deployed organization-wide. Your application **references** existing Business Roles—it doesn't create new ones.

    See [Business Roles](business-roles.md) for details on creating new Business Roles.

Define references to existing Business Roles in a dedicated class:

```csharp
// Auth/BusinessRoles.cs
public static class BusinessRoles
{
    // Corporate (Entra) users - must exist in business roles repo
    public static readonly BusinessRole ClaimsAdjuster = new("Claims Adjuster");
    public static readonly BusinessRole PolicyViewer = new("Policy Viewer");

    // External (Okta) users - must exist in business roles repo
    public static readonly BusinessRole InjuredWorker = new("Injured Worker");
}
```

!!! warning "Names Must Match Exactly"
    Business role names must match the names defined in the business roles repository exactly. If the role doesn't exist, the auth pipeline will fail with a "group not found" error.

---

## Step 3: Define App Role Assignments

Create `AppRoleAssignment` instances that pair business roles with the app roles they should receive:

```csharp
// Auth/AppRoleAssignments.cs
public static class AppRoleAssignments
{
    // Corporate (Entra) users
    public static readonly AppRoleAssignment ClaimsAdjuster = new(
        BusinessRoles.ClaimsAdjuster,
        [Permissions.ClaimsRead, Permissions.ClaimsWrite]);

    public static readonly AppRoleAssignment PolicyViewer = new(
        BusinessRoles.PolicyViewer,
        [Permissions.PolicyRead]);

    // External (Okta) users
    public static readonly AppRoleAssignment InjuredWorker = new(
        BusinessRoles.InjuredWorker,
        [Permissions.ClaimsRead, Permissions.PolicyRead]);
}
```

!!! tip "Compile-Time Validation"
    `AppRoleAssignment` only accepts `AppRole` arrays, not `Scope`. This ensures you can't accidentally add a delegated permission to a user role.

---

## Step 4: Configure in AppHost

Use the Aspire hosting extensions to configure security:

```csharp
// Program.cs in your AppHost project
var builder = DistributedApplication.CreateBuilder(args);

var api = builder.AddProject<Projects.MyApi>("api");

// Add security resource with app role assignments
builder.AddSecurity()
    .WithCorporateAssignment(AppRoleAssignments.ClaimsAdjuster)   // Entra users
    .WithCorporateAssignment(AppRoleAssignments.PolicyViewer)     // Entra users
    .WithExternalAssignment(AppRoleAssignments.InjuredWorker);    // Okta users

// Configure upstream API dependencies
builder.AddUpstreamApi("it-api-proc-orders")
    .WithScope(Permissions.OrdersRead)                  // Delegated permission
    .WithScope(Permissions.OrdersWrite)                 // Delegated permission
    .WithAppRole(Permissions.OrdersAppRead);            // Application permission

builder.Build().Run();
```

!!! info "Architecture Pattern"
    `SecurityResource` owns the pipeline steps for generating security configuration files. It collects corporate and external app role assignments from annotations, then generates YAML files during `aspire publish`.

### Extension Methods

| Method                                | Description                                             |
| ------------------------------------- | ------------------------------------------------------- |
| `AddSecurity()`                       | Creates the security resource                           |
| `WithCorporateAssignment(assignment)` | Adds an app role assignment for corporate (Entra) users |
| `WithExternalAssignment(assignment)`  | Adds an app role assignment for external (Okta) users   |
| `AddUpstreamApi(projectId)`           | Declares a dependency on an upstream API                |
| `WithScope(scope)`                    | Grants a delegated permission to the upstream API       |
| `WithAppRole(appRole)`                | Grants an application permission to the upstream API    |

---

## Step 5: Publish to Generate Configuration

Run `aspire publish` to generate the YAML configuration files:

```bash
dotnet run --project src/MyApi.AppHost -- publish
```

The publish pipeline generates four configuration files in your `infra/` folder.

---

## Generated Files

### `infra/api/config.yml`

All app roles exposed by this API (deduplicated from business roles):

```yaml
app_roles:
  - value: Claims.Read
    display_name: Read claims data
  - value: Claims.Write
    display_name: Write claims data
  - value: Policy.Read
    display_name: Read policy data
scopes: []
```

### `infra/auth/corp/config.yml`

Business roles for corporate (Entra) users and authorized upstream apps:

```yaml
business_roles:
  - name: Claims Adjuster
    app_roles:
      - Claims.Read
      - Claims.Write
  - name: Policy Viewer
    app_roles:
      - Policy.Read
authorized_apps:
  - project_id: it-api-proc-orders
    app_roles:
      - Orders.App.Read
    scopes:
      - Orders.Read
      - Orders.Write
```

### `infra/auth/ext/user/business-role-app-role.yml`

Business roles for external (Okta) users:

```yaml
authorized_business_roles:
  - name: Injured Worker
    app_roles:
      - Claims.Read
      - Policy.Read
```

### `infra/auth/ext/app/authorized-apps.yml`

Upstream API delegated permissions (scopes for Okta):

```yaml
authorized_apps:
  - project_id: it-api-proc-orders
    scopes:
      - Orders.Read
      - Orders.Write
```

---

## ✅ Verification

After publishing:

1. **Check generated files** - Verify the YAML files exist in `infra/`
2. **Validate YAML** - Ensure the content matches your C# configuration
3. **Run auth pipeline** - Deploy the configuration to your identity providers

---

## 🔍 Troubleshooting

### Files Not Generated

✅ **Check:**

- `AddSecurity()` is called in your AppHost
- You're running `aspire publish`, not `dotnet run`
- The AppHost project references `SAIF.Platform.Aspire.Hosting`

### Missing Business Role in Output

✅ **Check:**

- Role is added with `WithCorporateAssignment()` or `WithExternalAssignment()`
- `AppRoleAssignment` has at least one app role defined

### Upstream API Not in Output

✅ **Check:**

- `AddUpstreamApi()` is called with the correct project ID
- At least one permission is granted with `WithScope()` or `WithAppRole()`

---

## 💡 Design Notes

### Permission Types

| Type                | Class               | Use Case                                                 |
| ------------------- | ------------------- | -------------------------------------------------------- |
| App Role            | `AppRole`           | Application permissions for users and service-to-service |
| Scope               | `Scope`             | Delegated permissions where user context flows through   |
| Business Role       | `BusinessRole`      | Name reference to an identity system group               |
| App Role Assignment | `AppRoleAssignment` | Pairs a business role with app roles it should receive   |

### Why Separate CorporateAssignment and ExternalAssignment?

These map to different identity providers (Entra vs Okta) and generate to different file locations. The API abstracts **where** the config goes, but the distinction is meaningful for the infrastructure.

### Why C# Instead of YAML?

- **Type safety** - Compile-time validation prevents misconfiguration
- **Refactoring** - Rename a permission and all usages update
- **Discoverability** - IntelliSense shows available permissions
- **Testing** - Unit test your security configuration

---

## 📚 Related Documentation

- [Aspire Publish](../../development/aspire-publish.md) - Generate pipelines and infrastructure config from your AppHost
- [User Permissions](user-permissions.md) - Understanding YAML-based user permission configuration
- [App Permissions](app-permissions.md) - YAML-based app permission configuration
- [Business Roles](business-roles.md) - How Business Roles work
- [Deployment Workflow](deployment-workflow.md) - Step-by-step deployment guide
