# Identity Group Providers

> For the overall PBAC architecture, evaluation flow, and extension points, see `fides/src/fides/service/pbac/README.md`.

Resolves query log user identities to DataConsumers by querying cloud platform APIs (IAM, RBAC, group membership). This is a Fidesplus extension — OSS uses `BasicIdentityResolver` which matches by email only.

Identity Group Providers are **separate from SSO/Identity Providers**. Each provider has its own credentials and resolves a specific platform's identity groupings.

## How it works

```
Poll batch (1000 queries)
  |
  v
Prefetch phase (once per batch, across all providers)
  - Load DataConsumers (scoped by type + scope keys)
  - Create CompositeIdentityGroupResolver chaining all registered providers
  - Each provider queries its platform APIs only for registered consumer groups/roles
  - Build user -> [IdentityGroup] index (merged across providers)
  - Cache in Redis (TTL 15min)
  |
  v
Per-entry resolution
  - Fast path: direct email match (no API calls)
  - Platform path: lookup prefetched attributes from all providers
  - Strict match: consumer.type AND consumer.scope keys must match
  - Multi-match: union purposes across matched consumers
```

## Architecture

```
fidesplus/service/pbac/identity/
  __init__.py           - Provider registry + instantiate_from_config factory
  interface.py          - IdentityGroupProvider Protocol + types
  gcp.py                - GCPIdentityGroupProvider (IAM + service accounts)
  google_workspace.py   - GoogleWorkspaceIdentityGroupProvider (Google Groups via Admin Directory)
  composite.py          - CompositeIdentityGroupResolver (chains multiple providers)
  config/
    entities.py         - IdentityGroupProviderConfig (persistent config entity)
    repository.py       - Redis-backed CRUD
    service.py          - Validation + facade
  README.md             - This file
```

The `IdentityGroupProvider` Protocol (in `interface.py`) defines four things every provider must implement:

1. **`supported_consumer_types`** — What types of identity groupings the platform supports (surfaced to the UI)
2. **`prefetch()`** — Bulk-fetch identity attributes for a batch of users
3. **`resolve()`** — Resolve a single user identity string to a `DataConsumerEntity`
4. **`list_scopes()`** — Enumerate all discoverable scope combinations (powers the consumer form dropdown)

### Provider configuration

Each provider is backed by an `IdentityGroupProviderConfig` (stored in Redis) that pairs a `provider_type` with a `ConnectionConfig` for credentials. This is created via the "Identity resolution" tab on the integration page.

Each integration type maps to a provider type:
- **BigQuery** integration (`connection_type=bigquery`) → `provider_type="gcp"`
- **Google Workspace** integration (`connection_type=google_workspace`) → `provider_type="google_workspace"`

The `instantiate_from_config()` factory in `__init__.py` creates the right provider class from a config + connection.

## Consumer types

Each provider declares `ConsumerTypeDescriptor`s that describe the identity groupings it can resolve. These are surfaced via `GET /api/v1/plus/pbac/consumer-types` so the UI can show the right options when creating a DataConsumer.

GCP types (platform `gcp`):

| Key | Name | What it matches | `scope_keys` |
|-----|------|-----------------|--------------|
| `gcp_iam_role` | IAM Role | Project-level IAM role binding | `{"domain": "Google Workspace domain", "project_id": "GCP project ID"}` |
| `gcp_service_account` | Service Account | Direct email match on service account | `{"domain": "Google Workspace domain", "email": "Service account email"}` |

Google Workspace types (platform `google_workspace`):

| Key | Name | What it matches | `scope_keys` |
|-----|------|-----------------|--------------|
| `google_group` | Google Group | Cloud Identity group membership | `{"domain": "Google Workspace domain"}` |

When a user creates a DataConsumer, they pick a type from this list and provide the required scope values. The provider uses strict matching: both `type` AND `scope` keys must match an `IdentityGroup` from the prefetch.

## Provider details

### GCPIdentityGroupProvider (`gcp.py`)

- Uses a service account with the `browser` role for IAM policy reading
- No domain-wide delegation needed
- Resolves `gcp_iam_role` via Cloud Resource Manager API (project IAM policies)
- Resolves `gcp_service_account` via direct email match

### GoogleWorkspaceIdentityGroupProvider (`google_workspace.py`)

- Requires `delegation_subject` for domain-wide delegation
- Uses the Admin Directory API to list groups and Cloud Identity API for membership lookups
- Requires domain-wide delegation scopes: `admin.directory.group.readonly` + `cloud-identity.groups.readonly`
- Configured as a separate integration type (`google_workspace`) with its own connection and credentials

### MockIdentityGroupProvider (`mock.py`)

- Returns realistic sample data without requiring cloud credentials
- 5 Google Groups, 5 IAM roles, 3 service accounts (all `example.com`)
- Use `provider_type="mock"` to enable
- Available in test/development environments via the "Mock integration" on the integrations page
- Also created automatically by the PBAC seed script

## Adding a new platform

### 1. Define the consumer types

Decide what identity groupings the platform supports. For example, Snowflake has roles and database roles:

