Let's Go Further Permission-based authorization › Setting up the permissions model
Previous · Contents · Next
Chapter 16.3.

Setting up the permissions model

Next, let’s head to our internal/data package and add a PermissionModel to manage the interactions with our new tables. For now, the only thing we want to include in this model is a GetAllForUser() method to return all permission codes for a specific user. The idea is that we’ll be able to use this in our handlers and middleware like so:

// Return a slice of the permission codes for the user with ID = 1. This would return
// something like []string{"movies:read", "movies:write"}.
app.models.Permissions.GetAllForUser(1) 

Behind the scenes, the SQL statement that we need to fetch the permission codes for a specific user looks like this:

SELECT permissions.code
FROM permissions
INNER JOIN users_permissions ON users_permissions.permission_id = permissions.id
INNER JOIN users ON users_permissions.user_id = users.id
WHERE users.id = $1

In this query we are using the INNER JOIN clause to join our permissions table to our users_permissions table, and then using it again to join that to the users table. Then we use the WHERE clause to filter the result, leaving only rows which relate to a specific user ID.

Let’s go ahead and set up the new PermissionModel. First create a new internal/data/permissions.go file:

$ touch internal/data/permissions.go

And then add the following code:

File: internal/data/permissions.go
package data

import (
    "context"
    "database/sql"
    "time"
)

// Define a Permissions slice, which we will use to hold the permission codes (such as
// "movies:read" and "movies:write") for a single user.
type Permissions []string

// Add a helper method to check whether the Permissions slice contains a specific 
// permission code.
func (p Permissions) Include(code string) bool {
    return slices.Contains(p, code)
}

// Define the PermissionModel type.
type PermissionModel struct {
    DB *sql.DB
}

// The GetAllForUser() method returns all permission codes for a specific user in a 
// Permissions slice. The code in this method should feel very familiar --- it uses the
// standard pattern that we've already seen before for retrieving multiple data rows in 
// an SQL query.
func (m PermissionModel) GetAllForUser(userID int64) (Permissions, error) {
    query := `
        SELECT permissions.code
        FROM permissions
        INNER JOIN users_permissions ON users_permissions.permission_id = permissions.id
        INNER JOIN users ON users_permissions.user_id = users.id
        WHERE users.id = $1`

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    rows, err := m.DB.QueryContext(ctx, query, userID)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var permissions Permissions

    for rows.Next() {
        var permission string

        err := rows.Scan(&permission)
        if err != nil {
            return nil, err
        }

        permissions = append(permissions, permission)
    }
    if err = rows.Err(); err != nil {
        return nil, err
    }

    return permissions, nil
}

Then the last thing we need to do is add the PermissionModel to our parent Model struct, so it’s available to our handlers and middleware. Like so:

File: internal/data/models.go
package data

...

type Models struct {
    Movies      MovieModel
    Permissions PermissionModel // Add a new Permissions field.
    Tokens      TokenModel
    Users       UserModel
}

func NewModels(db *sql.DB) Models {
    return Models{
        Movies:      MovieModel{DB: db},
        Permissions: PermissionModel{DB: db}, // Initialize a new PermissionModel instance.
        Tokens:      TokenModel{DB: db},
        Users:       UserModel{DB: db},
    }
}