# Keep customers' data apart

When many customer companies share your app, one must never see another's data. In
Jadpo you name the entity that is a customer, and every record then belongs to one. A
grant only ever applies inside companies where the caller has a membership, and that
holds for every query and write in the app, with no filter for you to remember. This
guide makes a work tracker where each company's projects and chores stay its own.

You start from an app where people sign in as a `Member`, with an action called
`register`, and with queries and output types for projects and chores.
[Add sign-in](/guides/guide-add-sign-in.md) and [Query records](/guides/guide-query-records.md) show how to
write those.

## Name the tenant

A tenant is one record of the entity you choose. The root `policy.jadpo` names that
entity once. An action that isn't reached through an entity's own list, such as starting
a company, is listed under `actions:`.

`policy.jadpo`:

```jadpo
use work/entities { Company }
use work

policy {
    tenant: Company
    actions: {
        work.found_company: [Access.authenticated]
    }
}
```

Naming a tenant adds a rule to every entity: it needs a reference to `Company`. Leave one out
and `jadpo check` refuses it:

```text
error: `Note` doesn't belong to a `Company`
 --> domains/work/entities/note.jadpo:4:8
  |
4 | entity Note {
  |        ^^^^ no reference to `Company`
  reason: Root `policy { }` names `Company` as the tenant, so every entity carries its own reference to it and its grants apply only within the `Company`s where the caller has a membership; an entity every tenant shares says `tenant: Tenancy.shared`.
  next: Add a field typed `Company.id` to `Note`, or `tenant: Tenancy.shared` to its `policy:` (agent)
  rule: policy.scope_missing
  help: https://jadpo.dev/docs/errors/policy.scope_missing
```

`Member` is the other way out: its records belong to no company, so its `policy:` says
`tenant: Tenancy.shared`. A person reads a note saying so when it goes in.

`domains/work/entities/member.jadpo`:

```jadpo
use work/types { Handle }

/// Someone who signs in.
entity Member {
    id: Uuid
    handle: Handle {
        unique: true
    }

    policy: {
        create: [Access.public]
        read: [id]
        tenant: Tenancy.shared
    }
}
```

## Say who belongs where

People belong to a company through a membership, a stored record saying which member holds
which role in which company. Its `membership:` section names those three fields. Grants then
name roles: `CompanyRole.viewer` in a `read:` list lets in members who hold that role in the
record's company.

`domains/work/types/company_role.jadpo`:

```jadpo
/// What a member may do in a company.
type CompanyRole = Enum {
    owner
    viewer
}
```

`domains/work/entities/company.jadpo`:

```jadpo
use work/types { CompanyRole, Label }

/// A customer: the tenant every other record belongs to.
entity Company {
    id: Uuid
    name: Label

    policy: {
        read: [CompanyRole.owner, CompanyRole.viewer]
    }
}
```

`domains/work/entities/company_membership.jadpo`:

```jadpo
use work/entities { Company, Member }
use work/types { CompanyRole }

/// A member's role in a company.
entity CompanyMembership {
    id: Uuid
    company_id: Company.id {
        on_delete: OnDelete.cascade
    }
    member_id: Member.id {
        on_delete: OnDelete.cascade
    }
    role: CompanyRole

    membership: {
        scope: company_id
        member: member_id
        role: role
    }

    policy: {
        create: [CompanyRole.owner]
        read: [CompanyRole.owner, member_id]
    }
}
```

## Found a company and add people

A new company has no members, so nobody holds a role that could let them create its first
membership. That first one is allowed in one place only: the action that creates the
company, naming the caller as the member. Every other membership needs someone who belongs
already, so nobody can make themselves a member of someone else's company.

`domains/work/actions/found_company.jadpo`:

