# Access Lab (Jarroba Tools) — a guide for AIs

This guide is for an **AI to write a "world"**: a small JSON object describing a real (or example)
identity/RBAC setup — Azure RBAC, AWS IAM/Organizations, GCP Resource Manager, or Kubernetes RBAC —
that a person can **paste** into the `/accesos` tool (**Import** tab, "World (JSON)" box) or that an
agent can pass directly as the `world` argument of the `access_check`, `access_who_can` and
`access_diff` MCP tools.

**There is no single universal shape.** Pick ONE provider first — the field names below are exactly
what `crearMundoAzure` / `crearMundoAws` / `crearMundoGcp` / `crearMundoK8s` accept, which is also
exactly what the MCP tools' `world` parameter accepts for that `provider`. Every field is optional
and defaults to empty except `ambitos`, which you almost always want.

## Shared building blocks

These three shapes are the same across all four providers:

```ts
Ambito   { id: string, tipo: string, padreId?: string, etiquetas?: Record<string, string> }
```
A node of the scope tree (management group/subscription/resource group, org/OU/account, cluster/
namespace…). `tipo` is a free label used only for display, **except** `tipo: 'cuenta'` for AWS
(the engine needs to know which scopes are accounts) — everything else is cosmetic. `padreId` links
it to its parent; omit it for a root. A resource you ask about is just a leaf `Ambito` like any
other — you don't need to declare "resources" separately from "scopes".

