Skip to content

Three separate questions

  • Lesson 1
  • 45 min
  • Module 1: Identity and permission basics

What you will be able to do

Ask three separate questions of every request to the ticket API: who is calling, in which organization, and what may they do there. Set up the course project, and see that its one shared key answers none of them well.

Before you startWhat to know and to have readyAbout 45 minutes

What you need to know

What you need on your computer

  • Python 3.12 or later, and Docker Desktop, as in the databases course. Install and verify in the Docker course shows the Docker install.
  • About 2 GB of free disk space. No account, no key and no money.

You start a new project folder, ~/projects/ticket-api, from the snapshot start. It is the databases course’s project at its end, with no change.

Think of an office building with one door code. Everybody who works there knows the code. The door opens for anyone who types it.

The code works well while one small company uses the building. Then a second company rents the second floor. Now the code has three problems. The door cannot say who came in: the cleaner, the manager and a visitor all type the same digits. It cannot say which company the person visits. And it cannot say what they may do inside: the code opens every room on every floor.

A good reception desk asks three separate questions instead. Who are you? Show your badge. Which company are you visiting? Is your name on their list? What may you do there? A visitor may sit in the meeting room, but not open the cash box.

The ticket API has a door code. In this lesson, you see exactly what it can and cannot answer.

You know Larkfield’s help desk from the earlier courses. Grace leads the support agents, and Sam answers tickets. Omar, the product manager, reads tickets to plan the next features. Amira builds the ticket API. Tomás is an AI engineer who likes shortcuts.

Larkfield now offers its help-desk service to a second shop, Bramble Books, an independent bookshop. Its owner is Ines. Bramble Books will keep its customers, tickets and files in the same API and the same database. Camille, an experienced Larkfield agent, will help Ines in her first weeks.

Kwame joins the team as solution architect. He guides the design in this course. Before any code, he asks one thing of every request: “Who is the caller, what may they do, and in which organization?”

The project is ticket-api as the databases course left it. If you finished that course, your folder is the same as start. Make a new copy anyway: this course changes it step by step, and the lessons expect the same files as yours.

  1. Get the files of the course. If you cloned the labs repository for an earlier course, update it in ~/projects:

    Terminal window
    cd ~/projects/nextia-learning-labs
    git pull
    cd ..

    If you do not have it yet, clone it in ~/projects with git clone https://github.com/Nextia-Intelligence/nextia-learning-labs.git. Without Git, download the ZIP file and unzip it in ~/projects.

  2. If a folder ticket-api exists already, from the databases course, first stop its containers and delete their data. Docker names the containers and the data volumes after the folder. Your new folder has the same name, so it would reuse the old database, with its old password and its old data:

    Terminal window
    cd ticket-api
    docker compose down --volumes
    cd ..

    Your old files stay, and the course can load the data again at any time. Then rename the old folder, for example to ticket-api-databases. Copy the snapshot start, go into it, and make your .env:

    Terminal window
    cp -R nextia-learning-labs/C23/snapshots/start ticket-api
    cd ticket-api
    cp .env.example .env
  3. Open the folder in VS Code. In .env, replace change-me-local-only with a password of your own, in all three places.

  4. Start Docker Desktop. Then start PostgreSQL and Azurite, the object storage for files:

    Terminal window
    docker compose up -d db azurite

    Run docker compose ps until the db line says (healthy).

  5. Make the virtual environment and install the packages:

    Terminal window
    python3 -m venv .venv
    source .venv/bin/activate
    python -m pip install -r requirements.txt
  6. Load the data:

    Terminal window
    python -m scripts.load
    applied 001_create_classifications
    applied 002_add_score
    …
    applied 012_erasure
    Database is up to date (12 migrations).
    Loaded the small data in 1.9 s:
    customers 40
    tickets 200
    messages 331
    attachments 43
    documents 36
    ai_runs 294
    ticket_embeddings 200
    43 files uploaded to the object storage.
    3 messages have no ticket: they are in orphaned_messages.

    Lines 003 to 011 are left out here. The twelve migrations are the databases course’s. Your time differs.

  7. Run the tests:

    Terminal window
    python -m pytest

    The last line is 108 passed, 1 deselected. The deselected test needs Azurite and runs only with -m azurite, as in the databases course.

If you see these two results, your setup works. The course tested every step on macOS (Apple silicon). The Windows and Linux commands are written, not run.

Now put Kwame’s three questions to the API itself. Start it with an API key, as in the API course. Open a second terminal in the project folder, activate the virtual environment, and run:

Terminal window
API_KEY=local-dev-key fastapi dev

In the first terminal, ask for ticket T-30002 without the key. On Windows, type curl.exe instead of curl:

Terminal window
curl -i http://127.0.0.1:8000/v1/tickets/T-30002
HTTP/1.1 401 Unauthorized
content-type: application/json
x-request-id: 7bbff66891b8
{"error":{"code":"unauthorized","message":"A valid X-API-Key header is required.","request_id":"7bbff66891b8","fields":[]}}

