# adas-core.LdapLogin — LDAP Authentication Strategy

> Satellite authentication strategy implementing `ILoginService` for LDAP / Active Directory credential verification, user provisioning, and authority synchronization.

---

## Table of Contents

1. [Overview](#overview)
2. [Responsibilities](#responsibilities)
3. [Project Structure](#project-structure)
4. [Dependencies](#dependencies)
5. [Authentication Flow](#authentication-flow)
6. [Configuration](#configuration)
7. [Role Mapping](#role-mapping)
8. [Design Rules](#design-rules)
9. [Back to Root](#back-to-root)

---

## Overview

`adas-core.LdapLogin` is a satellite authentication strategy within the ADAS Core modular monolith. It provides a concrete implementation of the `ILoginService` contract defined in `adas-core.Authentication`, enabling users to authenticate against an external LDAP or Active Directory server.

When a user submits a username and password, the service:

1. Establishes a TCP connection to the configured LDAP server.
2. Binds using either a dedicated service account or the user's own credentials.
3. Searches the directory for the matching user entry.
4. Maps LDAP attributes (e.g., `givenName`, `sn`, `sAMAccountName`) to the application `User` model.
5. Provisions the user in the local identity store if not already present.
6. Resolves application authorities from LDAP group memberships (`AuthoritiesMap`) and explicit per-user overrides (`WhiteList`).
7. Persists the resolved authorities via `IAuthorityService` and returns the hydrated `User`.

Other `ILoginService` operations—token-based login (`Login(HttpContext)`), `Authenticate`, `GetById`, `GetByEmail`, `GetByUsername`, and `GetAllUsers`—are intentionally not supported and throw `LoginServicesException("Not implemented")`.

---

## Responsibilities

| # | Responsibility | Description |
|---|----------------|-------------|
| 1 | **LDAP Credential Verification** | Opens an LDAP connection, optionally binds with a service account, and validates the end-user's credentials through an LDAP bind or subtree search. |
| 2 | **User Entry Search** | Executes an LDAP subtree search against `SearchBase` filtering on `UserNameProperty` to locate the canonical user entry. |
| 3 | **Attribute Mapping** | Translates LDAP attributes (`givenName`, `sn`, `sAMAccountName`, `displayName`) into the application `User` entity. |
| 4 | **User Provisioning** | Locates an existing application user by username or display name; creates a new record via `IUserService` when no match exists. |
| 5 | **Group-to-Role Mapping** | Compares the user's LDAP groups (`memberOf`) against `AuthoritiesMap` to derive application roles and permissions. |
| 6 | **Whitelist Authority Override** | Grants explicit authorities to individual users via `WhiteList`, matched by DN substring or exact username attribute. |
| 7 | **Authority Synchronization** | Upserts resolved authorities into the application store through `IAuthorityService`, preserving existing records that allow updates. |
| 8 | **Configuration Validation** | Validates `LdapConfig` at service instantiation using `FluentValidation`; fails fast on malformed settings. |
| 9 | **DI Registration** | Exposes `AddLdapAuth()` and `LdapModule.Configure()` to wire the scoped `ILoginService` implementation into the Host container. |


---

## Project Structure

```
adas-core.LdapLogin/
├── Configuration/
│   ├── LdapConfig.cs              # Strongly-typed LDAP settings model
│   ├── LdapConfigValidator.cs     # FluentValidation rules for LdapConfig
│   └── LDAPModule.cs              # DI registration module
├── RegistrationExtension/
│   └── LdapRegistration.cs        # Host extension: AddLdapAuth()
├── LdapLoginService.cs            # ILoginService implementation
├── IModule.cs                     # Minimal module contract
└── adas-core.LdapLogin.csproj     # Project definition
```

| File | Role |
|------|------|
| `LdapLoginService.cs` | Core implementation of `ILoginService`. Handles bind, search, mapping, provisioning, and authority sync. Throws `LoginServicesException("Not implemented")` for non-credential methods. |
| `LdapConfig.cs` | All LDAP-specific settings: server, port, search base, attribute mappings, authority map, whitelist. |
| `LdapConfigValidator.cs` | FluentValidation validator for `LdapConfig`. Currently reserves validation rules for future enforcement (all rules are commented out). |
| `LDAPModule.cs` | Binds `LdapConfig` to configuration section `"Ldap"` and registers `LdapLoginService` as a scoped `ILoginService`. |
| `LdapRegistration.cs` | Public entry-point extension called by the Host: `services.AddLdapAuth(configuration)`. |
| `IModule.cs` | Minimal module contract with a static `Configure(services, configuration)` entry point. |

---

## Dependencies

### Downstream References

| Project | Role |
|---------|------|
| `adas-core.Authentication` | Defines `ILoginService`, `IUserService`, and `IAuthorityService` contracts implemented by this strategy. |
| `adas-core.Domain` | Provides `User`, `UserEnum.LoginMethod`, `LoginServicesException`, `UserNotFoundException`, and `PermissionEnum`. |

### Upstream References

| Project | Reason |
|---------|--------|
| `adas-core` (Host) | Calls `AddLdapAuth()` during startup to register this strategy in the DI container. Routes login requests here when the authentication scheme is configured for LDAP. |

### NuGet Packages

| Package | Version | Purpose |
|---------|---------|---------|
| `Novell.Directory.Ldap.NETStandard` | `4.0.0` | Cross-platform LDAP client for .NET Standard. Performs connect, bind, search, and entry iteration against LDAP / Active Directory. |
| `FluentValidation` | `12.1.1` | Declarative configuration validation for `LdapConfig`. |
| `AuditLogs` | `1.0.59` | Audit logging integration for login attempts and authority synchronizations. |

---

## Authentication Flow

```
  +------------------+      +-------------------+      +------------------+
  |  User submits    |      |  LdapLoginService   |      |  Connect to      |
  |  username + pwd  |----->|  .Login(name,pwd)   |----->|  LDAP server     |
  +------------------+      +-------------------+      +------------------+
                                                               |
                                                               v
  +------------------+      +-------------------+      +------------------+
  |  Return user     |<-----|  Provision / get    |<-----|  Search user by  |
  |  with roles      |      |  IUserService       |      |  UserNameProperty|
  +------------------+      +-------------------+      +------------------+
                                   |
                                   v
                            +-------------------+
                            |  Synchronize      |
                            |  authorities      |
                            |  (map + whitelist)|
                            +-------------------+
```

### Detailed Steps

1. **Connect** — Open an `LdapConnection` to the configured server and port (default 389).
2. **Bind** — Authenticate using either:
   - Service-account bind (`LdapUser`/`LdapPassword`) for privileged search.
   - User-direct bind (`UserDomainName\username` + password) for self-authentication.
3. **Search** — Execute a subtree search under `SearchBase` filtering by `({UserNameProperty}={username})`.
4. **Map Entry** — Extract `sAMAccountName`, `givenName`, `sn`, and `memberOf` attributes into a temporary `User` entity via `GetUser()`.
5. **Provision** — Call `GetOrCreateUser()` to find or create a persistent `User` record.
6. **Sync Authorities** — Resolve roles from:
   - `AuthoritiesMap` — maps LDAP group DNs to application roles.
   - `WhiteList` — explicit per-user role grants.
7. **Return** — Return the provisioned `User` entity with populated `Authorization` collection.

---

## Configuration

All settings are bound from the `"Ldap"` configuration section.

| Setting | Type | Default | Description |
|---------|------|---------|-------------|
| `Enabled` | `bool` | `false` | Whether LDAP authentication is active. |
| `Server` | `string?` | — | LDAP server hostname or IP address. |
| `Port` | `int?` | `389` | LDAP port (636 for LDAPS if TLS is terminated externally). |
| `UserDomainName` | `string?` | — | Domain prefix used for user-principal-style logins (`DOMAIN\username`). |
| `UserNameProperty` | `string?` | `"sAMAccountName"` | LDAP attribute used as the username identifier in search filters. |
| `SearchBase` | `string?` | — | Distinguished name (DN) where user subtree searches begin. |
| `GroupsProperty` | `string?` | `"memberOf"` | LDAP attribute containing the user's group membership DNs. |
| `LdapUser` | `string?` | — | Optional service-account DN for privileged directory search. |
| `LdapPassword` | `string?` | — | Password for the optional service account. |
| `FirstNameProperty` | `string?` | `"givenName"` | LDAP attribute mapped to the user's first name. |
| `LastNameProperty` | `string?` | `"sn"` | LDAP attribute mapped to the user's last name. |
| `DisplayNameProperty` | `string?` | `"displayName"` | LDAP attribute mapped to the user's display name. |
| `AuthoritiesMap` | `List<AuthorityMap>` | `[]` | Maps LDAP group DNs to application roles and display IDs. |
| `WhiteList` | `List<UserLdap>` | `[]` | Explicit per-user role overrides matched by DN substring or exact username. |

### Example `appsettings.json`

```json
{
  "Ldap": {
    "Enabled": true,
    "Server": "dc.corp.local",
    "Port": 389,
    "UserDomainName": "CORP",
    "UserNameProperty": "sAMAccountName",
    "SearchBase": "DC=corp,DC=local",
    "GroupsProperty": "memberOf",
    "LdapUser": null,
    "LdapPassword": null,
    "FirstNameProperty": "givenName",
    "LastNameProperty": "sn",
    "DisplayNameProperty": "displayName",
    "AuthoritiesMap": [
      {
        "Group": "CN=Admins,OU=Groups,DC=corp,DC=local",
        "Rol": "Admin",
        "Actions": [],
        "DisplayId": "admin-console"
      },
      {
        "Group": "CN=Operators,OU=Groups,DC=corp,DC=local",
        "Rol": "Operator",
        "Actions": [],
        "DisplayId": "operator-panel"
      }
    ],
    "WhiteList": [
      {
        "Name": null,
        "Username": "emergency.admin",
        "Rol": "Admin",
        "Actions": [],
        "DisplayId": "emergency-access"
      }
    ]
  }
}
```

> **Security Note**: `LdapPassword` should never be committed in plaintext. Store it in environment variables, Azure Key Vault, or .NET User Secrets in non-local environments.

---

## Role Mapping

This strategy supports two complementary mechanisms for assigning application authorities.

### Group-Based Mapping (`AuthoritiesMap`)

The user's LDAP group memberships (`memberOf`) are compared against the configured `AuthoritiesMap` entries. Each match produces an `Authorization` record linked to the user.

```csharp
var groups = ldapEntry.GetAttribute("memberOf").StringValueArray.ToList();
var authorities = _ldapConfig.AuthoritiesMap
    .Where(m => groups.Exists(g =>
        string.Equals(g, m.Group, StringComparison.CurrentCultureIgnoreCase)))
    .Select(m => new Authorization
    {
        UserId = user.Id,
        DisplayId = m.DisplayId,
        Rol = m.Rol
    })
    .ToList();
```

### Whitelist Override (`WhiteList`)

Explicit per-user grants that apply independently of group membership. Entries are matched by:
- `Name` — case-insensitive substring match against the user's LDAP DN.
- `Username` — case-insensitive exact match against the value of `UserNameProperty`.

Useful for emergency access, testing, and privileged accounts with fixed responsibilities.

### Merge Logic

The two lists are merged with **whitelist precedence**:

1. Compute `authorizationWhiteList` from `WhiteList` matches.
2. Compute `authorizationMap` from `AuthoritiesMap` group matches.
3. Filter `authorizationMap` to remove entries whose `DisplayId` already exists in `authorizationWhiteList`.
4. Concatenate `authorizationWhiteList` + filtered `authorizationMap`.
5. For each item in the combined list:
   - If an existing authority with the same `DisplayId` exists for the user and `CanUpdate` is `true`, update its `Rol`.
   - Otherwise, insert a new authority record.
6. Return the final authoritative list from `IAuthorityService.GetUserAuthorities(user.Id)`.

This ensures whitelist entries always win, and no duplicate `DisplayId` authorities are created.

---

## Design Rules

1. **Strategy Contract Compliance** — Must implement `ILoginService` exactly. No additional public surface beyond the interface.
2. **No Business Logic in Strategy** — Orchestration of the login flow belongs here; business rules (lockout, session policies) belong in `Authentication` or `Application`.
3. **Exception Translation** — All `LdapException` and socket errors are wrapped into domain exceptions (`LoginServicesException`, `UserNotFoundException`). Raw stack traces never reach the Host.
4. **Fail Fast on Bad Configuration** — `LdapConfigValidator` runs eagerly inside the `LdapLoginService` constructor via `options.ThrowOnFailures()`. Invalid configuration surfaces immediately.
5. **Lazy UserService Dependency** — `IUserService` is consumed as `Lazy<IUserService>` to prevent circular initialization issues during Host startup.
6. **Idempotent Provisioning** — Calling `Login()` twice for the same LDAP user must not create duplicate records. Upsert semantics are enforced by matching on `UserName` then `Name` before creating.
7. **Graceful Degradation on Sync Failure** — Authority synchronization runs inside `try/catch`; if it throws, the login still succeeds but the user receives an empty authorization list.
8. **No Hardcoded Magic Strings** — Default values (port 389, attribute names) are declared as defaults on `LdapConfig` properties, not inline literals in `LdapLoginService`.
9. **Scoped Lifetime** — `LdapLoginService` is registered as **Scoped** to align with per-request execution and potential per-request configuration overrides.
10. **Connection Hygiene** — `LdapConnection.Disconnect()` is invoked after search completes. Connections are not pooled; each login allocates a fresh connection and releases it deterministically.

---

<p align="center">
  Back to <a href="../README.md">ADAS Core Root README</a>
</p>

