# LDAP Authentication Administration Guide

## 1. Overview and Theory of Operation

### 1.1 Introduction

IRIS Focus supports LDAP (Lightweight Directory Access Protocol) authentication, allowing organizations to centralize user authentication through their existing LDAP directory services such as Microsoft Active Directory, OpenLDAP, or other LDAP-compliant servers. This integration eliminates the need to manage separate passwords in IRIS Focus and ensures that user access and roles remain synchronized with your organization's directory service.

### 1.2 Hybrid Authentication Mode

IRIS Focus operates in a **hybrid authentication mode**, supporting both LDAP-authenticated users and local database users simultaneously. This architecture provides operational flexibility and ensures that emergency "break-glass" admin accounts remain available even if LDAP connectivity is lost.

**User Account Types:**

- **LDAP-Managed Users**: Authenticate against the LDAP directory. Passwords are validated by the LDAP server on every login. User roles are synchronized from LDAP group memberships on each authentication.

- **Local Users**: Authenticate using credentials stored in the IRIS Focus database. These accounts are managed through the IRIS Focus administrative interface and are independent of LDAP.

Each user account in the system has an internal `ldap_user` flag that determines which authentication method is used. This flag ensures that LDAP users cannot authenticate using stale credentials stored locally, even if the LDAP password changes or the account is disabled in LDAP.

### 1.3 Authentication Flow

When a user attempts to log in to IRIS Focus with LDAP enabled, the following process occurs:

1. **Username Normalization**: The system checks if a local account exists for the username.

2. **Local Account Preservation Check** (if `preserve-local-accounts: true`):
   - If a local account exists and is flagged as a local user (`ldap_user = false`), the system skips LDAP authentication entirely and uses local database authentication.
   - This prevents automatic conversion of local accounts to LDAP management.

3. **LDAP Authentication Attempt** (if not preserved as local):
   - The system attempts authentication using each configured LDAP provider (config1 through config4) in order until one succeeds.
   - For each provider, multiple authentication patterns can be tried (e.g., UPN format, email format, CN format).
   - The bind operation authenticates the user's credentials against the LDAP server.
   - If authentication succeeds, the system retrieves user attributes (username, given name, surname, email, group memberships).

4. **Username Canonicalization**: The username is normalized to the canonical value retrieved from LDAP (typically `sAMAccountName` or `mail` attribute). This prevents users from creating multiple accounts by varying the case of their username (e.g., "JohnDoe" vs "johndoe").

5. **User Account Synchronization**:
   - If the user doesn't exist locally, a new account is created with an unusable password marker (e.g., `!LDAP_USER_NO_LOCAL_AUTH_<timestamp>_<random>`).
   - If the user already exists and `preserve-local-accounts: false`, the local password is replaced with an unusable password marker and the account is converted to LDAP management.
   - The `ldap_user` flag is set to `true` for LDAP-managed accounts.

6. **Role Synchronization**: User roles are determined by mapping LDAP group memberships (from the `memberOf` attribute) to IRIS Focus roles. This synchronization occurs on **every login**, including for administrator accounts, ensuring that role changes in LDAP take effect immediately.

7. **License Validation**: For users with `focus-radar` or `focus-lightning` roles, the system verifies that license seats are available.

8. **Session Creation**: A secure session is established using Apache Shiro with EhCache-backed session management.

### 1.4 Security Features

The current LDAP implementation includes several security enhancements:

- **Stale Password Prevention**: LDAP users cannot authenticate using old passwords stored in the local database. The system stores an unusable password marker instead of the actual LDAP password.

- **Continuous Role Synchronization**: All users, including administrators, have their roles synchronized from LDAP on every login. This ensures that privilege revocations in LDAP take effect immediately.

- **Canonical Username Enforcement**: Usernames are normalized to prevent multiple accounts with case variations (e.g., "admin" vs "ADMIN").

- **Hybrid Mode Support**: Local accounts can coexist with LDAP accounts, ensuring emergency access is always available.