Some headers are left out. Send the same request with the key:

Terminal window
curl -H "X-API-Key: local-dev-key" http://127.0.0.1:8000/v1/tickets/T-30002
{"ticket_id":"T-30002","customer_id":"C-0022","customer_name":"Hugo Garcia",
"subject":"Charged twice for order LK-182074","team":"billing","priority":1,"status":"open",
"message_count":1,"messages":[{"message_id":2,"author":"agent","body":"Could you send the receipt …"}], …}

The answer is shortened here. Now add a message to the ticket, as an agent does:

Terminal window
curl -X POST -H "X-API-Key: local-dev-key" -H "Content-Type: application/json" -d '{"author": "agent", "body": "We refunded one payment."}' http://127.0.0.1:8000/v1/tickets/T-30002/messages
{"message_id":332,"author":"agent","body":"We refunded one payment.","created_at":"2026-10-10T06:09:21.800162Z"}

In PowerShell, quotes inside a string work differently. Put the body in a file, msg.json, and send it with -d "@msg.json".

Look at the API’s terminal. The API wrote one line for each request:

INFO ticket_api: GET /v1/tickets/T-30002 401 10ms queries=0 id=7bbff66891b8
INFO ticket_api: GET /v1/tickets/T-30002 200 27ms queries=3 id=3b9d4f4fea83
INFO ticket_api: POST /v1/tickets/T-30002/messages 201 11ms queries=3 id=89aa010fe214

Now ask the three questions of the last request.

Who wrote the message? The log says POST, the path, 201, a time and a request ID. The database row says author: agent. Grace, Sam and Omar all send the same key, so the request is the same, byte for byte, whoever sent it.

Three people, Grace who leads the support agents, Sam a support agent and Omar a product manager who only reads, each send the same request: POST /v1/tickets/T-30002/messages with X-API-Key: local-dev-key. ticket-api checks one thing, whether the key is right, and answers 201 Created. What it records: a log line POST …/messages 201, and a message with author = agent. A note says the API cannot tell the three people apart, because the bytes are equal.
With one shared key, a request from Grace, from Sam and from Omar is the same request. The API can only check the key.
Text description of the diagram

Grace, Sam and Omar each send POST /v1/tickets/T-30002/messages with the header X-API-Key: local-dev-key. The API compares the key with its setting, and it matches, so it saves the message and answers 201 Created. It records a log line and a message whose author is “agent”. Nothing in the request or in the record names the person.

In which organization? The question has no answer yet. There is one shop, so the API has no word for “organization”. Look in psql: docker compose exec db psql -U tickets -d tickets, then \d tickets. The table has ticket_id, customer_id, subject and ten more columns. No column says which shop a ticket belongs to.

What may the caller do? Everything that the key opens. Omar only needs to read tickets. With the key, he can also write messages, upload files and read every ticket. The API cannot give him less, because it does not know that the caller is Omar.

Type \q to leave psql. Stop the API with Ctrl+C.

Kwame writes the three questions on the whiteboard. Each one has a name.

Authentication answers “Who is the caller?” The caller proves who they are, and the API checks the proof. A shared key proves only “this caller has the key”. From Module 2, a person signs in, and the API receives a user ID: a stable name for that one person, such as usr-sam.

The tenant answers “In which organization?” A tenant is one customer organization whose data lives in a shared application. The word comes from a building: many tenants rent space in it, and each one keeps its own rooms. Larkfield and Bramble Books are the two tenants of the ticket API. “Multi-tenant” means that one application serves several of them.

Authorization answers “What may they do there?” The API checks whether this person may do this action in this organization. Authentication comes first: you cannot decide what a person may do before you know who they are.

A table of three questions in order: 1. Who is the caller? (authentication), 2. In which organization? (tenant), 3. What may they do there? (authorization). The start's API, with one shared key, answers: someone who has the key, not a person; no answer, the API has no organization; everything the key opens, read and write every ticket. At the end of the course, the answers are: usr-sam, from a token that the API checked; larkfield, named in the path, and Sam is a member there; staff, read and change tickets, upload files.
The three questions of every request. The start's key answers the first one weakly and the other two not at all. This course builds the answers in the bottom row.
Text description of the diagram

The three questions, in the order the API asks them. First, who is the caller: authentication. Second, in which organization: the tenant. Third, what may they do there: authorization. Today’s API answers “someone who has the key”, “no answer” and “everything”. At the end of the course, the first answer is “usr-sam, from a checked token”. The second is “larkfield: the path names it, and Sam is a member there”. The third answer is “staff: read and change tickets, upload files”.

Sam asks the obvious question: “Why three? Why not one check, ‘may Sam do this?’”

Kwame points at Camille’s name on the plan. Camille is staff at Larkfield, so Camille reads and answers Larkfield’s tickets. At Bramble Books, Camille only helps Ines, so they may read but not change anything. The same person gets two different answers to “What may they do?” The answer depends on the organization. So the API must ask “in which organization?” before “what may they do?” That is why the diagram numbers the questions in that order.

