Describe permissions

Authentication doesn't make sense if the APIs can't be protected by a mechanism takes use of it. Permissions usually take the account from the request and validate against permission rules.

Permission on the account itself

Let's implement a quite simple logic: a user can update and delete himself.

Create a file named schema.teo with this content.

schema.teo
connector {
  provider: .sqlite,
  url: "sqlite:./database.sqlite"
}
 
server {
  bind: ("0.0.0.0", 5053)
}
 
@identity.tokenIssuer($identity.jwt(expired: 3600 * 24 * 365))
@identity.jwtSecret(ENV["JWT_SECRET"]!)
@canMutate($when(.update | .delete, $match($account, [
    $case($cast(type User), $is($self)).asAny,
    $case($cast(type Null), $invalid.asAny)
]), otherwise: $valid.asAny))
model User {
  @id @autoIncrement @readonly
  id: Int
  @unique @onSet($if($presents, $isEmail)) @identity.id
  email: String
  @writeonly @onSet($presents.bcrypt.salt)
  @identity.checker($get(.value).presents.bcrypt.verify($self.get(.password).presents))
  password: String
  name: String
 
  include handler identity.signIn
  include handler identity.identity
}
 
middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]

Create a file named .env.

.env
JWT_SECRET=my_top_secret

Start the server.

Create a user and try to update the user. Send this JSON input to /User/create.

{
  "create": {
    "email": "john@gmail.com",
    "password": "Aa123456",
    "name": "John"
  }
}
Hide HTTP response
{
  "data": {
    "id": 1,
    "email": "john@gmail.com",
    "name": "John"
  }
}

Send this to /User/update.

{
  "where": {
    "id": 1
  },
  "update": {
    "name": "John Larrison"
  }
}
Hide HTTP response
{
  "error": {
    "type": "Unauthorized",
    "message": "input is invalid"
  }
}

You get a unauthorized error since you are not signed in. Sign in with the account. Send this to /User/signIn.

{
  "credentials": {
    "email": "john@gmail.com",
    "password": "Aa123456"
  }
}
Hide HTTP response
{
  "data": {
    "id": 1,
    "email": "john@gmail.com",
    "name": "John"
  },
  "meta": {
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpZCI6eyJpZCI6MX0sIm1vZGVsIjpbIlVzZXIiXSwiZXhwIjoxNzQyNjc4MTA5fQ.qCubhmJGJzXqq4U5HDaTqHtFYYuzm3akklggAKugQ7Y"
  }
}

Update the user again with header authorization set to Bearer #token#.

{
  "where": {
    "id": 1
  },
  "update": {
    "name": "John Larrison"
  }
}
Hide HTTP response
{
  "data": {
    "id": 1,
    "email": "john@gmail.com",
    "name": "John Larrison"
  }
}

This time, Teo detects the correct user identity and updates the user.

Permission on direct owned models

Update schema.teo with this content. This time we define a new model Post.

schema.teo
connector {
  provider: .sqlite,
  url: "sqlite:./database.sqlite"
}
 
server {
  bind: ("0.0.0.0", 5053)
}
 
@identity.tokenIssuer($identity.jwt(expired: 3600 * 24 * 365))
@identity.jwtSecret(ENV["JWT_SECRET"]!)
@canMutate($when(.update | .delete, $match($account, [
    $case($cast(type User), $is($self)).asAny,
    $case($cast(type Null), $invalid.asAny)
]), otherwise: $valid.asAny))
model User {
  @id @autoIncrement @readonly
  id: Int
  @unique @onSet($if($presents, $isEmail)) @identity.id
  email: String
  @writeonly @onSet($presents.bcrypt.salt)
  @identity.checker($get(.value).presents.bcrypt.verify($self.get(.password).presents))
  password: String
  name: String
  @relation(fields: .id, references: .userId)
  posts: Post[]
 
  include handler identity.signIn
  include handler identity.identity
}
 
