feat(model,repo,permission): permissions repository and model
This commit is contained in:
145
model/permission.go
Normal file
145
model/permission.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"database/sql/driver"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Permissions int64
|
||||
|
||||
var (
|
||||
_ sql.Scanner = (*Permissions)(nil)
|
||||
_ driver.Value = Permissions(0)
|
||||
_ fmt.Stringer = Permissions(0)
|
||||
)
|
||||
|
||||
func (p Permissions) Has(perm ...Permissions) bool {
|
||||
// Bitwise AND to compare if p has a permission
|
||||
//
|
||||
// If for example, p is 0x0010 ("edit.accessibility") and perm is
|
||||
// 0x0001 ("read"): 0x0010 AND 0x0001 = 0x0000, which is not equal
|
||||
// to 0x0001, return false.
|
||||
//
|
||||
// If p is 0x0011 ("edit.accessibility" and "read") and perm is
|
||||
// 0x0001 ("read"): 0x0011 AND 0x0001 results in 0x0001, which
|
||||
// is equal to 0x0001 ("read").
|
||||
if len(perm) == 0 {
|
||||
return false
|
||||
}
|
||||
if len(perm) == 1 {
|
||||
return p&perm[0] == perm[0]
|
||||
}
|
||||
for _, pe := range perm {
|
||||
if p&pe != pe {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *Permissions) Add(perm ...Permissions) {
|
||||
if p == nil {
|
||||
t := Permissions(0)
|
||||
p = &t
|
||||
}
|
||||
// Bitwise OR to add permissions.
|
||||
//
|
||||
// If p is 0x0001 ("read") and pe is 0x0010 ("edit.accessibility"):
|
||||
// 0x0001 OR 0x0010 results in 0x0011, which means we added the "edit.accessibility" bit.
|
||||
for _, pe := range perm {
|
||||
*p = *p | pe
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Permissions) Remove(perm ...Permissions) {
|
||||
if p == nil {
|
||||
return
|
||||
}
|
||||
// Bitwise NOT AND
|
||||
//
|
||||
// If p is 0x0011 ("read" + "edit.accessibility"), and perm is 0x0010 ("edit.accessibility"):
|
||||
// we first convert perm to a bit-mask using NOT, so it becomes 0x1101; then we use AND to
|
||||
// remove the "edit.accessibility", since 0x0011 AND 0x1101 results in 0x0001 ("read").
|
||||
for _, pe := range perm {
|
||||
*p = *p & (^pe)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Permissions) Scan(src any) error {
|
||||
switch src := src.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case int64:
|
||||
*p = Permissions(src)
|
||||
case string:
|
||||
if strings.HasPrefix(src, "0x") {
|
||||
i, err := strconv.ParseInt(strings.TrimPrefix(src, "0x"), 2, 64)
|
||||
if err != nil {
|
||||
return errors.Join(errors.New("Scan: unable to scan binary Permissions"), err)
|
||||
}
|
||||
return p.Scan(i)
|
||||
}
|
||||
i, err := strconv.ParseInt(src, 10, 64)
|
||||
if err != nil {
|
||||
return errors.Join(errors.New("Scan: unable to scan base10 Permissions"), err)
|
||||
}
|
||||
return p.Scan(i)
|
||||
case []byte:
|
||||
return p.Scan(string(src))
|
||||
default:
|
||||
return fmt.Errorf("Scan: unable to scan type %T into Permissions", src)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p Permissions) Value() (driver.Value, error) {
|
||||
return int64(p), nil
|
||||
}
|
||||
|
||||
func (p Permissions) String() string {
|
||||
if p.Has(PermissionAuthor) {
|
||||
return "author"
|
||||
}
|
||||
|
||||
labels := []string{}
|
||||
for perm, l := range PermissionLabels {
|
||||
if p.Has(perm) {
|
||||
labels = append(labels, l)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(labels, ",")
|
||||
}
|
||||
|
||||
const (
|
||||
PermissionAuthor Permissions = 0x1111111111111111 // "author"
|
||||
PermissionAdminDelete Permissions = 0x1000000000000000 // "admin.delete" -----
|
||||
PermissionAdminAll Permissions = 0x0111110000000001 // "admin.all"
|
||||
PermissionAdminProject Permissions = 0x0100000000000000 // "admin.project"
|
||||
PermissionAdminMembers Permissions = 0x0010000000000000 // "admin.members"
|
||||
PermissionEditAll Permissions = 0x0000001111111111 // "edit.all" ---------
|
||||
PermissionEditPages Permissions = 0x0000000100000000 // "edit.pages"
|
||||
PermissionEditInteractions Permissions = 0x0000000010000000 // "edit.interactions"
|
||||
PermissionEditDialogs Permissions = 0x0000000000001000 // "edit.dialogs"
|
||||
PermissionEditTranslations Permissions = 0x0000000000000100 // "edit.translations"
|
||||
PermissionEditAccessibility Permissions = 0x0000000000000010 // "edit.accessibility"
|
||||
PermissionRead Permissions = 0x0000000000000001 // "read"
|
||||
)
|
||||
|
||||
var PermissionLabels = map[Permissions]string{
|
||||
PermissionAuthor: "author",
|
||||
PermissionAdminDelete: "admin.delete",
|
||||
PermissionAdminProject: "admin.project",
|
||||
PermissionAdminMembers: "admin.members",
|
||||
PermissionEditPages: "edit.pages",
|
||||
PermissionEditInteractions: "edit.interactions",
|
||||
PermissionEditDialogs: "edit.dialogs",
|
||||
PermissionEditTranslations: "edit.translations",
|
||||
PermissionEditAccessibility: "edit.accessibility",
|
||||
PermissionRead: "read",
|
||||
}
|
||||
283
repository/permission.go
Normal file
283
repository/permission.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"time"
|
||||
|
||||
"forge.capytal.company/capytalcode/project-comicverse/model"
|
||||
"forge.capytal.company/loreddev/x/tinyssert"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Permissions struct {
|
||||
baseRepostiory
|
||||
}
|
||||
|
||||
// Must be initiated after [User] and [Project]
|
||||
func NewPermissions(
|
||||
ctx context.Context,
|
||||
db *sql.DB,
|
||||
log *slog.Logger,
|
||||
assert tinyssert.Assertions,
|
||||
) (*Permissions, error) {
|
||||
b := newBaseRepostiory(ctx, db, log, assert)
|
||||
|
||||
tx, err := db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
q := fmt.Sprintf(`
|
||||
CREATE TABLE IF NOT EXISTS project_permissions (
|
||||
project_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
permissions_value INTEGER NOT NULL DEFAULT '0',
|
||||
_permissions_text TEXT NOT NULL DEFAULT '', -- For display purposes only, may not always be up-to-date
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
|
||||
PRIMARY KEY(project_id, user_id)
|
||||
FOREIGN KEY(project_id)
|
||||
REFERENCES projects (id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE RESTRICT,
|
||||
FOREIGN KEY(user_id)
|
||||
REFERENCES users (id)
|
||||
ON DELETE CASCADE
|
||||
ON UPDATE RESTRICT
|
||||
)
|
||||
`)
|
||||
|
||||
_, err = tx.ExecContext(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return nil, errors.Join(errors.New("unable to create project tables"), err)
|
||||
}
|
||||
|
||||
return &Permissions{baseRepostiory: b}, nil
|
||||
}
|
||||
|
||||
func (repo Permissions) Create(project, user uuid.UUID, permissions model.Permissions) error {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
|
||||
tx, err := repo.db.BeginTx(repo.ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Join(ErrDatabaseConn, err)
|
||||
}
|
||||
|
||||
q := `
|
||||
INSERT INTO project_permissions (project_id, user_id, permissions_value, _permissions_text, created_at, updated_at)
|
||||
VALUES (:project_id, :user_id, :permissions_value, :permissions_text, :created_at, :updated_at)
|
||||
`
|
||||
|
||||
now := time.Now()
|
||||
|
||||
log := repo.log.With(slog.String("project_id", project.String()),
|
||||
slog.String("user_id", user.String()),
|
||||
slog.String("permissions", fmt.Sprintf("%d", permissions)),
|
||||
slog.String("permissions_text", permissions.String()),
|
||||
slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Inserting new project permissions")
|
||||
|
||||
_, err = tx.ExecContext(repo.ctx, q,
|
||||
sql.Named("project_id", project),
|
||||
sql.Named("user_id", user),
|
||||
sql.Named("permissions_value", permissions),
|
||||
sql.Named("permissions_text", permissions.String()),
|
||||
sql.Named("created_at", now.Format(dateFormat)),
|
||||
sql.Named("updated_at", now.Format(dateFormat)),
|
||||
)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to insert project permissions", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to commit transaction", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrCommitQuery, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo Permissions) GetByID(project uuid.UUID, user uuid.UUID) (model.Permissions, error) {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.log)
|
||||
|
||||
q := `
|
||||
SELECT permissions_value FROM project_permissions
|
||||
WHERE project_id = :project_id
|
||||
AND user_id = :user_id
|
||||
`
|
||||
|
||||
log := repo.log.With(slog.String("projcet_id", project.String()),
|
||||
slog.String("user_id", user.String()),
|
||||
slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Getting by ID")
|
||||
|
||||
row := repo.db.QueryRowContext(repo.ctx, q,
|
||||
sql.Named("project_id", user),
|
||||
sql.Named("user_id", user))
|
||||
|
||||
var p model.Permissions
|
||||
if err := row.Scan(&p); err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to get permissions by ID", slog.String("error", err.Error()))
|
||||
return model.Permissions(0), errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// GetByUserID returns a project_id-to-permissions map containing all projects and permissions that said userID
|
||||
// has relation to.
|
||||
func (repo Permissions) GetByUserID(user uuid.UUID) (permissions map[uuid.UUID]model.Permissions, err error) {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.log)
|
||||
|
||||
// Begin tx so we don't read rows as they are being updated
|
||||
tx, err := repo.db.BeginTx(repo.ctx, nil)
|
||||
if err != nil {
|
||||
return nil, errors.Join(ErrDatabaseConn, err)
|
||||
}
|
||||
|
||||
q := `
|
||||
SELECT project_id, permissions_value FROM project_permissions
|
||||
WHERE user_id = :user_id
|
||||
`
|
||||
|
||||
log := repo.log.With(slog.String("user_id", user.String()),
|
||||
slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Getting by user ID")
|
||||
|
||||
rows, err := tx.QueryContext(repo.ctx, q, sql.Named("user_id", user))
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to get permissions by user ID", slog.String("error", err.Error()))
|
||||
return nil, errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
err = rows.Close()
|
||||
if err != nil {
|
||||
err = errors.Join(ErrCloseConn, err)
|
||||
}
|
||||
}()
|
||||
|
||||
ps := map[uuid.UUID]model.Permissions{}
|
||||
|
||||
for rows.Next() {
|
||||
var project uuid.UUID
|
||||
var permissions model.Permissions
|
||||
|
||||
err := rows.Scan(&project, &permissions)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to scan permissions of user id", slog.String("error", err.Error()))
|
||||
return nil, errors.Join(ErrInvalidOutput, err)
|
||||
}
|
||||
|
||||
ps[project] = permissions
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to commit transaction", slog.String("error", err.Error()))
|
||||
return nil, errors.Join(ErrCommitQuery, err)
|
||||
}
|
||||
|
||||
return ps, nil
|
||||
}
|
||||
|
||||
func (repo Permissions) Update(project, user uuid.UUID, permissions model.Permissions) error {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.log)
|
||||
|
||||
tx, err := repo.db.BeginTx(repo.ctx, nil)
|
||||
if err != nil {
|
||||
return errors.Join(ErrDatabaseConn, err)
|
||||
}
|
||||
|
||||
q := `
|
||||
UPDATE project_permissions
|
||||
SET permissions_value = :permissions_value
|
||||
_permissions_text = :permissions_text
|
||||
updated_at = :updated_at
|
||||
WHERE project_uuid = :project_uuid
|
||||
AND user_uuid = :user_uuid
|
||||
`
|
||||
|
||||
log := repo.log.With(slog.String("project_id", project.String()),
|
||||
slog.String("user_id", user.String()),
|
||||
slog.String("permissions", fmt.Sprintf("%d", permissions)),
|
||||
slog.String("permissions_text", permissions.String()),
|
||||
slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Updating project permissions")
|
||||
|
||||
now := time.Now()
|
||||
|
||||
_, err = tx.ExecContext(repo.ctx, q,
|
||||
sql.Named("permissions_value", permissions),
|
||||
sql.Named("permissions_text", permissions.String()),
|
||||
sql.Named("updated_at", now.Format(dateFormat)),
|
||||
sql.Named("project_id", project),
|
||||
sql.Named("user_id", user),
|
||||
)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to update project permissions", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to commit transaction", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrCommitQuery, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repo Permissions) Delete(project, user uuid.UUID) error {
|
||||
repo.assert.NotNil(repo.db)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
repo.assert.NotNil(repo.ctx)
|
||||
|
||||
tx, err := repo.db.BeginTx(repo.ctx, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
q := `
|
||||
DELETE FROM project_permissions
|
||||
WHERE project_id = :project_id
|
||||
AND user_id = :user_id
|
||||
`
|
||||
|
||||
log := repo.log.With(slog.String("project_id", project.String()),
|
||||
slog.String("user_id", user.String()),
|
||||
slog.String("query", q))
|
||||
log.DebugContext(repo.ctx, "Deleting project permissions")
|
||||
|
||||
_, err = tx.ExecContext(repo.ctx, q,
|
||||
sql.Named("project_id", project),
|
||||
sql.Named("user_id", user),
|
||||
)
|
||||
if err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to delete project permissions", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrExecuteQuery, err)
|
||||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
log.ErrorContext(repo.ctx, "Failed to commit transaction", slog.String("error", err.Error()))
|
||||
return errors.Join(ErrCommitQuery, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user