Files
kleiax 904d14b64c
CI / test (push) Canceled after 0s
Initial commit
2026-09-12 22:22:17 +02:00

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