```ts
Pertenencia { grupoId: string, miembroId: string }
```
A static group membership (`miembroId` is a member of `grupoId`), nestable — a group can be a
member of another group. Not used by AWS (IAM doesn't have nested principal groups in this model).

```ts
principalId: string
```
Principals (users, groups, service principals / managed identities / IAM roles / Kubernetes
service accounts) are **just strings** — there's no separate `Principal` list to declare. A
principal exists the moment something references its id (a role assignment, a binding, an identity
policy…).

## Azure RBAC

```ts
{
  ambitos?:      Ambito[],
  pertenencias?: Pertenencia[],
  roles?:        { id: string, nombre: string, conceder: string[], excluir?: string[],
                   dataConceder?: string[], dataExcluir?: string[] }[],
  asignaciones?: { id: string, principalId: string, rolId: string, ambitoId: string, condicion?: string }[],
  denegaciones?: { id: string, principales: string[] | 'TODOS', excludePrincipales?: string[],
                   ambitoId: string, doNotApplyToChildScopes?: boolean,
                   acciones: string[], accionesExcluidas?: string[],
                   dataAcciones?: string[], dataAccionesExcluidas?: string[], condicion?: string }[],
}
```

- **Control plane vs. data plane are separate grant sets.** `conceder`/`excluir` are control-plane
  actions (e.g. `Microsoft.Storage/storageAccounts/read`); `dataConceder`/`dataExcluir` are
  data-plane (e.g. `Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read`). Owner's
  `conceder: ['*']` does **not** reach data-plane actions — pass `plane: "data"` when calling
  `access_check`/`access_who_can` (or the "Datos" toggle in the UI) to ask a data-plane question.
- **`excluir` ("NotActions") is local to its own role, not a global deny.** If role A excludes an
  action but role B (a *different* assignment) grants it, the principal gets it — the union of
  assignments wins. If you omit `roles`, the built-in catalogue (`owner`, `contributor`, `reader`,
  `storage-blob-data-reader`, `storage-blob-data-contributor`) is used, so you can write
  `asignaciones` referencing those ids without declaring `roles` at all.
- **`denegaciones` model deny assignments**, which really do block regardless of any grant.
  `principales: 'TODOS'` means Azure's *All Principals*; `excludePrincipales` is the one way out.
  They inherit to child scopes unless `doNotApplyToChildScopes` is set.
- **Nothing implicit is ever granted.** If no assignment and no deny decide it, the answer is
  `denied` — Azure has no "allow by default" path.
- `condicion` (on an assignment or a deny) is a free string: the engine does **not** evaluate ABAC
  conditions. If present and it would be the deciding factor, the verdict is `indeterminate` rather
  than a guess.

Action strings look like `Microsoft.Storage/storageAccounts/blobServices/containers/blobs/read`;
wildcards (`*`) are allowed in any segment, e.g. `Microsoft.Compute/*`, `*/read`.

Sources: [role-definitions](https://learn.microsoft.com/en-us/azure/role-based-access-control/role-definitions),
[deny-assignments](https://learn.microsoft.com/en-us/azure/role-based-access-control/deny-assignments).

### Example: Contributor is missing one action that a narrower role grants

```json
{
  "ambitos": [{ "id": "sub", "tipo": "subscription" }, { "id": "rg", "tipo": "resourceGroup", "padreId": "sub" }],
  "roles": [{ "id": "rg-admin", "nombre": "RG Role Admin", "conceder": ["Microsoft.Authorization/roleAssignments/write"] }],
  "asignaciones": [
    { "id": "a1", "principalId": "ana", "rolId": "contributor", "ambitoId": "sub" },
    { "id": "a2", "principalId": "ana", "rolId": "rg-admin", "ambitoId": "rg" }
  ]
}
```
`ana` can assign roles at `rg` (the narrower role grants it there) but not at `sub` (Contributor
excludes it, and nothing else grants it at that scope).

## AWS IAM / Organizations

```ts
{
  ambitos?:       Ambito[],                 // give account-level scopes tipo: 'cuenta'
  cuentasGestion?: string[],                // ids (from `ambitos`) that are the management account
  identidades?:   Record<string, Declaracion[]>,   // key = principalId
  recursos?:      Record<string, DeclaracionRecurso[]>,   // key = the resource's ambitoId
  scps?:          Record<string, Declaracion[]>,   // key = an ambitoId in the org tree
  boundaries?:    Record<string, Declaracion[]>,   // key = principalId
}
// Declaracion:         { efecto: 'permitir'|'denegar', acciones: string[], condicion?: string }
// DeclaracionRecurso:  Declaracion & { principalId: string, via: 'usuario'|'sesionDeRol'|'rol' }
```

- **Scope tree**: root → OU (nestable) → account → resource. Mark every account-level `Ambito` with
  `tipo: "cuenta"` — that's the only thing the engine reads from `tipo` here.
- **SCPs need an explicit Allow at every level** in the path root→…→account (unlike Azure/GCP, one
  level missing it is enough to deny), and a level with no entry in `scps` gets the AWS-managed
  `FullAWSAccess` default (allow everything) — same as a real AWS org. SCPs never apply to the
  management account (list it in `cuentasGestion`) or to service-linked roles.
- **`recursos` keys are the *resource's own* `ambitoId`**, and each entry's `principalId` is who the
  resource-based policy names. The `via` field is what a naive model gets wrong: a resource policy
  naming a plain **user** ARN or a role-**session** ARN (`via: "usuario"` or `"sesionDeRol"`) is
  **not** limited by that principal's boundary; naming the **role** ARN itself (`via: "rol"`, no
  session) **is** still limited by the boundary.
- **Identity ∪ resource, then intersected with the boundary** (if the principal has one at all — no
  entry in `boundaries` means no boundary, not "boundary of nothing").
- **Implicit default is allow**, not deny: if nothing denied it and it cleared the SCP gate and got
  an Allow from identity or resource, it's allowed — the opposite of Azure.

Action strings look like `s3:GetObject`, `ec2:DescribeInstances`, `s3:*`.

Sources: [scps_evaluation](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps_evaluation.html),
[scps](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_manage_policies_scps.html),
[policy-eval-denyallow](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_evaluation-logic_policy-eval-denyallow.html).

### Example: an OU-level SCP that only allows EC2

```json
{
  "ambitos": [{ "id": "raiz", "tipo": "raiz" }, { "id": "ou", "tipo": "ou", "padreId": "raiz" }, { "id": "cuenta", "tipo": "cuenta", "padreId": "ou" }],
  "identidades": { "ana": [{ "efecto": "permitir", "acciones": ["*"] }] },
  "scps": { "ou": [{ "efecto": "permitir", "acciones": ["ec2:*"] }] }
}
```
`ana` has `AdministratorAccess`-equivalent identity permissions, but the OU's SCP only allows EC2,
so she can describe EC2 instances but cannot read S3 — the SCP gate blocks it before identity is
even consulted.

## GCP Resource Manager

```ts
{
  ambitos?:      Ambito[],
  pertenencias?: Pertenencia[],
  roles?:        { id: string, permisos: string[] }[],
  bindings?:     { id: string, principalId: string, rolId: string, ambitoId: string }[],
  denyPolicies?: { id: string, principales: string[], excludePrincipales?: string[],
                   permisosDenegados: string[], ambitoId: string }[],
}
```

- **Deny policies are checked before allow bindings**, and — unlike AWS's per-level gate — **both**
  deny and allow **inherit down the resource hierarchy** with a plain union: a single binding
  *anywhere* in the ancestor path is enough, there's no "must allow at every level" requirement.
- **Implicit default is deny.**

Permission strings look like `storage.objects.get`, `compute.instances.list`.

Source: [policy-types](https://docs.cloud.google.com/iam/docs/policy-types) (deny checked before allow,
both inherit down the hierarchy, allow "operates on a union model").

### Example

```json
{
  "ambitos": [{ "id": "org", "tipo": "organization" }, { "id": "proj", "tipo": "project", "padreId": "org" }],
  "roles": [{ "id": "viewer", "permisos": ["storage.objects.get"] }],
  "bindings": [{ "id": "b1", "principalId": "ana", "rolId": "viewer", "ambitoId": "org" }]
}
```
`ana` can read objects in `proj` even though the binding is at `org` — allow bindings flow down.

## Kubernetes RBAC

```ts
{
  ambitos?:      Ambito[],       // a 'cluster' root plus one child per namespace, typically
  pertenencias?: Pertenencia[],
  roles?:        { id: string, reglas: string[] }[],       // patterns "verb:resource", e.g. "get:pods"
  bindings?:     { id: string, principalId: string, rolId: string, ambitoId: string }[],
}
```

- **Purely additive: there is no deny rule.** Every RoleBinding/ClusterRoleBinding that reaches the
  principal at that scope is **unioned** — the first one that matches a rule wins, but a rule that
  doesn't match one binding might match another.
- A binding with `ambitoId: "cluster"` reaches every namespace underneath (model a ClusterRoleBinding
  this way, or a RoleBinding that references a ClusterRole but is scoped to one namespace by giving
  it that namespace's `ambitoId` instead).
- **Implicit default is deny.**

Rule strings are `verb:resource`, e.g. `get:pods`, `*:secrets`, `list:*`.

Source: [Kubernetes RBAC — "permissions are purely additive"](https://kubernetes.io/docs/reference/access-authn-authz/rbac/).

### Example

```json
{
  "ambitos": [{ "id": "cluster", "tipo": "cluster" }, { "id": "default", "tipo": "namespace", "padreId": "cluster" }],
  "roles": [{ "id": "pod-reader", "reglas": ["get:pods", "list:pods"] }],
  "bindings": [{ "id": "b1", "principalId": "jane", "rolId": "pod-reader", "ambitoId": "default" }]
}
```
`jane` can `get:pods` in `default`, but not in any other namespace, and not `delete:pods` anywhere.

## The question you ask against a world

Once you have a `world`, a question is always the same three fields (`plane` only matters for
Azure): `principalId`, `action`, `scopeId`. The answer is one of three verdicts, never a guess:

- **allowed** / **denied** — a rule decided it, and the trace names which one.
- **indeterminate** — something in the world would decide it but isn't modelled (an ABAC condition
  string, mainly). This model refuses to invent an answer for what it can't evaluate.

## Ground rules

1. **Return only the JSON** — the object itself, no prose, no code fences.
2. **Leave out what you don't know.** A missing optional field defaults to empty, exactly like a
   real environment with nothing configured there; inventing a role or a binding that doesn't exist
   silently changes the answer without anyone noticing.
3. **Don't model what the source material doesn't say.** If you're describing a real environment
   and a detail (a condition, an exact boundary) isn't in what you were given, leave it out rather
   than guess — the tool will say `indeterminate` honestly instead of you saying something false.
4. **Test it before handing it over**: paste it into the "World (JSON)" box in the Import tab (or
   call `access_check` if you have the MCP) and ask at least one question you already know the
   answer to.
5. **Nothing here calls an external AI provider.** This is a static reasoning model over data you
   provide; there is no lookup of a real Azure/AWS/GCP/Kubernetes account.