- **Secure LDAPS Support**: SSL/TLS encrypted connections (ldaps://) protect credentials in transit.

### 1.5 Local Account Preservation

The `preserve-local-accounts` configuration option controls whether local accounts are automatically converted to LDAP management when a user successfully authenticates via LDAP:

**When `preserve-local-accounts: false` (default behavior)**:
- Any local account will be converted to LDAP management upon first successful LDAP authentication for that username.
- The local password is replaced with a random unusable value to prevent future local authentication.
- The account becomes permanently LDAP-managed (`ldap_user = true`).
- Use this mode when you want all users to authenticate via LDAP after it's enabled.

**When `preserve-local-accounts: true`**:
- Local accounts remain local accounts even if LDAP authentication would succeed for that username.
- The system skips the expensive LDAP authentication check if it detects a local account.
- Local passwords are preserved and the account continues to use local authentication (`ldap_user = false`).
- Use this mode when you want to maintain a mix of local and LDAP-managed accounts, or when transitioning to LDAP gradually.

**Important**:
- New accounts created via LDAP login will always be LDAP-managed.
- New accounts created through the administrative interface start as local accounts but will be converted to LDAP-managed accounts upon first successful LDAP authentication if `preserve-local-accounts: false`. Only when `preserve-local-accounts: true` will these manually created accounts remain as local accounts permanently.

---

## 2. Configuration Guide

### 2.1 Prerequisites

Before configuring LDAP authentication, ensure you have:

1. **LDAP Server Access**: Network connectivity to your LDAP server (typically port 636 for LDAPS or 389 for LDAP).

2. **LDAP Group Structure**: LDAP groups created and mapped to IRIS Focus roles (see Section 2.6).

3. **Dedicated Local Admin Account**: A local admin account in IRIS Focus that will be used internally for role synchronization. This account should:
   - Have a username that does NOT exist in your LDAP directory (e.g., `ldapauth.focus`, `svc.focus.ldap`)
   - Be assigned the `admin` role in IRIS Focus
   - Have a strong password
   - Never be used for interactive login by humans (it will be logged out whenever an LDAP user logs in)

4. **SSL Certificate** (for LDAPS): If using secure LDAP (ldaps://), ensure the LDAP server's SSL certificate is trusted by the IRIS Focus system.

5. **LDAP Attributes and Search Base**: Knowledge of your LDAP directory structure, including:
   - Base DN for user searches (e.g., `OU=Users,DC=corp,DC=example,DC=com`)
   - Attribute names used (typically: `sAMAccountName`, `givenName`, `sn`, `mail`, `memberOf`)
   - Bind format for authentication (e.g., UPN format like `user@domain.com`)

### 2.2 Configuration File Location

LDAP authentication is configured in the `application.yaml` file located at:

```
/etc/vaisala/radarsw/configuration/application.yaml
```

**File Permissions and Ownership**:

Since this file contains sensitive information including passwords, it should have restricted permissions:

```bash
# Set proper ownership and permissions
sudo chown radarweb:radarsw /etc/vaisala/radarsw/configuration/application.yaml
sudo chmod 600 /etc/vaisala/radarsw/configuration/application.yaml
```

The file should be owned by the `radarweb` user and `radarsw` group with permissions of `600` (read/write for owner only).

**IMPORTANT**: After making any changes to `application.yaml`, you must restart the vaisala-radarsw-webapp service for the changes to take effect:

```bash
systemctl restart vaisala-radarsw-webapp
```

### 2.3 Enabling LDAP Authentication

To enable LDAP authentication, set the master switch in `application.yaml`:

```yaml
ldap:
  enabled: true
```

Until this is set to `true`, IRIS Focus will only use local database authentication.

### 2.4 Local Admin Account Configuration

Configure the dedicated local admin account that IRIS Focus will use internally for role synchronization:

```yaml
ldap:
  local-admin:
    username: ldapauth.focus
    password: your-strong-password-here
```

**CRITICAL SECURITY RECOMMENDATION**: Create a new local admin account specifically for this purpose with a username that does NOT exist in your LDAP directory. This account is used internally by IRIS Focus to temporarily authenticate with admin privileges in order to update user roles after LDAP authentication. The account will be automatically logged in and out during LDAP user authentication, so it should never be used for interactive human login.

**Steps to create the local admin account**:
1. Log in to IRIS Focus as an administrator
2. Navigate to User Management
3. Create a new user with username `ldapauth.focus` (or similar)
4. Assign the `admin` role
5. Set a strong password
6. Document this password securely

### 2.5 Local Account Preservation

Configure whether local accounts should be preserved or converted to LDAP management:

```yaml
ldap:
  preserve-local-accounts: false
```

**Options**:
- `false` (default): Local accounts will be automatically converted to LDAP management upon first successful LDAP authentication for that username. This includes both pre-existing local accounts and newly created accounts via the administrative interface. Their local passwords will be replaced with unusable markers, and they will authenticate via LDAP going forward.

- `true`: Local accounts will NOT be converted to LDAP management. If a local account exists, LDAP authentication will be skipped entirely for that user, and the local password will continue to work. This allows you to maintain a mix of local and LDAP-managed accounts.

**When to use `preserve-local-accounts: true`**:
- Gradual migration to LDAP (some users continue using local accounts during transition)
- Maintaining emergency "break-glass" accounts that should never depend on LDAP
- Testing LDAP configuration with new accounts before converting existing users
- Mixed environment where some users legitimately need local authentication

**When to use `preserve-local-accounts: false`**:
- Full LDAP migration where all users should authenticate via LDAP
- Ensuring centralized password management
- Simplifying user management by having a single source of truth

### 2.6 LDAP Provider Configuration

IRIS Focus supports up to 4 LDAP provider configurations (`config1` through `config4`). Each configuration can connect to a different LDAP server or use different authentication criteria. Configurations are tried in order (config1, config2, config3, config4) until one succeeds.

**Example configuration for config1**:

```yaml
ldap:
  config1:
    enabled: true

    provider:
      # LDAP server URL - use ldaps:// for secure SSL/TLS connection
      # Port 636 is standard for LDAPS, port 389 for unencrypted LDAP
      url: ldaps://ldap.example.com:636

    search:
      # Base DN for user searches - adjust to match your LDAP structure
      base: OU=Users,DC=corp,DC=example,DC=com

      # Attributes to retrieve from LDAP after successful authentication
      # Order matters: the FIRST attribute becomes the IRIS Focus username
      # Standard mapping:
      #   Position [0]: sAMAccountName - becomes IRIS Focus username
      #   Position [1]: givenName - user's first name
      #   Position [2]: sn - user's surname/last name
      #   Position [3]: mail - user's email address
      #   Position [4]: memberOf - LDAP group memberships (used for role mapping)
      attributes: sAMAccountName, givenName, sn, mail, memberOf

    authentication:
      # Multiple authentication patterns - tried in order until one succeeds
      # Use {0} as placeholder for the username/email entered at login

      # Pattern 1: Active Directory UPN format (most common)
      - bind-template: "{0}@corp.example.com"
        search-filter: "(&(objectClass=person)(sAMAccountName={0}))"

      # Pattern 2: Email binding (if user enters full email)
      - bind-template: "{0}"
        search-filter: "(&(objectClass=person)(mail={0}))"
```

**Configuration Details**:

- **`enabled`**: Set to `true` to activate this LDAP provider configuration.

- **`provider.url`**: The LDAP server URL. Always use `ldaps://` (LDAP over SSL) for production environments to encrypt authentication credentials in transit. The standard port is 636 for LDAPS and 389 for unencrypted LDAP.

- **`search.base`**: The base Distinguished Name (DN) where user searches begin. This should point to the organizational unit (OU) or container that holds your user accounts.

- **`search.attributes`**: A comma-separated list of LDAP attributes to retrieve. **The order is critical** - the first attribute in this list becomes the canonical username in IRIS Focus and must be unique per user. Ensure your chosen attributes are populated in LDAP.

- **`authentication`**: A list of bind/search pattern pairs. Each pattern represents a different way users might authenticate:
  - **`bind-template`**: The DN format used to bind (authenticate) to LDAP. The `{0}` placeholder is replaced with whatever the user types in the login field.
  - **`search-filter`**: The LDAP search filter used to find the user's attributes after successful binding.

**Multiple Authentication Patterns**:

Users may log in using different formats (username, email, UPN, etc.). By configuring multiple authentication patterns, you can support all of these formats. For example:

- User enters `jdoe` → Pattern 1 tries `jdoe@corp.example.com`
- User enters `john.doe@example.com` → Pattern 2 tries `john.doe@example.com` directly

Regardless of which pattern succeeds, the canonical username stored in IRIS Focus will be the value of the first attribute (typically `sAMAccountName`), ensuring consistent user identity.

### 2.7 LDAP Attribute Mapping

The attributes retrieved from LDAP are mapped to IRIS Focus user properties in a specific order:

| Position | LDAP Attribute (typical) | IRIS Focus Field | Description |
|----------|--------------------------|------------------|-------------|
| [0] | `sAMAccountName` | Username | **Primary identifier** - becomes the IRIS Focus account name. Must be unique per user. |
| [1] | `givenName` | First Name | User's given/first name. Used for display purposes. |
| [2] | `sn` | Last Name | User's surname/last name. Used for display purposes. |
| [3] | `mail` | Email | User's email address. Used for notifications and display. |
| [4] | `memberOf` | Role Mapping | LDAP group memberships. Used to determine IRIS Focus roles (see Section 2.8). |

**Important Notes**:
- The first attribute (`sAMAccountName` in the example) is the most critical - it becomes the permanent username in IRIS Focus and should never change.
- You can use different attributes if your LDAP schema differs, but ensure they are unique per user.
- The `mail` attribute could be used as both the primary identifier (position [0]) and the user's email (possition [3]) if you prefer email-based usernames, but be aware that email addresses may change over time.
- All five attributes should be configured to ensure proper user management and role synchronization.

### 2.8 Role Mapping Configuration

LDAP group memberships (from the `memberOf` attribute) are mapped to IRIS Focus roles. Configure the LDAP group names that correspond to each IRIS Focus role:

```yaml
ldap:
  roles:
    user: GRP_USER_IRISFOCUS_USER
    poweruser: GRP_USER_IRISFOCUS_POWERUSER
    admin: GRP_USER_IRISFOCUS_ADMIN
    kiosk: GRP_USER_IRISFOCUS_KIOSK
    radar: GRP_USER_IRISFOCUS_FOCUS_RADAR
    lightning: GRP_USER_IRISFOCUS_FOCUS_LGT
```

**Role Descriptions**:

| IRIS Focus Role | Description | License Impact |
|-----------------|-------------|----------------|
| `user` | Standard user with basic viewing capabilities | No additional license seat consumed |
| `poweruser` | Enhanced user with advanced features (product configuration, alerts, warnings) | No additional license seat consumed |
| `admin` | System administrator with full configuration access | No additional license seat consumed |
| `kiosk` | Limited user for display-only kiosk mode | No additional license seat consumed |
| `radar` | Enables radar data visualization features | Consumes one `focus-radar` license seat |
| `lightning` | Enables lightning data visualization features | Consumes one `focus-lightning` license seat |

**Role Combinations**:

Users can have multiple roles assigned simultaneously. Common combinations include:

- `user` - Light user with basic features
- `user admin` - System administrator
- `user radar` - Normal user with radar capabilities (consumes 1 focus-radar seat)
- `user lightning` - Normal user with lightning capabilities (consumes 1 focus-lightning seat)
- `user radar lightning` - Normal user with both radar and lightning (consumes 1 focus-radar seat + 1 focus-lightning seat)
- `poweruser radar lightning` - Power user with both radar and lightning capabilities

**License Seat Management**:

The `focus-radar` and `focus-lightning` roles consume license seats from your IRIS Focus license pool. The number of available seats is determined by your license file. If all seats are consumed, additional users with these roles will not have access to the features associated with those roles.

To monitor license seat usage, enable debug logging (see Section 3.5) and search for "LdapUserRepository" and "UserLoginServiceImpl" in the logs. These messages include license validation details during login.

**Creating LDAP Groups**:

Before configuring role mapping, you must create the corresponding groups in your LDAP directory (or identify existing groups to use). The group names can be any string - they don't need to follow a specific format. However, we recommend descriptive names that clearly indicate their purpose, such as:

- `GRP_USER_IRISFOCUS_USER`
- `GRP_USER_IRISFOCUS_POWERUSER`
- `GRP_USER_IRISFOCUS_ADMIN`
- `GRP_USER_IRISFOCUS_FOCUS_RADAR`
- `GRP_USER_IRISFOCUS_FOCUS_LGT`

Assign users to these groups in your LDAP directory according to the access levels they should have in IRIS Focus.

### 2.9 Multiple LDAP Provider Configurations

If you have multiple LDAP servers or need to support different authentication methods, you can enable additional provider configurations:

```yaml
ldap:
  config1:
    enabled: true
    # ... configuration for first LDAP server

  config2:
    enabled: true
    # ... configuration for second LDAP server or alternate method

  config3:
    enabled: false

  config4:
    enabled: false
```

**Use Cases for Multiple Configurations**:
- Multiple Active Directory domains or forests
- Failover to a secondary LDAP server
- Different authentication patterns for different user populations
- Testing new LDAP configurations alongside production

Configurations are tried in numerical order (config1, config2, config3, config4) until one successfully authenticates the user.

### 2.10 Complete Configuration Example

Here's a complete example for a typical Active Directory environment:

```yaml
ldap:
  enabled: true
  preserve-local-accounts: false

  local-admin:
    username: ldapauth.focus
    password: SecurePassword123!

  config1:
    enabled: true
    provider:
      url: ldaps://ad.example.com:636
    search:
      base: OU=Employees,DC=corp,DC=example,DC=com
      attributes: sAMAccountName, givenName, sn, mail, memberOf
    authentication:
      - bind-template: "{0}@corp.example.com"
        search-filter: "(&(objectClass=person)(sAMAccountName={0}))"
      - bind-template: "{0}"
        search-filter: "(&(objectClass=person)(mail={0}))"

  config2:
    enabled: false

  config3:
    enabled: false

  config4:
    enabled: false

  roles:
    user: GRP_FOCUS_USERS
    poweruser: GRP_FOCUS_POWERUSERS
    admin: GRP_FOCUS_ADMINS
    kiosk: GRP_FOCUS_KIOSK
    radar: GRP_FOCUS_RADAR_ACCESS
    lightning: GRP_FOCUS_LIGHTNING_ACCESS
```

After saving this configuration, restart the webapp service:

```bash
systemctl restart vaisala-radarsw-webapp
```

---

## 3. Troubleshooting

### 3.1 Verifying LDAP Server Connectivity and Certificate

Before troubleshooting LDAP authentication issues, verify that the IRIS Focus system can reach the LDAP server and that the SSL certificate is valid.

**Test LDAPS connectivity**:

```bash
# Preferred method: Test using ldaps protocol directly
curl ldaps://ldap.example.com:636

# Alternative: Test using https protocol
curl https://ldap.example.com:636
```

**Note**: `curl` supports the `ldaps://` protocol, which directly tests LDAP over SSL. This is the preferred method as it will cause the LDAP server to return protocol-specific information, confirming both connectivity and that the service is actually LDAP.

**Expected results**:

- **Success**: With `ldaps://`, you may see LDAP protocol responses or errors like "curl: (67) LDAP local: bind failed". With `https://`, you receive an error like "curl: (52) Empty reply from server" or "malformed response". These indicate the SSL handshake succeeded and the server is reachable.

- **Certificate Error**: You receive an error like "SSL certificate problem: unable to get local issuer certificate" or "SSL certificate problem: self signed certificate". This indicates the certificate is not trusted by the system.

- **Connection Error**: You receive "Failed to connect" or "Connection refused". This indicates a network connectivity problem or incorrect hostname/port.

**Installing SSL Certificates on AlmaLinux 9**:

If you encounter certificate trust issues, you need to install the LDAP server's certificate (or CA certificate) on the IRIS Focus system.

1. **Obtain the certificate**: Get the certificate file from your LDAP administrator. It should be in PEM format (text file starting with `-----BEGIN CERTIFICATE-----`).

2. **Copy certificate to trusted location**:

```bash
# Copy the certificate to the system trust store
sudo cp ldap-server-cert.pem /etc/pki/ca-trust/source/anchors/

# Update the trust store
sudo update-ca-trust extract
```

3. **Verify the certificate is trusted**:

```bash
curl ldaps://ldap.example.com:636
```

4. **Restart the webapp service**:

```bash
systemctl restart vaisala-radarsw-webapp
```

**Alternative: Extract certificate directly from LDAP server**:

If you don't have the certificate file, you can extract it from the LDAP server. Note that you need to install the **CA (Certificate Authority) certificate** that issued the server certificate, not the server certificate itself.

```bash
# Method 1: Extract the CA certificate chain (excluding the server cert)
# This gets the intermediate and root CA certificates
echo | openssl s_client -connect ldap.example.com:636 -showcerts 2>/dev/null | \
  awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/ {if (++count > 1) print}' | \
  sudo tee /etc/pki/ca-trust/source/anchors/ldap-ca.pem > /dev/null

# Update trust store
sudo update-ca-trust extract

# Verify the certificate is now trusted
curl ldaps://ldap.example.com:636

# Restart webapp
systemctl restart vaisala-radarsw-webapp
```

**Troubleshooting certificate extraction**:

If the above method doesn't work (you still get "unable to get local issuer certificate"), you may need to install the complete certificate chain:

```bash
# Extract the full certificate chain
echo | openssl s_client -connect ldap.example.com:636 -showcerts 2>/dev/null | \
  awk '/BEGIN CERTIFICATE/,/END CERTIFICATE/ {print}' | \
  sudo tee /etc/pki/ca-trust/source/anchors/ldap-full-chain.pem > /dev/null

# Update trust store
sudo update-ca-trust extract

# Verify
curl ldaps://ldap.example.com:636
```

If you continue to have certificate issues, contact your LDAP/Active Directory administrator to obtain the proper CA certificate files.

### 3.2 Testing LDAP Authentication with ldapsearch

The `ldapsearch` command-line tool allows you to test LDAP authentication and attribute retrieval independently of IRIS Focus. This is invaluable for troubleshooting configuration issues.

**Install openldap-clients package** (if not already installed):

```bash
sudo dnf install openldap-clients
```

**Basic ldapsearch test**:

```bash
ldapsearch -x -H ldaps://ldap.example.com:636 \
  -D "user@corp.example.com" \
  -W \
  -b "OU=Users,DC=corp,DC=example,DC=com" \
  "(&(objectClass=person)(sAMAccountName=jdoe))" \
  sAMAccountName givenName sn mail memberOf
```

**Parameter explanations**:

- `-x`: Use simple authentication (username/password)
- `-H ldaps://...`: LDAP server URL (use the same URL as in your config)
- `-D "user@..."`: Bind DN - the username to authenticate as (this is what you're testing)
- `-W`: Prompt for password (secure - password not visible in command history)
- `-b "OU=..."`: Search base - must match your config's `search.base`
- `"(&(objectClass=person)(sAMAccountName=jdoe))"`: Search filter - replace `jdoe` with the actual username
- Final arguments: Attributes to retrieve (should match your config's `search.attributes`)

**Successful output example**:

```
# extended LDIF
#
# LDAPv3
# base <OU=Users,DC=corp,DC=example,DC=com> with scope subtree
# filter: (&(objectClass=person)(sAMAccountName=jdoe))
# requesting: sAMAccountName givenName sn mail memberOf
#

# John Doe, Users, corp.example.com
dn: CN=John Doe,OU=Users,DC=corp,DC=example,DC=com
sAMAccountName: jdoe
givenName: John
sn: Doe
mail: john.doe@example.com
memberOf: CN=GRP_FOCUS_USERS,OU=Groups,DC=corp,DC=example,DC=com
memberOf: CN=GRP_FOCUS_RADAR_ACCESS,OU=Groups,DC=corp,DC=example,DC=com

# search result
search: 2
result: 0 Success
```

**Common ldapsearch variations**:

Different LDAP servers and configurations may require different bind formats:

```bash
# Variation 1: UPN format (common for Active Directory)
ldapsearch -x -H ldaps://ldap.example.com:636 \
  -D "jdoe@corp.example.com" -W \
  -b "OU=Users,DC=corp,DC=example,DC=com" \
  "(&(objectClass=person)(sAMAccountName=jdoe))" \
  sAMAccountName givenName sn mail memberOf

# Variation 2: Email format
ldapsearch -x -H ldaps://ldap.example.com:636 \
  -D "john.doe@example.com" -W \
  -b "OU=Users,DC=corp,DC=example,DC=com" \
  "(&(objectClass=person)(mail=john.doe@example.com))" \
  sAMAccountName givenName sn mail memberOf

# Variation 3: Full DN format (for restrictive LDAP servers)
ldapsearch -x -H ldaps://ldap.example.com:636 \
  -D "CN=John Doe,OU=Users,DC=corp,DC=example,DC=com" -W \
  -b "OU=Users,DC=corp,DC=example,DC=com" \
  "(&(objectClass=person)(cn=John Doe))" \
  sAMAccountName givenName sn mail memberOf

# Variation 4: Simple username (OpenLDAP with uid attribute)
ldapsearch -x -H ldaps://ldap.example.com:636 \
  -D "uid=jdoe,ou=people,dc=example,dc=com" -W \
  -b "ou=people,dc=example,dc=com" \
  "(&(objectClass=inetOrgPerson)(uid=jdoe))" \
  uid givenName sn mail memberOf
```

**Troubleshooting ldapsearch errors**:

- **"Invalid credentials"**: Wrong username or password. Try different bind formats (-D parameter).
- **"No such object"**: Incorrect search base (-b parameter). Verify the base DN with your LDAP administrator.
- **"Operations error"** or **"Inappropriate authentication"**: Anonymous bind not allowed. Ensure you're providing credentials with -D and -W.
- **"Can't contact LDAP server"**: Network connectivity issue or wrong URL. Check hostname, port, and firewall rules.
- **Certificate errors**: See Section 3.1 for certificate installation steps.

### 3.3 Checking Local User Accounts

You can query the IRIS Focus database directly to see which users are configured for local vs LDAP authentication.

**List all local accounts** (ldap_user = false):

```bash
psql -h localhost -d wxdb2 -U wxuser -c 'SELECT email, ldap_user FROM identity.identity WHERE ldap_user = false;'
```

**List all LDAP-managed accounts** (ldap_user = true):

```bash
psql -h localhost -d wxdb2 -U wxuser -c 'SELECT email, ldap_user FROM identity.identity WHERE ldap_user = true;'
```

**List all users with their authentication type**:

```bash
psql -h localhost -d wxdb2 -U wxuser -c 'SELECT email, ldap_user, CASE WHEN ldap_user THEN '\''LDAP'\'' ELSE '\''Local'\'' END AS auth_type FROM identity.identity ORDER BY ldap_user, email;'
```

**Check specific user**:

```bash
psql -h localhost -d wxdb2 -U wxuser -c "SELECT email, ldap_user FROM identity.identity WHERE email = 'jdoe@example.com';"
```

**Interpreting results**:

- `ldap_user = false`: Local user account - authenticates with local database password
- `ldap_user = true`: LDAP-managed account - authenticates via LDAP server
- `ldap_user IS NULL`: Should not occur in normal operation (indicates database issue)

**Manually converting a user back to local authentication** (emergency use only):

```bash
# WARNING: Only do this if you need to emergency-convert an LDAP user to local authentication
psql -h localhost -d wxdb2 -U wxuser -c "UPDATE identity.identity SET ldap_user = false WHERE email = 'jdoe@example.com';"

# You'll also need to reset the password through the admin interface, as the current password is an unusable marker
```

### 3.4 Common Configuration Issues

**Issue: Users can't log in after enabling LDAP**

- **Check**: Is `ldap.enabled` set to `true` in `application.yaml`?
- **Check**: Did you restart the webapp service after configuration changes?
- **Check**: Can ldapsearch successfully authenticate with the same credentials?
- **Check**: Are the `search.base` and `authentication` patterns correct for your LDAP structure?
- **Check**: Is the `local-admin` account configured correctly and does it exist in IRIS Focus?

**Issue: Users authenticate but get "No subscription" message**

- **Cause**: User authenticated successfully but has no LDAP groups that map to IRIS Focus roles.
- **Solution**: Ensure the user is a member of at least one LDAP group configured in the `ldap.roles` section.
- **Check**: Run ldapsearch to verify the user's `memberOf` attribute contains the expected groups.

**Issue: Roles are not synchronizing from LDAP**

- **Cause**: Role mapping doesn't match LDAP group names exactly (case-sensitive).
- **Solution**: Verify the exact group names in LDAP using ldapsearch and ensure they match the `ldap.roles` configuration exactly.
- **Check**: Enable debug logging (Section 3.5) and look for role mapping messages in the logs.

**Issue: Admin privileges not being revoked when removed from LDAP**

- **Cause**: This was a bug in versions prior to FIRE-12667. Admins were previously exempted from role synchronization.
- **Solution**: Upgrade to a version that includes the FIRE-12667 fixes. Current versions synchronize all users' roles, including admins.

**Issue: LDAP user can still log in with old password after LDAP password change**

- **Cause**: This was a critical security bug in versions prior to FIRE-12667.
- **Solution**: Upgrade to a version that includes the FIRE-12667 fixes, which prevent local authentication for LDAP users.
- **Verify**: Check the user's `ldap_user` flag in the database. It should be `true` for LDAP users.

**Issue: User creates multiple accounts with case variations (admin, Admin, ADMIN)**

- **Cause**: Username normalization bug in versions prior to FIRE-12667.
- **Solution**: Upgrade to a version that includes canonical username enforcement.
- **Cleanup**: Manually delete duplicate accounts through the admin interface or database.

**Issue: "Invalid credentials" errors despite correct password**

- **Check**: Is the bind template format correct for your LDAP server? Try variations (see Section 3.2).
- **Check**: Does the user account exist in the LDAP directory under the configured search base?
- **Check**: Is the user account enabled (not disabled or locked) in LDAP?
- **Check**: Does the LDAP server require additional authentication attributes?

**Issue: "Focus-radar" or "focus-lightning" license seats exhausted**

- **Cause**: All available license seats are in use by currently logged-in users.
- **Solution**:
  - Wait for other users to log out (seats are released on logout)
  - Increase license seat count by updating your license file
  - Review which users have these roles assigned and whether they all need them
- **Monitor**: Enable debug logging to track license seat usage (Section 3.5)

### 3.5 Debug Logging

For detailed troubleshooting of LDAP authentication issues, enable debug logging for the LDAP-related components.

**Enable debug logging**:

Edit `/etc/vaisala/radarsw/configuration/logback.xml` and add these logger entries:

```xml
<!-- LDAP Authentication Debug Logging -->
<logger name="com.vaisala.fire.server.service.ldap" level="DEBUG"/>
<logger name="com.vaisala.fire.server.service.repository.LdapUserRepository" level="DEBUG"/>
<logger name="com.vaisala.fire.server.service.UserLoginServiceImpl" level="DEBUG"/>
```

**Restart the webapp service**:

```bash
systemctl restart vaisala-radarsw-webapp
```

**View debug logs**:

```bash
# Tail the log file in real-time
tail -f /var/log/vaisala/radarsw/webapp.log

# Search for LDAP-related messages
grep -i "LdapUserRepository\|UserLoginServiceImpl" /var/log/vaisala/radarsw/webapp.log

# Search for specific user's login attempts
grep -i "username=jdoe" /var/log/vaisala/radarsw/webapp.log
```

**What to look for in debug logs**:

- **LDAP authentication attempts**: Shows which LDAP provider configs and authentication patterns are being tried
- **Username normalization**: Shows canonical username retrieved from LDAP
- **Role mapping**: Shows which LDAP groups were found and how they map to IRIS Focus roles
- **License validation**: Shows focus-radar and focus-lightning license seat consumption
- **ldap_user flag operations**: Shows when users are marked as LDAP-managed or local
- **Transaction commits**: Shows when user account changes are persisted to database
- **Error messages**: Detailed stack traces for authentication failures

**Example debug output**:

```
DEBUG LdapUserRepository - Checking ldap_user flag for user jdoe@example.com
DEBUG LdapUserRepository - User jdoe@example.com has ldap_user=false (local account)
DEBUG UserLoginServiceImpl - Attempting LDAP authentication for user: jdoe
DEBUG UserLoginServiceImpl - LDAP authentication succeeded for user: jdoe
DEBUG UserLoginServiceImpl - Canonical LDAP username: jdoe
DEBUG UserLoginServiceImpl - LDAP groups: [GRP_FOCUS_USERS, GRP_FOCUS_RADAR_ACCESS]
DEBUG UserLoginServiceImpl - Mapped roles: [user, focus-radar]
DEBUG UserLoginServiceImpl - License validation: focus-radar seat available (3/10 seats used)
DEBUG LdapUserRepository - Set ldap_user=true for user jdoe@example.com
```

**Disable debug logging after troubleshooting**:

Debug logging can generate large log files. After troubleshooting is complete, change the level back to `INFO` or remove the logger entries:

```xml
<logger name="com.vaisala.fire.server.service.ldap" level="INFO"/>
<logger name="com.vaisala.fire.server.service.repository.LdapUserRepository" level="INFO"/>
<logger name="com.vaisala.fire.server.service.UserLoginServiceImpl" level="INFO"/>
```

Restart the webapp service to apply changes.

### 3.6 Testing Checklist

After configuring LDAP authentication, use this checklist to verify proper operation:

- [ ] **Connectivity Test**: `curl ldaps://ldap-server:636` succeeds without certificate errors
- [ ] **ldapsearch Test**: Can authenticate and retrieve attributes for a test user
- [ ] **LDAP User Login**: LDAP user can log in with their LDAP credentials
- [ ] **Role Assignment**: LDAP user receives correct roles based on group membership
- [ ] **Local User Login**: Existing local users can still log in (if `preserve-local-accounts: true`)
- [ ] **Admin Account Preserved**: Emergency local admin account still works
- [ ] **Role Synchronization**: Changing LDAP groups and logging in again updates roles
- [ ] **Password Change**: Changing LDAP password requires new password on next login
- [ ] **License Validation**: Focus-radar and focus-lightning roles properly consume license seats
- [ ] **Multiple Authentication Patterns**: Users can log in with different formats (username, email, UPN)
- [ ] **Case Insensitivity**: Logging in as "JDoe" and "jdoe" uses the same account
- [ ] **Admin Role Revocation**: Removing admin group from LDAP user removes admin privileges on next login

---

## Appendix: Migration from Previous Versions

If you are upgrading from a version prior to FIRE-12667, be aware of the following changes:

### Security Enhancements

1. **LDAP users can no longer authenticate with stale passwords**: The system now prevents local authentication fallback for LDAP-managed users.

2. **Administrator role synchronization**: Admin users now have their roles synchronized from LDAP on every login, just like regular users.

3. **Username canonicalization**: Usernames are normalized to prevent duplicate accounts with case variations.

### Database Changes

A new `ldap_user` boolean column has been added to the `identity.identity` table. This column defaults to `false` for backward compatibility. Existing users will be migrated automatically:

- Existing local users: Remain as `ldap_user = false` (local authentication continues to work)
- Existing LDAP users: Will be converted to `ldap_user = true` on their next login

### Configuration Changes

A new configuration option `ldap.preserve-local-accounts` has been added. The default value is `false`, which maintains backward compatibility for full LDAP migration. Set this to `true` if you want to preserve local accounts and prevent automatic conversion to LDAP management. Note that when set to `false`, ALL local accounts (including those created after enabling LDAP) will be converted to LDAP management upon successful LDAP authentication.

### Rollback Considerations

If you need to roll back to a previous version after upgrading, the `ldap_user` column can remain in the database (it will be ignored by older versions). However, users converted to LDAP management will have unusable passwords and will need their passwords reset through the admin interface to use local authentication again.
