PurelyFunctional.tv Newsletter 429: CRUD Authorization
Issue 429 - June 01, 2021 · Archives · Subscribe
Clojure Tip 💡
CRUD Authorization
We've been building up a CRUD system on top of Ring, using the process of development of Ring itself. We've developed a paging extension. This week, we'll develop an authorization extension to let our handlers decide whether the current request is authorized.
We're developing these extensions for two reasons. One reason is that for standard functionality (that is, functionality that is very common across web applications), it is more efficient to standardize the keys and values for that functionality. Other libraries can rely on the same keys and can therefore interoperate. Ring does this with the session extension, for example.
The second reason is that it allows us to prove the limits of the CRUD abstraction's extensibility. If we can develop an extension, it means others can, too. If we can't, we can change the abstraction to make it more extensible. This week, our authorization extension will test our basic abstraction.
Authorization is tightly linked with, and often confused with, authentication. Authentication is the process of identifying who is making the request. It is common to use a username and password or a secret token to authenticate a request.
Now that we know who is making the request, we can check the authorization. What is this person allowed to do? That's authorization. Commonly, entities in our application are owned by a single person who can authorize other people to read or modify the entity.
What we need to do in our extension is provide:
- A way to represent the identity of an agent making the request.
- A way to signal that the agent is not identified but needs to be.
- A way to signal that the agent who is identified does not have the authority to do the current operation.
Since this is an extension, we only have to specify the new keys.
1. Identity
We need to add identity to the request. We don't know what form the
identity will take at this level of abstraction. We'll leave that up to
the authentication mechanism and the handler. It could be a String
representing a signed token, a UUID token to look up in the session
database, or even a username/password pair. It should be some proof of
the identity of the agent. Even though we're leaving the type
unspecified, we will say that it should go in the :identity
key. The
authorization extension should add that key. The handler will use it to
check the authority of the agent.
2. Error: Authorization Required
Let's imagine someone does a :read
operation on a publicly accessible
entity. We should allow that, even if they are not identified.
However, only the owner of that entity should be able to :merge
to it.
The handler should check the proof of identity under :identity
and see
if they're authorized. If there is no :identity
key, we should return
an :authorization-required
status.
3. Error: Not Authorized
Let's continue with the example from the previous section. If they do
have an :identity
key, we should check if they're authorized. If they
are, fine, we do the operation. But if they're not, we should return a
:not-authorized
status.
Security concern
There's a slight security hole now: If we return a :not-found
status
for entities that don't exist, but an :authorization-required
for
entities that do exist, we are letting people know the IDs of the
entities that they are not allowed to read. This presents more attack
surface. For example, someone could request every ID and see which ones
exist. We should mitigate that.
HTTP allows the server to respond 404 Not Found
instead of
401 Not Authorized
. This makes entities you don't have access to
appear the same as entities that don't exist. Unless you're authorized,
they are invisible.
Here's our rule:
If you would normally return
:authorization-required
or:not-authorized
, you may instead return:not-found
.
Note that it has to be done with care. It would be confusing if the
:read
request succeeds (because it's publicly visible) but then the
:merge
request fails with :not-found
. Of course it exists. We could
make the rule more specific:
If any request to entity A from agent B would result in a status other than
:not-found
, then all requests to entity A from agent B should result in a status other than:not-found
.
Once you let a requester know that it exists, don't hide it from them.
Okay, that's about it for CRUD. I learned a lot and hope you did, too. We used a method of specifying the data abstraction, an adapter that translates to/from that abstraction, handlers for the specific logic, and middleware for cross-cutting logic. Ring was the first to develop this method to a full extent, and we applied it to building a CRUD abstraction. The method should work for any kind of message-based protocol. Have you used something similar? How did it work out?
Course 📚
If you like the idea of Ring, check out the Web Development in Clojure course. We build a complete TODO list using handlers and middleware. And you learn some of the HTTP spec along the way.
This is one of my earliest courses. I tried to use animations to make the pieces of Ring more understandable. The notes and code are extensive. And I'm in the process of upgrading it. Of course, you'll get the new version when it comes out. But why not start now? Check it out.
Podcast episode🎙
This week on the podcast, I excerpt from and comment on the 1970 Turing Award Lecture by Jim Wilkinson. I learned a lot from this because I know very little about his field of study, numerical analysis.
Conference alert 🚨
StrangeLoop is happening Sept 30-Oct 2 this year. Tickets are available. (In person in St Louis)
:clojureD, the German Clojure conference, is coming up in a few days. (Online)
I'll be speakin g next week:
At DevDays Europe 2021, June 8, I'll be keynoting.
On Manning Publication's live stream, also June 8.
Book update ✍️
Grokking Simplicity is now available on Amazon! People have already been receiving their copies.
Please, if you like the book and/or believe in its mission of starting a discussion about the practice of FP in the industry, please leave a 5-star review. Reviews will help people learn whether the book is good before they buy. I'm up to four reviews so far.
You can also get a copy of Grokking Simplicity at Manning's site. There you can use the coupon TSSIMPLICITY for 50% off.
Quarantine update 😷
I know a lot of people are going through tougher times than I am. If you, for any reason, can't afford my courses, and you think the courses will help you, please hit reply and I will set you up. It's a small gesture I can make, but it might help.
I don't want to shame you or anybody that we should be using this time to work on our skills. The number one priority is your health and safety. I know I haven't been able to work very much, let alone learn some new skill. But if learning Clojure is important to you, and you can't afford it, just hit reply and I'll set you up. Keeping busy can keep us sane.
Stay healthy. Wash your hands. Wear a mask. Take care of loved ones.
Clojure Challenge 🤔
Last issue's challenge
- Formatted prime factorization - Submissions
This week's challenge
Headline Scroller
A local news station needs you to program their LED scroller screen. It will scroll headlines from the right side to the left side all day. Write a function that takes a headline (string) and the number of characters in the LED screen (integer) and returns a list of the different refreshes of the screen.
The first refresh should be a string of all spaces to clear the screen:
(nth (scroller "HEADLINE" 10) 0) ;=> " "
It should be a number of spaces equal to the width of the screen (in this case, 10).
The second refresh should have the first letter after 9 spaces:
(nth (scroller "HEADLINE" 10) 1) ;=> " H"
Then:
(nth (scroller "HEADLINE" 10) 2) ;=> " HE"
The full output:
(scroller "HEADLINE" 10) => (" "
" H"
" HE"
" HEA"
" HEAD"
" HEADL"
" HEADLI"
" HEADLIN"
" HEADLINE"
" HEADLINE "
"HEADLINE "
"EADLINE "
"ADLINE "
"DLINE "
"LINE "
"INE "
"NE "
"E "
" ")
The scroller should end with a string of 10 spaces again.
Thanks to this site for the problem idea, where it is rated Very Hard in Ruby. The problem has been modified.
Please submit your solutions as comments on this gist.
Rock on!
Eric Normand