User Administration with the "adm.l" library

Search for a command to run...

No comments yet. Be the first to comment.
In this series, we will show how to create a basic web application in PicoLisp.
In the last post we discussed the theory of using the adm.l library to add a simple user login field. Today we will show how to do that in our app. Creating the Login Form The global variable *Login stores whether a user is logged into the session. ...
In our previous post, we introduced how to utilize Discrete Event Simulation (DES) in PicoLisp. Now, let's explore another application: a railroad model simulation that includes tracks and trains. The visualization is ASCII-based, so feel free to unl...

The Dining Philosophers

In this post, we will introduce the concept of co-routines and how they are handled in PicoLisp. This will be the foundation for the subsequent posts on Discrete Event Simulation and finally a couple of examples, including an ASCII model railway simu...

One of the many nice things of PicoLisp is that it comes with a built-in database. This makes it easy to collect and store our own data, for example via a mobile app. But not all data is best represented in tables and lists: sometimes a colorful litt...

In the last two posts, we have built a little app that shows the current location and displays the nearest ice cream shops: https://picolisp-explored.com/an-openstreetmap-app-written-in-picolisp-pt-1 https://picolisp-explored.com/an-openstreetmap-ap...

Typically, a web application also requires some kind of user administration: Who has access to the data, who can modify or delete? In order to not re-invent the wheel every time, we can use the pre-installed adm.l library. You can find it in the lib/ folder of your PicoLisp installation.
Passwords and user data are very sensitive topics, and I think it's important that the developer understands also the cryptographic basics. So let's take a few minutes to dive a little bit into the adm.l library.
First of all, we can see four functions related to passwords: passwd, salt, randpw, auth.
Generally, passwords should only be stored as hashes, never in cleartext. A hash is a "one-way" function, which means that it is easy to calculate the hash value of a value, but not the other way around. Why is this so important?
Hashing is the most basic protection against the case that the database is compromised and the data is obtained by someone else. If the hash is strong enough, it is computationally infeasible to reverse a hash back to the original string.
Nevertheless, it is well known that many users are still using extremely weak passwords (like "password123", "12345678", and so on). If the user has a weak password and the password hash gets leaked, it is likely that the attacker with brute-force methods (hashing a list of common passwords and comparing the results) or prefabricated hash dictionary, so-called rainbow tables.
Now in order to prevent this, we can add a salt to the hash calculation - a random string that is added to the hash calculation. Both hashes and salt are stored in plaintext in the database. Because of the hash value, it is close to impossible to use rainbow tables for deciphering the hashes.
In the adm.l library, the "salt" is created by the (salt) function which uses the shape defined in the global variable *Salt. In order to understand what the library is doing, let's open it in the REPL to make some tests.
$ pil +
: (load "@lib/adm.l")
-> T
: (pool "test.db")
-> T
Let's set *Salt to the value proposed in the comment section and run (salt):
: (setq *Salt (16 . "$6$@1$"))
-> (16 . "$6$@1$")
: (salt)
-> "$6$dfEjiLwaLKE44..A$"
We get the number 6 and a 16 characters string, each divided by $delimitors.
What did we do here?
The CAR of *Salt defines the length of the salt value, in this case 16 characters. The CDR has two parts, divided by the delimitor $: 6 and @1. This shape is defined in the crypt(3) function.
-6 defines the hashing algorithm, in case of 6 it's the SHA-512 hash function,
-@1 is replaced by a random 16-characters string in the salt function:
# <adm.l>
(de salt ()
(text (cdr *Salt) (randpw (car *Salt))) )
where randpw returns a random password of a defined length.
Any password can be hashed using the function passwd which takes a string as argument. Let's test it:
: (passwd "mySecretPassword")
-> "$6$BUZxYCJ7UMMD/Y59$Uh/AwZyuUNlZw9kKVoWbNflGZDO4TXAfVb3cmtPWRbvhhafjsYsjaIE3UuVWh.Alaupp84b543EmXQeAegkJ61"
We receive back three values, each delimited by the $symbol.
$6: SHA-512 algorithm,$BUZxYCJ7UMMD/Y59: the salt,
-$Uh/AwZyuUNlZw9kKVoWbNflGZDO4TXAfVb3cmtPWRbvhhafjsYsjaIE3UuVWh.Alaupp84b543EmXQeAegkJ61: the actual password hash.The (salt) function is called each time a password is hashed.
The adm.l defines a basic user model +User and role model `+Role:
### Role ###
(class +Role +Entity)
(rel nm (+Need +Key +String)) # Role name
(rel perm (+List +Symbol)) # Permission list
(rel usr (+List +Joint) role (+User)) # Associated users
...
### User ###
(class +User +Entity)
(rel nm (+Need +Key +String)) # User name
(rel pw (+Swap +String)) # Password
(rel role (+Joint) usr (+Role)) # User role
(rel nam (+String)) # Full Name
(rel tel (+String)) # Phone
(rel em (+String)) # EMail
The "name" property nm of the user model inherits from the classes +Need (name is required) and +Key (name is unique), which means there can't be two users with the same name.
Let's try to add a user to our database. To keep it simple, we only use password and name.
: (request '(+User) 'nm "TestUser" 'pw (passwd "abcd1234"))
-> {6}
: (commit)
-> T
As you can see, we only store the hash value of the password in the database, not the cleartext!
Now we can try to authenticate this user using the auth function, which takes name and plaintext password as arguments:
: (auth "TestUser" "abcd1234")
-> {6}
The function returns the correct database entry. How does it work? auth first finds the user from the database by their name. Then the provided password is hashed using the salt and algorithm of the stored password. if these are equal, the function returns the user object, otherwise NIL:
: (auth "TestUser" "wrongPassword")
-> NIL
login and logout functionsThe library also defines the functions login and logout. When a user logs in (for example with username and password), the global variable *Login is set to this object, and the process-ID and timestamp are printed to the terminal:
: (login "TestUser" "abcd1234")
6165 * 2021-11-13 12:49:59 TestUser
-> {6}
: *Login
{6}
The function logout sets *Login back to NIL.
: (logout)
6165 / 2021-11-13 13:05:35
-> NIL
: *Login
-> NIL
6165 is our current process id, that we can also check with *Pid:
: *Pid
6165
Not only the current process, but also all of its family members (i.e. all children of the current process, and all other children of the parent process) are informed about the login and logout using the function tell.
must and mayThe adm. library defines two methods for the permission management: must and may.
must is called at the beginning of a function and checks:
If any of these conditions is not fulfilled, the client receives a "403 NO PERMISSION" status code response.
may is defined within the code and returns NIL if the permission is not fulfilled.
In the next post, we will add a simple user administration with roles to our todo app example.
https://software-lab.de/doc/index.html
https://man7.org/linux/man-pages/man3/crypt.3.html