```python
# snowflake.py

SNOWFLAKE_CONSUMER_TYPES = [
    ConsumerTypeDescriptor(
        key="snowflake_role",
        name="Role",
        description="Snowflake role (RBAC). Roles can be granted to users and other roles.",
        platform="snowflake",
        supports_members=True,
        scope_keys={"account": "Snowflake account identifier"},
    ),
    ConsumerTypeDescriptor(
        key="snowflake_database_role",
        name="Database Role",
        description="Scoped role within a Snowflake database.",
        platform="snowflake",
        supports_members=True,
        scope_keys={"account": "Snowflake account identifier", "database": "Snowflake database name"},
    ),
    ConsumerTypeDescriptor(
        key="snowflake_service_user",
        name="Service User",
        description="Snowflake service user (non-human principal).",
        platform="snowflake",
        supports_members=False,
        scope_keys={"account": "Snowflake account identifier"},
    ),
]
```

### 2. Implement the provider

Create a new file (e.g., `snowflake.py`) that implements the `IdentityGroupProvider` Protocol:

```python
# snowflake.py

class SnowflakeIdentityGroupProvider:

    def __init__(self, credentials: dict, ...):
        # Store credentials, set up repos, etc.
        # Each provider manages its own credentials
        ...

    @property
    def supported_consumer_types(self) -> list[ConsumerTypeDescriptor]:
        return SNOWFLAKE_CONSUMER_TYPES

    def prefetch(self, user_identities: set[str]) -> None:
        # 1. Load consumers matching snowflake types from Redis
        # 2. Group by type (snowflake_role, snowflake_database_role, etc.)
        # 3. For each role consumer: SHOW GRANTS TO ROLE {scope["role"]}
        #    -> get list of users with that role
        # 4. Intersect with user_identities from the batch
        # 5. Build self._attribute_index: user -> [IdentityGroup]
        # 6. Cache in Redis
        ...

    def resolve(self, identity: str) -> ResolvedConsumer | None:
        # 1. Fast path: email match
        # 2. Lookup prefetched attributes
        # 3. Strict (type, scope) match against consumers
        # 4. Multi-match: union purposes
        ...
```

Key platform-specific logic goes in the prefetch. For Snowflake:
- `SHOW GRANTS TO ROLE {role_name}` to get users assigned to a role
- `SHOW GRANTS TO USER {user_name}` to get roles assigned to a user

For Databricks:
- SCIM API `GET /api/2.0/preview/scim/v2/Groups` for group membership
- Unity Catalog permissions API for role bindings

For Redshift:
- `pg_catalog` views (`pg_user`, `pg_group`) for DB users/groups
- IAM API for IAM role bindings

### 3. Register in the registry

Add the new provider to `__init__.py` as a factory function:

```python
# __init__.py

from fidesplus.service.pbac.identity.snowflake import SnowflakeIdentityGroupProvider

def create_snowflake_provider(credentials: dict, ...) -> SnowflakeIdentityGroupProvider:
    return SnowflakeIdentityGroupProvider(credentials=credentials, ...)

# The registry uses factory functions, not a ConnectionType→class map
```

That's it. The `GET /consumer-types` endpoint, consumer creation validation, and the `CompositeIdentityGroupResolver` in `QueryLogService` all pick up the new provider automatically through the registry.

### 4. Add tests

Follow the pattern in `tests/ops/service/pbac/identity/test_bigquery_resolver.py`:

- Mock the platform API calls (never hit real APIs in tests)
- Test prefetch builds correct attribute index
- Test strict matching (type + scope keys)
- Test multi-match unions purposes
- Test cache hit skips API calls
- Test graceful degradation when API fails

### 5. Extended entry type (optional)

If the platform provides richer identity context in query logs (e.g., Snowflake's `ROLE_NAME`), use `PlatformQueryLogEntry`:

```python
from fidesplus.service.pbac.connectors.types import (
    PlatformQueryIdentity,
    PlatformQueryLogEntry,
)

# In your connector's parse method:
return PlatformQueryLogEntry(
    source_id=...,
    identity=user_name,  # plain string for OSS compatibility
    ...,
    platform_identity=PlatformQueryIdentity(
        user=user_name,
        role=role_name,        # Snowflake ROLE_NAME
        service_account=None,
    ),
)
```

The `platform_identity` field carries the richer context. The provider can access it if needed, but the basic resolution flow works with just the `identity` string.

## Design decisions

- **Consumer-driven prefetch**: We only query the platform for groups/roles that are registered as DataConsumers. This bounds API calls by O(registered_consumers), not O(groups_in_org).
- **Scope-based matching**: Both `type` AND `scope` keys must match. The scope dict always includes the full namespace chain (domain at the top), enabling precise scoping without cross-platform ambiguity.
- **Most permissive multi-match**: When a user matches multiple consumers, their purposes are unioned. The policy override layer handles tightening.
- **Separate providers per platform**: GCP IAM and Google Workspace are separate providers with separate credentials. GCP uses the `browser` role (no delegation), while Google Workspace requires `delegation_subject` for domain-wide delegation since the Cloud Identity API cannot be accessed via standard IAM roles.
- **Composite resolution**: The `CompositeIdentityGroupResolver` chains multiple providers, aggregating identity groups across all platforms in a single prefetch/resolve cycle.
- **IAM role breadth caveat**: Role-type consumers (like `roles/bigquery.dataViewer`) can match many users. Recommend group-type consumers for specificity, role consumers for catch-all policies.
- **Cross-batch caching**: Prefetched data is cached in Redis with configurable TTL (default 15 minutes) to avoid redundant API calls across poll intervals.
- **Factory-based registry**: The `__init__.py` registry uses factory functions rather than a `ConnectionType→class` map, allowing more flexible provider instantiation with per-provider credentials.
