Skip to main content

IAM Service: Component Breakdown

This document provides a deep dive into the internal architecture of the iam-service, following the principles of Domain-Driven Design (DDD) and Clean Architecture as established in ADR 0001: Standardized Service Layout. It breaks down the components and their interactions for key use cases, serving as a blueprint for developers.

The architecture is layered to separate concerns:

  • API Layer: The entrypoint for external requests (e.g., REST controllers).
  • Application Layer: Orchestrates use cases by coordinating domain objects and infrastructure.
  • Domain Layer: Contains the core business logic, entities, and rules.
  • Infrastructure Layer: Implements external concerns like databases, message brokers, and third-party APIs.

Visualizing the Flow

The following sequence diagram illustrates how a request flows through the layers for the "Create Tenant" use case. Note that we use a standard Layered Architecture with Application Services, rather than a strict CQRS Command Bus.

Use Case: Create a New Tenant

This is the primary workflow, triggered by POST /tenants.

1. api Layer (Entrypoint)

  • DTO (CreateTenantDTO):
    • File: internal/api/v1/dtos/tenant.go
    • Purpose: Defines the shape of the incoming JSON request body with validation tags.
  • Handler (TenantHandler):
    • File: internal/api/v1/handlers/tenants.go
    • Purpose: Defines the HTTP handler for POST /tenants. It parses the DTO and calls the application service.

2. application Layer (Orchestration)

  • Service (TenantService):
    • File: internal/application/services/tenant_service.go
    • Purpose: The core orchestrator for the use case. It uses domain objects and infrastructure interfaces to execute the business workflow.

3. domain Layer (Business Logic & Rules)

  • Bounded Context: identity
  • Aggregate (Tenant):
    • File: internal/domain/identity/aggregates/tenant.go
    • Purpose: The core Tenant entity. It holds the state (ID, Name, Status) and enforces business rules (invariants).
  • Value Objects (TenantID, TenantName):
    • File: internal/domain/identity/value_objects/tenant_name.go, etc.
    • Purpose: Ensures type safety and encapsulates validation for simple values.
  • Factory (TenantFactory):
    • File: internal/domain/identity/aggregates/tenant.go (Factory methods often reside with the aggregate)
    • Purpose: Encapsulates the logic for creating a new, valid Tenant aggregate.
  • Repository Interface (TenantRepository):
    • File: internal/domain/identity/repositories/tenant_repository.go
    • Purpose: An interface that defines the contract for how Tenant aggregates are persisted (e.g., Save, FindByID).
  • Domain Event (TenantCreated):
    • File: internal/domain/identity/events/tenant_events.go
    • Purpose: A data structure representing the fact that a new tenant was successfully created.

4. infrastructure Layer (External Details)

  • Repository Implementation (GormTenantRepository):
    • File: internal/infrastructure/persistence/postgres/tenant_repository.go
    • Purpose: The concrete implementation of the TenantRepository interface using a specific technology like GORM/PostgreSQL.
  • Event Bus (NatsEventBus / KafkaEventBus):
    • File: internal/infrastructure/eventbus/nats_adapter.go
    • Purpose: Implements the ports.EventBus interface. It publishes domain events to the configured message broker (NATS or Kafka).
  • Transactional Outbox:
    • Mechanism: Instead of publishing directly to the bus during the HTTP request, events are often saved to an outbox table in the same transaction as the aggregate. A background poller then picks them up and publishes them via the EventBus.

5. Wiring It All Together

  • Container Builder: A container builder function (e.g., in internal/app/container.go) uses manual wiring to construct all the concrete infrastructure components and inject them into the application services.
  • CLI Commands (app/cmd/): The application is structured around Cobra CLI commands. The serve command (app/cmd/serve.go) initializes the container and starts the API server.

Use Case: Get a Tenant by ID (Query)

This workflow is triggered by GET /tenants/{tenant_id}.

1. api Layer

  • Handler (TenantHandler):
    • File: internal/api/v1/handlers/tenants.go
    • Purpose: Defines the HTTP handler for GET /tenants/{tenant_id}. It parses the ID and calls the service.

2. application Layer

  • Service (TenantService):
    • File: internal/application/services/tenant_service.go
    • Purpose: The orchestrator for the read operation. It uses the repository to fetch the data.

3. infrastructure Layer

  • Repository (GormTenantRepository):
    • File: internal/infrastructure/persistence/postgres/tenant_repository.go
    • Purpose: A concrete implementation that queries the database.

Testing Strategy

We will use an "outside-in" TDD approach, where tests are written from the perspective of the user/client and drive the implementation of internal components.

  1. API Layer Test (Behavior/Integration Test):

    • Tool: net/http/httptest
    • Goal: Write a test that makes a real HTTP request to the POST /tenants endpoint and asserts the expected outcome (e.g., HTTP 202 Accepted, command bus was called). This test drives the creation of the router and DTO.
  2. Application Layer Test (Logic/Integration Test):

    • Tool: testify/mock
    • Goal: Write a test for the CreateTenantCommandHandler. All external dependencies (repositories, adapters) are mocked. This test verifies the orchestration logic: that the correct domain methods and infrastructure interfaces are called in the right order.
  3. Domain Layer Test (Unit Test):

    • Tool: Standard testing package.
    • Goal: Write pure unit tests for the Tenant aggregate, its factory, and value objects. These tests are fast, have zero dependencies, and verify the core business rules and invariants of the system.

