internal/model/permission.go
package model
import (
"context"
"database/sql"
"errors"
"strings"
"sync"
"github.com/piotrkowalczuk/charon"
"github.com/piotrkowalczuk/qtypes"
)
// Permission returns charon.Permission value that is concatenated
// using entity properties like subsystem, module and action.
func (pe *PermissionEntity) Permission() charon.Permission {
return charon.Permission(pe.Subsystem + ":" + pe.Module + ":" + pe.Action)
}
// PermissionProvider ...
type PermissionProvider interface {
Find(ctx context.Context, criteria *PermissionFindExpr) ([]*PermissionEntity, error)
FindOneByID(ctx context.Context, id int64) (entity *PermissionEntity, err error)
// FindByUserID retrieves all permissions for user represented by given id.
FindByUserID(ctx context.Context, userID int64) (entities []*PermissionEntity, err error)
// FindByGroupID retrieves all permissions for group represented by given id.
FindByGroupID(ctx context.Context, groupID int64) (entities []*PermissionEntity, err error)
Register(ctx context.Context, permissions charon.Permissions) (created, untouched, removed int64, err error)
Insert(ctx context.Context, entity *PermissionEntity) (*PermissionEntity, error)
InsertMissing(ctx context.Context, permissions charon.Permissions) (int64, error)
}
// PermissionRepository extends PermissionRepositoryBase
type PermissionRepository struct {
PermissionRepositoryBase
findByUserIDQuery string
}
// NewPermissionRepository ...
func NewPermissionRepository(dbPool *sql.DB) *PermissionRepository {
return &PermissionRepository{
PermissionRepositoryBase: PermissionRepositoryBase{
DB: dbPool,
Table: TablePermission,
Columns: TablePermissionColumns,
},
findByUserIDQuery: `SELECT DISTINCT ON (p.id)
` + columns(TablePermissionColumns, "p") + `
FROM ` + TableUserPermissions + ` AS up
LEFT JOIN ` + TablePermission + ` AS p
ON up.` + TableUserPermissionsColumnPermissionSubsystem + ` = p.` + TablePermissionColumnSubsystem + `
AND up.` + TableUserPermissionsColumnPermissionModule + ` = p.` + TablePermissionColumnModule + `
AND up.` + TableUserPermissionsColumnPermissionAction + ` = p.` + TablePermissionColumnAction + `
WHERE up.` + TableUserPermissionsColumnUserID + ` = $1
UNION
SELECT DISTINCT ON (p.id) ` + columns(TablePermissionColumns, "p") + `
FROM ` + TableUserGroups + ` AS ug
INNER JOIN ` + TableGroupPermissions + ` AS gp ON ug.` + TableUserGroupsColumnGroupID + ` = gp.` + TableGroupPermissionsColumnGroupID + `
LEFT JOIN ` + TablePermission + ` as p
ON gp.` + TableGroupPermissionsColumnPermissionSubsystem + ` = p.` + TablePermissionColumnSubsystem + `
AND gp.` + TableGroupPermissionsColumnPermissionModule + ` = p.` + TablePermissionColumnModule + `
AND gp.` + TableGroupPermissionsColumnPermissionAction + ` = p.` + TablePermissionColumnAction + `
AND gp.` + TableGroupPermissionsColumnGroupID + ` = ug.` + TableUserGroupsColumnGroupID + `
WHERE ug.` + TableUserGroupsColumnUserID + ` = $1
`,
}
}
// FindByUserID implements PermissionProvider interface.
func (pr *PermissionRepository) FindByUserID(ctx context.Context, userID int64) ([]*PermissionEntity, error) {
// TODO: does it work?
return pr.FindBy(ctx, pr.findByUserIDQuery, userID)
}
// FindByGroupID implements PermissionProvider interface.
func (pr *PermissionRepository) FindByGroupID(ctx context.Context, groupID int64) ([]*PermissionEntity, error) {
// TODO: does it work?
return pr.FindBy(ctx, `
SELECT DISTINCT ON (p.id)
`+columns(TablePermissionColumns, "p")+`
FROM `+pr.Table+` AS p
LEFT JOIN `+TableGroupPermissions+` AS gp
ON gp.permission_subsystem = p.subsystem
AND gp.permission_module = p.module
AND gp.permission_action = p.action
WHERE gp.group_id = $1
`, groupID)
}
// FindBy ...
func (pr *PermissionRepository) FindBy(ctx context.Context, query string, args ...interface{}) ([]*PermissionEntity, error) {
rows, err := pr.DB.QueryContext(ctx, query, args...)
if err != nil {
return nil, err
}
defer rows.Close()
permissions := []*PermissionEntity{}
for rows.Next() {
var p PermissionEntity
err = rows.Scan(
&p.Action,
&p.CreatedAt,
&p.ID,
&p.Module,
&p.Subsystem,
&p.UpdatedAt,
)
if err != nil {
return nil, err
}
permissions = append(permissions, &p)
}
if rows.Err() != nil {
return nil, rows.Err()
}
return permissions, nil
}
func (pr *PermissionRepository) InsertMissing(ctx context.Context, permissions charon.Permissions) (int64, error) {
var aff int64
for _, permission := range permissions {
subsystem, module, action := charon.Permission(permission).Split()
count, err := pr.Count(ctx, &PermissionCountExpr{
Where: &PermissionCriteria{
Subsystem: qtypes.EqualString(subsystem),
Module: qtypes.EqualString(module),
Action: qtypes.EqualString(action),
},
})
if err != nil {
return 0, err
}
if count < 1 {
_, err := pr.Insert(ctx, &PermissionEntity{
Subsystem: subsystem,
Module: module,
Action: action,
})
if err != nil {
return 0, err
}
aff++
}
}
return aff, nil
}
var (
ErrEmptySliceOfPermissions = errors.New("empty slice, permissions cannot be registered")
ErrEmptySubsystem = errors.New("subsystem name is empty string, permissions cannot be registered")
ErrorInconsistentSubsystem = errors.New("provided permissions do not belong to one subsystem, permissions cannot be registered")
)
// Register ...
func (pr *PermissionRepository) Register(ctx context.Context, permissions charon.Permissions) (created, unt, removed int64, err error) {
var (
tx *sql.Tx
insert, delete *sql.Stmt
rows *sql.Rows
res sql.Result
subsystem string
entities []*PermissionEntity
affected int64
)
if len(permissions) == 0 {
return 0, 0, 0, ErrEmptySliceOfPermissions
}
subsystem = permissions[0].Subsystem()
if subsystem == "" {
return 0, 0, 0, ErrEmptySubsystem
}
for _, p := range permissions {
if p.Subsystem() != subsystem {
return 0, 0, 0, ErrorInconsistentSubsystem
}
}
tx, err = pr.DB.BeginTx(ctx, nil)
if err != nil {
return
}
defer func() {
if err != nil {
tx.Rollback()
} else {
err = tx.Commit()
unt = untouched(int64(len(permissions)), created, removed)
}
}()
rows, err = tx.Query("SELECT "+strings.Join(TablePermissionColumns, ",")+" FROM "+pr.Table+" AS p WHERE p.subsystem = $1", subsystem)
if err != nil {
return
}
defer rows.Close()
entities = []*PermissionEntity{}
for rows.Next() {
var entity PermissionEntity
err = rows.Scan(
&entity.Action,
&entity.CreatedAt,
&entity.ID,
&entity.Module,
&entity.Subsystem,
&entity.UpdatedAt,
)
if err != nil {
return
}
entities = append(entities, &entity)
}
if rows.Err() != nil {
return 0, 0, 0, rows.Err()
}
insert, err = tx.Prepare("INSERT INTO " + pr.Table + " (subsystem, module, action) VALUES ($1, $2, $3)")
if err != nil {
return
}
MissingPermissionsLoop:
for _, p := range permissions {
for _, e := range entities {
if p == e.Permission() {
continue MissingPermissionsLoop
}
}
if res, err = insert.Exec(p.Split()); err != nil {
return
}
if affected, err = res.RowsAffected(); err != nil {
return
}
created += affected
}
delete, err = tx.Prepare("DELETE FROM " + pr.Table + " AS p WHERE p.id = $1")
if err != nil {
return
}
RedundantPermissionsLoop:
for _, e := range entities {
for _, p := range permissions {
if e.Permission() == p {
continue RedundantPermissionsLoop
}
}
if res, err = delete.Exec(e.ID); err != nil {
return
}
if affected, err = res.RowsAffected(); err != nil {
return
}
removed += affected
}
return
}
// PermissionRegistry is an interface that describes in memory storage that holds information
// about permissions that was registered by 3rd party services.
// Should be only used as a proxy for registration process to avoid multiple sql hits.
type PermissionRegistry interface {
// Exists returns true if given charon.Permission was already registered.
Exists(ctx context.Context, permission charon.Permission) (exists bool)
// Register checks if given collection is valid and
// calls PermissionProvider to store provided permissions
// in persistent way.
Register(ctx context.Context, permissions charon.Permissions) (created, untouched, removed int64, err error)
}
// PermissionReg ...
type PermissionReg struct {
sync.RWMutex
repository PermissionProvider
permissions map[charon.Permission]struct{}
}
// NewPermissionRegistry ...
func NewPermissionRegistry(r PermissionProvider) *PermissionReg {
return &PermissionReg{
repository: r,
permissions: make(map[charon.Permission]struct{}),
}
}
// Exists ...
func (pr *PermissionReg) Exists(_ context.Context, permission charon.Permission) (ok bool) {
pr.RLock()
defer pr.RUnlock()
_, ok = pr.permissions[permission]
return
}
// Register ...
func (pr *PermissionReg) Register(ctx context.Context, permissions charon.Permissions) (created, untouched, removed int64, err error) {
pr.Lock()
defer pr.Unlock()
nb := 0
for _, p := range permissions {
if _, ok := pr.permissions[p]; !ok {
pr.permissions[p] = struct{}{}
nb++
}
}
if nb > 0 {
return pr.repository.Register(ctx, permissions)
}
return 0, 0, 0, nil
}
// FindByTag ...
func (pr *PermissionRepository) FindByTag(ctx context.Context, userID int64) ([]*PermissionEntity, error) {
query := `
SELECT DISTINCT ON (p.id)
` + columns(TablePermissionColumns, "p") + `
FROM ` + pr.Table + ` AS p
LEFT JOIN ` + TableUserPermissions + ` AS up ON up.permission_id = p.id AND up.user_id = $1
LEFT JOIN ` + TableUserGroups + ` AS ug ON ug.user_id = $1
LEFT JOIN ` + TableGroupPermissions + ` AS gp ON gp.permission_id = p.id AND gp.group_id = ug.group_id
WHERE up.user_id = $1 OR ug.user_id = $1
`
rows, err := pr.DB.QueryContext(ctx, query, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var permissions []*PermissionEntity
for rows.Next() {
var p PermissionEntity
err = rows.Scan(
&p.Action,
&p.CreatedAt,
&p.ID,
&p.Module,
&p.Subsystem,
&p.UpdatedAt,
)
if err != nil {
return nil, err
}
permissions = append(permissions, &p)
}
if rows.Err() != nil {
return nil, rows.Err()
}
return permissions, nil
}