202 lines
7.3 KiB
Go
202 lines
7.3 KiB
Go
package postgres
|
|
|
|
import (
|
|
"database/sql"
|
|
|
|
"gardomatic.kleiax.de/internal/storage"
|
|
"github.com/lib/pq"
|
|
)
|
|
|
|
// RoleModel implements storage.RoleModelInterface for shared and garden-owned
|
|
// PostgreSQL roles.
|
|
type RoleModel struct{ DB *sql.DB }
|
|
|
|
// List returns shared roles in a scope.
|
|
func (m RoleModel) List(scope storage.RoleScope) ([]storage.Role, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
rows, err := m.DB.QueryContext(ctx, `SELECT r.name,r.scope,r.garden_id,r.label,r.system,r.created_at,r.updated_at,COALESCE(array_agg(rp.permission) FILTER (WHERE rp.permission IS NOT NULL), '{}') FROM roles r LEFT JOIN role_permissions rp ON rp.role_name=r.name WHERE r.scope=$1 AND r.garden_id IS NULL GROUP BY r.name ORDER BY r.system DESC,r.label,r.name`, scope)
|
|
return scanRoles(rows, err)
|
|
}
|
|
|
|
// ListForGarden returns shared and custom roles available in a garden.
|
|
func (m RoleModel) ListForGarden(gardenID int) ([]storage.Role, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
rows, err := m.DB.QueryContext(ctx, `SELECT r.name,r.scope,r.garden_id,r.label,r.system,r.created_at,r.updated_at,COALESCE(array_agg(rp.permission) FILTER (WHERE rp.permission IS NOT NULL), '{}') FROM roles r LEFT JOIN role_permissions rp ON rp.role_name=r.name WHERE r.scope='garden' AND (r.garden_id IS NULL OR r.garden_id=$1) GROUP BY r.name ORDER BY r.system DESC,r.label,r.name`, gardenID)
|
|
return scanRoles(rows, err)
|
|
}
|
|
|
|
func scanRoles(rows *sql.Rows, err error) ([]storage.Role, error) {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
roles := []storage.Role{}
|
|
for rows.Next() {
|
|
var role storage.Role
|
|
var permissions pq.StringArray
|
|
if err := rows.Scan(&role.Name, &role.Scope, &role.GardenID, &role.Label, &role.System, &role.CreatedAt, &role.UpdatedAt, &permissions); err != nil {
|
|
return nil, err
|
|
}
|
|
role.Permissions = []string(permissions)
|
|
roles = append(roles, role)
|
|
}
|
|
return roles, rows.Err()
|
|
}
|
|
|
|
// Get returns a shared role by name.
|
|
func (m RoleModel) Get(name string) (storage.Role, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
var role storage.Role
|
|
var permissions pq.StringArray
|
|
err := m.DB.QueryRowContext(ctx, `SELECT r.name,r.scope,r.garden_id,r.label,r.system,r.created_at,r.updated_at,COALESCE(array_agg(rp.permission) FILTER (WHERE rp.permission IS NOT NULL), '{}') FROM roles r LEFT JOIN role_permissions rp ON rp.role_name=r.name WHERE r.name=$1 GROUP BY r.name`, name).Scan(&role.Name, &role.Scope, &role.GardenID, &role.Label, &role.System, &role.CreatedAt, &role.UpdatedAt, &permissions)
|
|
if err != nil {
|
|
return storage.Role{}, recordError(err)
|
|
}
|
|
role.Permissions = []string(permissions)
|
|
return role, nil
|
|
}
|
|
|
|
// GetForGarden returns a shared or custom role available in a garden.
|
|
func (m RoleModel) GetForGarden(gardenID int, name string) (storage.Role, error) {
|
|
role, err := m.Get(name)
|
|
if err != nil {
|
|
return storage.Role{}, err
|
|
}
|
|
if role.Scope != storage.RoleScopeGarden || role.GardenID != nil && *role.GardenID != gardenID {
|
|
return storage.Role{}, storage.ErrRecordNotFound
|
|
}
|
|
return role, nil
|
|
}
|
|
|
|
// Create adds a role and its base permissions atomically.
|
|
func (m RoleModel) Create(role storage.Role) (storage.Role, error) {
|
|
if role.Scope != storage.RoleScopeApplication && role.Scope != storage.RoleScopeGarden {
|
|
return storage.Role{}, storage.ErrConflict
|
|
}
|
|
if role.Scope == storage.RoleScopeApplication && role.GardenID != nil {
|
|
return storage.Role{}, storage.ErrConflict
|
|
}
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
tx, err := m.DB.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return role, err
|
|
}
|
|
defer tx.Rollback()
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO roles(name,scope,garden_id,label) VALUES($1,$2,$3,$4)`, role.Name, role.Scope, role.GardenID, role.Label); err != nil {
|
|
return role, recordError(err)
|
|
}
|
|
for _, p := range role.Permissions {
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO role_permissions(role_name,permission) VALUES($1,$2)`, role.Name, p); err != nil {
|
|
return role, err
|
|
}
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return role, err
|
|
}
|
|
return m.Get(role.Name)
|
|
}
|
|
|
|
// Update replaces a role's label and base permissions atomically.
|
|
func (m RoleModel) Update(role storage.Role) (storage.Role, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
tx, err := m.DB.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return role, err
|
|
}
|
|
defer tx.Rollback()
|
|
result, err := tx.ExecContext(ctx, `UPDATE roles SET label=$1,updated_at=CURRENT_TIMESTAMP WHERE name=$2`, role.Label, role.Name)
|
|
if err != nil {
|
|
return role, err
|
|
}
|
|
if count, countErr := result.RowsAffected(); countErr != nil {
|
|
return role, countErr
|
|
} else if count == 0 {
|
|
return storage.Role{}, storage.ErrRecordNotFound
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `DELETE FROM role_permissions WHERE role_name=$1`, role.Name); err != nil {
|
|
return role, err
|
|
}
|
|
for _, p := range role.Permissions {
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO role_permissions(role_name,permission) VALUES($1,$2)`, role.Name, p); err != nil {
|
|
return role, err
|
|
}
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return role, err
|
|
}
|
|
return m.Get(role.Name)
|
|
}
|
|
|
|
// Delete removes a non-system shared role that is not assigned.
|
|
func (m RoleModel) Delete(name string) error {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
result, err := m.DB.ExecContext(ctx, `DELETE FROM roles WHERE name=$1 AND system=false`, name)
|
|
if err != nil {
|
|
return recordError(err)
|
|
}
|
|
count, err := result.RowsAffected()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if count == 0 {
|
|
return storage.ErrConflict
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListGardenOverrides lists explicit permission grants and revocations in a garden.
|
|
func (m RoleModel) ListGardenOverrides(gardenID int) ([]storage.GardenRolePermissionOverride, error) {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
rows, err := m.DB.QueryContext(ctx, `SELECT garden_id,role_name,permission,granted FROM garden_role_permission_overrides WHERE garden_id=$1 ORDER BY role_name,permission`, gardenID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
result := []storage.GardenRolePermissionOverride{}
|
|
for rows.Next() {
|
|
var o storage.GardenRolePermissionOverride
|
|
if err := rows.Scan(&o.GardenID, &o.RoleName, &o.Permission, &o.Granted); err != nil {
|
|
return nil, err
|
|
}
|
|
result = append(result, o)
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// ReplaceGardenOverrides atomically replaces all permission overrides for a
|
|
// role in one garden.
|
|
func (m RoleModel) ReplaceGardenOverrides(gardenID int, roleName string, overrides []storage.GardenRolePermissionOverride) error {
|
|
ctx, cancel := contextWithTimeout()
|
|
defer cancel()
|
|
tx, err := m.DB.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
var exists bool
|
|
if err = tx.QueryRowContext(ctx, `SELECT EXISTS (SELECT 1 FROM roles WHERE name=$1 AND scope='garden' AND (garden_id IS NULL OR garden_id=$2))`, roleName, gardenID).Scan(&exists); err != nil {
|
|
return err
|
|
}
|
|
if !exists {
|
|
return storage.ErrRecordNotFound
|
|
}
|
|
if _, err = tx.ExecContext(ctx, `DELETE FROM garden_role_permission_overrides WHERE garden_id=$1 AND role_name=$2`, gardenID, roleName); err != nil {
|
|
return err
|
|
}
|
|
for _, o := range overrides {
|
|
if _, err = tx.ExecContext(ctx, `INSERT INTO garden_role_permission_overrides(garden_id,role_name,permission,granted) VALUES($1,$2,$3,$4)`, gardenID, roleName, o.Permission, o.Granted); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return tx.Commit()
|
|
}
|
|
|
|
var _ = sql.ErrNoRows
|