Example Test Flow for "Create Tenant"

  1. Write the API Test:

    • func TestCreateTenant_Endpoint_Success(t *testing.T)
    • It fails because the /tenants route doesn't exist.
    • Implement: Create the TenantRouter and the CreateTenant handler method to make the test pass.
  2. Write the Application Handler Test:

    • func TestCreateTenant_Handler_Success(t *testing.T)
    • It fails because the handler logic is empty.
    • Implement: Write the orchestration logic inside CreateTenantCommandHandler.Handle() using mocked dependencies.
  3. Write the Domain Unit Tests:

    • func TestTenantFactory_Create_Success(t *testing.T)
    • func TestTenantFactory_Create_EmptyName_Fails(t *testing.T)
    • They fail because the factory logic is missing.
    • Implement: Write the business logic in the TenantFactory and Tenant aggregate to satisfy the tests.

Use Case: Claims Enrichment

This is the primary runtime workflow of the service, triggered by the API Gateway calling POST /v1/system/enrich-token.

1. api Layer

  • Handler (SystemHandler):
    • File: internal/api/v1/handlers/system.go
    • Purpose: Defines the handler for /system/enrich-token. It extracts the token and tenant hint, calls the service, and sets the response headers from the result.

2. application Layer

  • Service (ClaimsService):
    • File: internal/application/services/claims_service.go
    • Purpose: Orchestrates the enrichment by calling the ClaimsProvider to validate the token and the UserRepository to fetch local policy.

3. infrastructure Layer

  • Claims Provider (JWTClaimsProvider, IntrospectionClaimsProvider): Implements the logic to get claims from the upstream IdP by either validating a JWT or calling an introspection endpoint.
  • Claims Provider Factory (ClaimsProviderFactory): Selects the correct ClaimsProvider based on the IdP configuration.
  • Repository Implementation (GormUserRepository): Implements the FindByIdpUserIDAndTenantID method to query the database for the user's specific policy record.

Use Case: List My Tenants

This workflow allows an authenticated user to retrieve a list of all tenants they are a member of. It is triggered by GET /me/tenants.

1. api Layer

  • Handler (UserHandler):
    • File: internal/api/v1/handlers/users.go
    • Purpose: Defines the handler for GET /me/tenants. It extracts the authenticated user's ID from the context and calls the service.

2. application Layer

  • Service (UserService):
    • File: internal/application/services/user_service.go
    • Purpose: Orchestrates the query by calling the user repository.

3. infrastructure Layer

  • Repository Implementation (GormUserRepository):
    • File: internal/infrastructure/persistence/postgres/user_repository.go
    • Purpose: Implements the FindTenantsByIdpUserID method, which queries the database to find all tenants associated with a user.

Use Case: Create a New User

This workflow is triggered by POST /tenants/{tenant_id}/users.

1. api Layer

  • DTO (CreateUserDTO):
    • File: internal/api/v1/dtos/user.go
    • Purpose: Defines the request body for creating a user (e.g., name, email, password).
  • Handler (UserHandler):
    • File: internal/api/v1/handlers/users.go
    • Purpose: Defines the handler for POST /tenants/{tenant_id}/users. It parses the DTO and tenant_id from the path, then calls the service.

2. application Layer

  • Service (UserService):
    • File: internal/application/services/user_service.go
    • Purpose: Orchestrates user creation. It will:
      1. Fetch the Tenant aggregate to ensure it exists.
      2. Use a UserFactory to create a new User aggregate.

3. domain Layer

  • Aggregate (User):
    • File: internal/domain/identity/aggregates/user.go
    • Purpose: A new aggregate root representing a user. It holds state like ID, Name, Email, Status, and a list of Roles.
  • Repository Interface (UserRepository):
    • File: internal/domain/identity/repositories/user_repository.go
    • Purpose: Defines the contract for persisting User aggregates.
  • Domain Event (UserCreated):
    • File: internal/domain/identity/events/user_events.go

4. infrastructure Layer

  • Repository Implementation (GormUserRepository):
    • File: internal/infrastructure/persistence/postgres/user_repository.go
    • Purpose: The concrete implementation of the UserRepository interface using GORM.

Use Case: Assign Attributes to a User

This workflow is triggered by POST /tenants/{tenant_id}/users/{user_id}/attributes. It replaces the traditional role assignment with a more flexible Attribute-Based Access Control (ABAC) model.

1. api Layer

  • DTO (AssignAttributesDTO):
    • File: internal/api/v1/dtos/attribute.go
    • Purpose: Defines the request body: {"key": "role", "value": "admin"}.

2. application Layer

  • Service (UserService):
    • File: internal/application/services/user_service.go
    • Purpose: Orchestrates attribute assignment. It will:
      1. Fetch the User aggregate.
      2. Validate the attribute using YAMLSchemaValidator (if applicable).
      3. Call user.SetAttribute(...).
      4. Save the updated User aggregate.

3. domain Layer

  • Aggregate (User):
    • The User aggregate has a SetAttribute method.
  • Validator (YAMLSchemaValidator):
    • File: internal/domain/policy/schema_validator.go
    • Purpose: Ensures that attributes assigned to users conform to defined schemas (e.g., allowed values for "role").
  • Domain Event (UserAttributeAssigned):
    • File: internal/domain/identity/events/user_events.go

4. infrastructure Layer

  • Repository Implementation (GormUserRepository):
    • The existing repository's Save method will handle persisting the updated User aggregate.