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