```jadpo
use work/entities { Company, CompanyMembership }
use work/types { CompanyRole, Label }

/// Starts a company with its founder as its owner.
action found_company(name: Label, founder: Principal.user) -> Company {
    var company: Company = Company.create {
        set: {
            name: name
        }
    }
    var owner: CompanyMembership = CompanyMembership.create {
        set: {
            company_id: company.id
            member_id: founder.id
            role: CompanyRole.owner
        }
    }
    assert owner.role == CompanyRole.owner
    return company
}
```

`domains/work/actions/add_member.jadpo`:

```jadpo
use work/entities { Company, CompanyMembership, CompanyNotFound, Member, MemberNotFound }
use work/types { CompanyRole }

/// Gives a member a role in a company; only its owners may.
action add_member(
    company_id: Company.id,
    member_id: Member.id,
    role: CompanyRole
) fails CompanyNotFound, MemberNotFound -> CompanyMembership {
    return attempt CompanyMembership.create {
        set: {
            company_id: company_id
            member_id: member_id
            role: role
        }
    }
}
```

## Give each record its tenant

Each entity has its own reference to `Company`, its tenancy reference, even a chore whose
project already has one. A request has no current tenant of its own, and the caller carries
no company. A record's tenant is the one its reference names, and someone who belongs to
several companies is let in to each by their membership there.

`domains/work/entities/project.jadpo`:

```jadpo
use work/entities { Company }
use work/types { CompanyRole, Label }

/// A piece of work in a company.
entity Project {
    id: Uuid
    company_id: Company.id {
        on_delete: OnDelete.cascade
    }
    name: Label

    policy: {
        create: [CompanyRole.owner]
        read: [CompanyRole.owner, CompanyRole.viewer]
    }
}
```

`domains/work/entities/chore.jadpo`:

```jadpo
use work/entities { Company, Project }
use work/types { CompanyRole, Label }

/// One step of a project.
entity Chore {
    id: Uuid
    company_id: Company.id {
        on_delete: OnDelete.cascade
    }
    project_id: Project.id {
        on_delete: OnDelete.cascade
    }
    title: Label

    policy: {
        create: [CompanyRole.owner]
        read: [CompanyRole.owner, CompanyRole.viewer]
    }
}
```

A route says which company it works in through its input.

`domains/work/actions/add_chore.jadpo`:

```jadpo
use work/entities { Chore, Company, CompanyNotFound, Project, ProjectNotFound }
use work/types { ChoreCard, Label }

/// What adding a chore takes.
type AddChore = Input {
    company_id: Company.id
    project_id: Project.id
    title: Label
}

/// Adds a chore to a project.
action add_chore(input: AddChore) fails CompanyNotFound, ProjectNotFound -> ChoreCard {
    var chore: Chore = attempt Chore.create {
        set: {
            company_id: input.company_id
            project_id: input.project_id
            title: input.title
        }
    }
    return ChoreCard {
        id: chore.id
        title: chore.title
    }
}
```

`domains/work/routes/chores.jadpo`:

```jadpo
use work/entities { Company }
use work/types { ChoreCard, ChoreView }
use work/actions { AddChore, add_chore }
use work/queries { company_chores }

/// A company's chores.
routes Chores {
    prefix: /chores

    /// Adds a chore.
    POST / {
        input: AddChore
        output: ChoreCard
        status: SuccessStatus.created
        run: add_chore(input)
    }

    /// A company's chores, each with its project.
    GET /{company_id: Company.id} {
        output: List<ChoreView>
        run: company_chores(company_id)
    }
}
```

Run `jadpo migrate new add_tenants`, then `jadpo records update`.

## Test the walls

Tests make requests as each person, which is what proves one company can't reach another.

`domains/work/routes/chores.test.jadpo`:

```jadpo
use work/entities { Company, CompanyMembership, Member }
use work/types { ChoreCard, ChoreView, CompanyRole, ProjectCard }
use work/actions { AddChore, add_chore, add_member, found_company, register, start_project }

test "a chore can't name another company's project" {
    var ann: Member = attempt register("ann")
    var northwind: Company = found_company("Northwind", Principal.user(ann))
    var contoso: Company = found_company("Contoso", Principal.user(ann))
    var rival: ProjectCard = attempt start_project(contoso.id, "Rival")
    var response: Response = request {
        method: HttpMethod.post
        path: "/chores"
        as: Principal.user(ann)
        input: AddChore {
            company_id: northwind.id
            project_id: rival.id
            title: "Borrow"
        }
    }
    assert response.status == 403
    assert response.code == "not_permitted"
}

test "a viewer sees each chore with its project" {
    var ann: Member = attempt register("ann")
    var bob: Member = attempt register("bob")
    var northwind: Company = found_company("Northwind", Principal.user(ann))
    var viewer: CompanyMembership = attempt add_member(northwind.id, bob.id, CompanyRole.viewer)
    assert viewer.role == CompanyRole.viewer
    var launch: ProjectCard = attempt start_project(northwind.id, "Launch")
    var kickoff: ChoreCard = attempt add_chore(
        AddChore {
            company_id: northwind.id
            project_id: launch.id
            title: "Plan"
        }
    )
    var seen: Response = request {
        method: HttpMethod.get
        path: "/chores/{company_id}"
        params: {
            company_id: northwind.id
        }
        as: Principal.user(bob)
    }
    assert seen.status == 200
    assert seen.output == [
        ChoreView {
            id: kickoff.id
            title: "Plan"
            project: launch
        }
    ]
}

test "someone from another company sees none of its chores" {
    var ann: Member = attempt register("ann")
    var eve: Member = attempt register("eve")
    var northwind: Company = found_company("Northwind", Principal.user(ann))
    var launch: ProjectCard = attempt start_project(northwind.id, "Launch")
    var kickoff: ChoreCard = attempt add_chore(
        AddChore {
            company_id: northwind.id
            project_id: launch.id
            title: "Plan"
        }
    )
    assert kickoff.title == "Plan"
    var seen: Response = request {
        method: HttpMethod.get
        path: "/chores/{company_id}"
        params: {
            company_id: northwind.id
        }
        as: Principal.user(eve)
    }
    assert seen.status == 200
    assert seen.output == []
}
```

```sh
jadpo test
```

```text
tested: 3 passed, 3 route tests
```

These three tests show the three things to know:

- **A reference can't cross companies.** Every write that sets a reference checks it points at
  the same tenant, whatever the caller's grants say. Ann belongs to both companies and still gets
  `403 not_permitted`, because a chore of Northwind's can't name Contoso's project.
- **A member reads what their role lets them.** Bob is a viewer, so he sees each chore along with
  its project, which is always in the same company.
- **An outsider finds nothing.** Eve's request answers `200` with an empty list, not `403`, as if
  the chores weren't there, so she can't even tell that they exist. Policy filters in the query's own
  statement.

## See who can do what

`access.jadpo_record` (written by `jadpo records update`) lists who can do what. It is committed,
so a change that widens access shows as a diff, and needs a person's acceptance:

```text
tenant Company

entity work.Chore
    create CompanyRole.owner
    read CompanyRole.owner
    read CompanyRole.viewer
    tenant company_id
```

`jadpo check` also writes `build/report/permissions.md`, for reading the whole app at once.

## Work with no caller

Schedules and subscribers have no caller, so they run as the system, and a grant must name
`Access.system` to let them in:

- A schedule's run reaches every tenant's records, since no tenant's event starts it.
- A subscriber's delivery reaches the tenant named by its event's field of the tenant's `id`
  type, so an event about a company carries a `company_id`.
- A batch's run reaches the tenant its saved values name.

Work whose value names no tenant reaches only what every tenant shares.

## Read more

[Tenancy](/docs/tenancy.md),
[A membership role](/docs/a-membership-role.md),
[Grants](/docs/grants.md),
[The permissions report](/docs/the-permissions-report.md) and
[Background work and tenants](/docs/background-work-and-tenants.md).