@canMutate($match($account, [
    $case($cast(type User), $get(.id).eq($self.get(.userId).presents)).asAny,
    $case($cast(type Null), $invalid.asAny)
]))
model Post {
  @id @autoIncrement @readonly
  id: Int
  title: String
  content: String
  @foreignKey
  userId: Int
  @relation(fields: .userId, references: .id)
  user: User
}
 
middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]

Look at the new code, a post can be created, updated and deleted by the owner. Other accounts cannot mutate this post. Feel free to create posts with different users and try with the API requests.

Admin's permissions

It's common that a web platform has a lot of admins. Admins have different roles. Replace the schema with this.

schema.teo
connector {
  provider: .sqlite,
  url: "sqlite:./database.sqlite"
}
 
server {
  bind: ("0.0.0.0", 5053)
}
 
@identity.tokenIssuer($identity.jwt(expired: 3600 * 24 * 365))
@identity.jwtSecret(ENV["JWT_SECRET"]!)
@canMutate($when(.update | .delete, $match($account, [
    $case($cast(type User), $is($self)).asAny,
    $case($cast(type Admin), $valid.asAny),
    $case($cast(type Null), $invalid.asAny)
]), otherwise: $valid.asAny))
model User {
  @id @autoIncrement @readonly
  id: Int
  @unique @onSet($if($presents, $isEmail)) @identity.id
  email: String
  @writeonly @onSet($presents.bcrypt.salt)
  @identity.checker($get(.value).presents.bcrypt.verify($self.get(.password).presents))
  password: String
  name: String
  @relation(fields: .id, references: .userId)
  posts: Post[]
 
  include handler identity.signIn
  include handler identity.identity
}
 
@canMutate($match($account, [
    $case($cast(type User), $get(.id).eq($self.get(.userId).presents)).asAny,
    $case($cast(type Null), $invalid.asAny),
    $case($cast(type Admin), $when(.delete, $valid, otherwise: $invalid).asAny)
]))
model Post {
  @id @autoIncrement @readonly
  id: Int
  title: String
  content: String
  @foreignKey
  userId: Int
  @relation(fields: .userId, references: .id)
  user: User
}
 
enum Role {
    root
    normal
}
 
@identity.tokenIssuer($identity.jwt(expired: 3600 * 24 * 365))
@identity.jwtSecret(ENV["JWT_SECRET"]!)
@canMutate($match($account, [
    $case($cast(type Admin), $any([
        $get(.role).presents.eq(.root).asAny,
        $is($self).asAny
    ])).asAny,
]))
@canRead($account.cast(type Admin))
model Admin {
  @id @autoIncrement @readonly
  id: Int
  @unique @onSet($if($presents, $isEmail)) @identity.id
  email: String
  @writeonly @onSet($presents.bcrypt.salt)
  @identity.checker($get(.value).presents.bcrypt.verify($self.get(.password).presents))
  password: String
  role: Role
  name: String  
}
 
middlewares [identity.identityFromJwt(secret: ENV["JWT_SECRET"]!)]

Let's describe the rule that we just newly created.

  • Admin can mutate users
  • Admin can delete a post if the post contains illegal or offensive content
  • Only admin can read admins
  • Only the normal admin himself and root admins can mutate the admin record

Indirect permissions

We cannot simply describe the indirect permissions with the pipeline items. Let's write some programmatic code to do this. Let's say, there are many projects belongs to teams, and a user can join any team. A team has many users. Only team user can read or mutate the projects. Let's transform our thoughts to code like this.

Transform the current directory into a project.

Let's generate the entity from the schema for programming.

Now start the server with the app entrance. Try create, update and read with different user accounts.

Try to create a project on a team that the user doesn't belong to, will cause an error.

{
  "create": {
    "teamId": 2
  }
}
Hide HTTP response
{
  "error": {
    "type": "Unauthorized",
    "message": "user doesn't belong to this project's team"
  }
}

Summary

Like anything else in Teo, permissions are readable and clear, too. Developing protected APIs and defining permission rules are quite fast and easy.