0%

Chapter 6

Who May Do What, and How Often

Logged In Is Not Allowed

Chapter 5 made the server sure who is asking. Being sure who someone is says nothing yet about what they may touch. This lesson changes no code. It separates two questions that the server has so far answered as if they were one.

Two questions

Authentication asks “who are you?”. A credential that verifies answers it: a password, a , a signed token. When that fails, the answer is 401.

Authorization asks “may you do this, to this?”. A rule answers it, and the rule needs to know the caller, the action, and the . When that fails, the answer is 403, or sometimes 404.

with Python drew the line between 401 and 403 with one coarse rule: may this token write at all. A request passes two gates, in this order:

  1. “Who are you?” The server reads the token and checks its tag. No token, or one that does not verify: 401, and the request stops here.

  2. “May you do this, to this?” The server looks at the caller, the , and the book. No rule allows it: 403 or 404, and the request stops here.

Only a request that passed both gates reaches the code that reads or changes a book.

Today the second gate looks at the caller and the method. It never looks at the book.

What is new in this chapter’s project

The project you open in the next lesson is your server from Chapter 5 with three changes, none of which you need to write.

  • GET /about is a new, public description of the service. It keeps the ETag and max-age behavior you built in HTTP with Python, on a route everyone may read.

  • The access decisions of the book routes now sit in two , collection_refusal for /books and item_refusal for /books/{id}. They decide exactly as your Chapter 5 code did.

  • handle is now two lines that call a new function, route, which holds all the routing. And request["user"] records who a request proved to be. Lesson 5 uses both.

Two facts about the sample users change too. ben may now write. And there is a third user, cleo, with the test password paper lantern, who may only read.

Read the decision

This is the supplied item_refusal. It returns None when the request may go on, or the response that refuses it. claims is the payload of a verified token, or None.

def item_refusal(method, claims, book):
    """Return None when this method may go on for this book, or the refusal. Lesson 2 rewrites this."""
    if method == "PUT" or method == "DELETE":
        if claims is None:
            return unauthorized("Bearer")
        if not claims["can_write"]:
            return json_error(403, "this token may read the list but not change it")
    if book is None:
        return json_error(404, "no book has that id")
    if method == "GET" or method == "HEAD" or method == "PUT" or method == "DELETE":
        return None
    return not_allowed("GET, HEAD, PUT, DELETE")

Ask of each which question it answers. claims is None is the first question. claims["can_write"] is a coarse form of the second: may this account write anything. No line asks whose book it is, and none could, because a book has no owner.

Look at the shape as well. The function what to refuse, and then lets through what is left.

Here is what that gives, for requests sent to the server as it is. Both sample books are meant to be ana’s.

RequestSent byAnswer nowRight?
DELETE /books/1nobody, no token401yes
POST /bookscleo, who may only read403yes
DELETE /books/1ben, with his own valid token204, and the book is goneno
PUT /books/2ben200, and the book is replacedno
GET /books/1ben200, with ana’s bookno
GET /booksnobody, no token200, with every bookno

ben has a valid token and may write. On the server as it is, he sends DELETE /books/1, and book 1 is ana's. What does item_refusal return?

A stranger with no token sends GET /books to the server as it is. What comes back?

The rule that is missing

The old rule looks only at the token. The missing rule looks at the token’s user and at the book. Run both for ben and ana’s book:

Try it

It prints:

old rule lets ben delete ana's book: True
owner rule lets ben delete ana's book: False
owner rule lets ana delete it: True

A rule that names the object is called object-level authorization. Leaving it out is an easy fault to miss, because everything still works for the honest user: ana only ever asks for her own books, and her requests succeed either way.

The owner rule above checks user is not None and book is not None before it compares anything. The next question shows why.

What does print({}.get("owner") == None) print?

403 or 404 for a book that is not yours

When ben asks for ana’s book, a 403 would be an honest answer. It would also say: that book exists, and it is not yours. Book ids here count 1, 2, 3. With that sentence a stranger can count upward and map which ids exist across every reader.

So this service will answer 404, the same answer it gives for an id that no book has. That is a choice of this service, and other services choose 403. The price is that a 404 tells an honest client less about its mistake.

The 403 you built in HTTP with Python stays for the account that may not write at all. That answer does not depend on any book, so it gives nothing away about one.

An id is not a secret, and an id that is hard to guess does not replace the check.

cleo's token verifies, and her account may only read. She sends POST /books. Which question fails, and with which status?

The next lesson runs ben’s request against your server, and then you write the rule.