# Access layers — Pysae IaC

Where and how a collaborator's access is declared. Three independent layers, two repos, three Terraform states. Examples use placeholder names (`jane.doe`, `prenom.nom`) — never put real teammate emails in this public package.

## Table of contents

1. [Overview](#overview)
2. [Layer 1 — AWS IAM (account-wide)](#layer-1--aws-iam-account-wide)
3. [Layer 2 — EKS cluster access (per env)](#layer-2--eks-cluster-access-per-env)
4. [Layer 3 — MongoDB Atlas (per env)](#layer-3--mongodb-atlas-per-env)
5. [Access profiles (level → layers)](#access-profiles-level--layers)
6. [Username vs email](#username-vs-email)
7. [Apply order and states](#apply-order-and-states)
8. [Offboard checklist](#offboard-checklist)

## Overview

| Layer | Repo / file | Keyed by | Scope |
|---|---|---|---|
| AWS IAM | `infra-common/users.tf` → `local.users` | username | account-wide |
| EKS access | `infra-cluster/clusters/<env>/main.tf` → `infra_users` | username | per env |
| MongoDB Atlas | `infra-cluster/clusters/<env>/main.tf` → `atlas.admin_usernames` / `read_usernames` | **email** | per env |

A person rarely needs all three. Resolve only the layers their role implies (profiles below).

## Layer 1 — AWS IAM (account-wide)

File: `infra-common/users.tf`. A single map drives everything:

```hcl
locals {
  users = {
    "jane.doe"    = ["root"]
    "john.roe"    = []                                  # IAM user with no privileged group
    "fin.person"  = ["billing", "cost-explorer", "mfa"]
  }
  # ...
  root_users = [for name, roles in local.users : name if contains(roles, "root")]
  dev_users  = [for name, roles in local.users : name if contains(roles, "dev")]
  # ... one derived list per role
}
```

Each entry creates an `aws_iam_user`, an `aws_iam_access_key`, a Secrets Manager secret `iam/<username>/access-key`, and membership in the `Pysae` group (self-management). Roles add the user to **additional** groups via the derived `local.<role>_users` lists.

**Available roles** (per the repo's own `CLAUDE.md`): `root`, `dev`, `read-only`, `billing`, `billing-ro`, `cost-explorer`, `cost-explorer-ro`, `route53`, `route53-ro`, `mfa`, `gitlab-deploy`, `secrets-manager`, `secrets-manager[<path>]`, `website-tools`.

### Gotcha: a role only grants something if it is *wired*

A role appearing in `local.users` and getting a `local.<role>_users` list does **not** by itself grant access — a group membership resource must consume that list. Always verify:

```bash
grep -n "<role>_users" infra-common/*.tf
```

If the only hit is the `local.<role>_users = [...]` definition (no `aws_iam_group_membership` consuming it), the role is a **no-op**: the user is created and self-manages, but gets no extra AWS rights.

Known case: **`dev` is currently a no-op** — `local.dev_users` is computed but not consumed by any group. So `["dev"]` ≡ `[]` in effect. For someone who needs EKS + DB but no broad AWS console rights, use an **empty list `[]`**, which is what the existing EKS-only teammates use. It is the honest declaration and matches the convention. (If a `Dev` group gets wired later, revisit this.)

## Layer 2 — EKS cluster access (per env)

File: `infra-cluster/clusters/<env>/main.tf`, map `infra_users` (username → tier):

```hcl
infra_users = {
  "jane.doe" = "admin"
  "john.roe" = "rw"
  "obs.user" = "ro"
}
```

Tiers (from the in-file comment):

| Tier | EKS policy | AWS |
|---|---|---|
| `admin` | `AmazonEKSClusterAdminPolicy` | R/W (`infra-<env>`) |
| `rw` | `AmazonEKSAdminPolicy` | R/W (`infra-<env>`) |
| `ro` | `AmazonEKSViewPolicy` | R/O (`infra-<env>-ro`) |

The username **must exist in `infra-common/users.tf`** — the cluster builds an EKS access entry from the IAM user's ARN. This is why the IAM user only needs to *exist* (empty role list is fine) for EKS access to work: the tier is granted by the access entry, not by an IAM group.

## Layer 3 — MongoDB Atlas (per env)

Same file, inside the `atlas` object. Two **email** lists:

```hcl
atlas = {
  # ...
  admin_usernames = [           # → mongodbatlas_team with GROUP_OWNER
    "jane.doe@pysae.com",
  ]
  read_usernames = [            # → mongodbatlas_team with GROUP_DATA_ACCESS_READ_ONLY
    "john.roe@pysae.com",
  ]
}
```

`admin_usernames` = project owner (full DB admin). `read_usernames` = read-only. Note `read_usernames` may be `[]` on some clusters — turn it into a list when adding the first entry.

## Access profiles (level → layers)

Presets for the common cases. "à-la-carte" roles (billing, route53…) are IAM-only and don't touch EKS/Mongo.

| level | IAM (`users.tf`) | EKS (`infra_users`) | Mongo |
|---|---|---|---|
| `admin` | `["root"]` | `admin` | `admin_usernames` |
| `dev` / `rw` | `[]` (EKS-only; `dev` is a no-op) | `rw` | `read_usernames` |
| `ro` | `[]` | `ro` | `read_usernames` |
| IAM-only (e.g. finance) | `["billing", "cost-explorer", "mfa"]` | — | — |

Always confirm with the user when the profile grants `admin`/`root` or touches prod.

## Username vs email

- **IAM + EKS** use a **username** (no `@pysae.com`), derived from the email local-part.
- **Mongo** uses the **full email**.

The username scheme is not perfectly uniform across historical entries (some use `.`, older ones use `-`). Reuse the **dominant / most recent** pattern: the email local-part verbatim (e.g. `prenom.nom@pysae.com` → `prenom.nom`). Don't invent a new scheme; eyeball the current `users.tf` and match it.

## Apply order and states

Backend: S3 bucket `pysae-tf-states`, region `eu-west-3`. States:

| Root | State key |
|---|---|
| `infra-common` | `infra-common.tfstate` |
| `infra-cluster/clusters/dev` | `clusters/dev.tfstate` |
| `infra-cluster/clusters/prod` | `clusters/prod.tfstate` |

**Apply `infra-common` first** (creates the IAM user + ARN), then each cluster root (its EKS access entry references that ARN). A cluster apply for a user missing from `infra-common` fails.

Each root is a separate `terraform apply`. Onboard touches up to three states; do them in the order above.

## Offboard checklist

Remove the person from **every** layer they appear in — find them first:

```bash
grep -rn "<username>\|<email>" infra-common/users.tf infra-cluster/clusters/*/main.tf
```

- Drop the `local.users` entry → destroys the IAM user, its access key secret and all group memberships (`force_destroy = true` is already set, so non-empty users delete cleanly).
- Drop the `infra_users` entry in each env → removes the EKS access entry.
- Drop the email from `admin_usernames` / `read_usernames` in each env → removes the Mongo team membership.

Order is usually irrelevant for removal. After apply, confirm via `/aws-secrets` that `iam/<username>/access-key` is gone.