Tomás has a shortcut ready: “The browser knows which shop the person works for. Let it send the shop’s ID in a header.” Kwame shakes his head. A person can change anything that the browser sends. The API must find the organization in its own data. The request names an organization, and the API checks that the caller is a member there. You build that check in Module 4.

What the shared key answers, and what it cannot

Section titled “What the shared key answers, and what it cannot”
Question The start’s API Why it is not enough for two shops
Who is the caller? “Someone who has the key.” Every person and every program has the same key. A log cannot show who did what.
In which organization? No answer. Bramble’s tickets will sit next to Larkfield’s, and nothing keeps them apart.
What may they do? Everything the key opens. Omar may only read, but the key lets him write.

The key is not wrong. It is the right tool for a different question. The API course used it for one program that calls another: a service-to-service check. The question there is “Is this one of our programs?”, and a key answers that well. At the end of this course, the key still protects /v1/classify and /v1/history, which other programs call. Every route that a person uses moves to signed-in users.

People need more. Each person needs their own identity, a list of the organizations they belong to, and a role in each one. That costs real work, and this course does it one step at a time. You add an identity provider, which signs people in and issues tokens. You add token checks in the API, a membership table, and a check on every route. Then you write tests that try every person against every route.

In the next lesson, you build the data that answers the second and third questions: people, organizations, memberships and roles. Then Threat examples shows what goes wrong while the API still has only the key. It tries five made-up attempts against your own practice API, and all five get what they should not.

Common mistake

docker compose up: port is already allocated

Symptom: docker compose up -d db azurite prints this, and db does not start:

Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint ticket-api-db-1 (…): Bind for 127.0.0.1:5432 failed: port is already allocated

Cause: another container already uses port 5432 on your computer. Usually it is the database of another project folder, for example from another course.

Diagnose: run docker ps. Look for a container with 127.0.0.1:5432->5432/tcp. The first part of its name is the name of its project folder.

Fix: in that folder, run docker compose down. Its data stays in its volume. Then run docker compose up -d db azurite again in ticket-api.

Common mistake

Every request with the key gets 401

Symptom: curl -H "X-API-Key: local-dev-key" … answers 401 and A valid X-API-Key header is required.

Cause: the API runs with another key, or the header name is wrong. If the API runs with no key at all, a request without a key gets 200. Then the API’s terminal shows API_KEY is not set: /v1/classify accepts requests without a key.

Fix: stop the API with Ctrl+C, and start it again with API_KEY=local-dev-key fastapi dev (PowerShell: set $env:API_KEY first). Check the header name: X-API-Key, with a colon after it.

Reset. python -m scripts.load --reset deletes the help-desk data, with your new message, and loads it again. docker compose down --volumes deletes the whole database and the files. After it, run docker compose up -d db azurite and python -m scripts.load.

Check your understanding

Name the three questions that the API must answer for every request, and the technical name of each. Why does the API ask about the organization before it decides what the caller may do?

Show the answer

Who is the caller? That is authentication. In which organization? That is the tenant. What may they do there? That is authorization. The organization comes before the permission, because one person can have different roles in different organizations. Camille is staff at Larkfield and read-only at Bramble Books. So “may Camille add a message?” has no answer until the API knows the organization.

Check your understanding

Predict: Omar only reads tickets in his work. He sends POST /v1/tickets/T-30002/messages with X-API-Key: local-dev-key to the start's API. What does the API answer, and what does the database record about who wrote the message?

Show the answer

201 Created. The key is right, and the key is the only thing that the API checks. The message row says author: agent, and the log line says POST /v1/tickets/T-30002/messages 201. Neither one names Omar. The API cannot refuse him, because it cannot tell his request from Sam’s.

Check your understanding

Practical check: show that your project works. Which commands do you run, and what must they print?

Show the answer

python -m scripts.load --reset ends with the counts (customers 40, tickets 200, messages 331, attachments 43, documents 36, ai_runs 294, ticket_embeddings 200) and 43 files uploaded to the object storage. python -m pytest ends with 108 passed, 1 deselected. With API_KEY=local-dev-key fastapi dev running, curl http://127.0.0.1:8000/v1/tickets/T-30002 answers 401, and the same request with -H "X-API-Key: local-dev-key" answers with the ticket.

Case studies in this course

At the end of the course, you use what you learn on a realistic project, from the data to the recommendation. Complete at least one for the certificate; the others are optional.

Recap

  • Every request must answer three questions: who is the caller (authentication), in which organization (the tenant), and what may they do there (authorization).
  • The API asks about the organization before the permission, because a person’s role depends on the organization.
  • The start’s shared key says only “the caller has the key”. Grace, Sam and Omar send the same bytes, and the API records author: agent.
  • A key stays right for one program that calls another. People need their own identity, memberships and roles.
  • Your project ~/projects/ticket-api is the snapshot start: 108 passed, 1 deselected.

Next, you add Bramble Books, the people and their memberships to the database, and you write down who may do what.

Next: Users and organizations