User Administration with the "adm.l" library

User Administration with the "adm.l" library


5 min read

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.

Password handling

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.

Defining the Salt

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.

Hashing a password

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 User Model

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

The login and logout functions

The 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

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

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 may

The adm. library defines two methods for the permission management: must and may.

must is called at the beginning of a function and checks:

  • if the address in the browser fits to the session,
  • if the user is logged in to the sessions,
  • if at least one of the permissions is set for the current user.

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.