new file: CHANGELOG.md
modified: README.md modified: support-provisioning-portal/assets/portal.css modified: support-provisioning-portal/assets/portal.js modified: support-provisioning-portal/includes/class-spp-activator.php modified: support-provisioning-portal/includes/class-spp-admin-page.php modified: support-provisioning-portal/includes/class-spp-http-proxmox-client.php modified: support-provisioning-portal/includes/class-spp-mock-proxmox-client.php new file: support-provisioning-portal/includes/class-spp-permissions.php modified: support-provisioning-portal/includes/class-spp-plugin.php modified: support-provisioning-portal/includes/class-spp-repository.php modified: support-provisioning-portal/includes/class-spp-rest-controller.php modified: support-provisioning-portal/includes/class-spp-shortcode.php modified: support-provisioning-portal/includes/interface-spp-proxmox-client.php modified: support-provisioning-portal/support-provisioning-portal.php
This commit is contained in:
@@ -11,7 +11,8 @@ final class SPP_REST_Controller
|
||||
public function __construct(
|
||||
private SPP_Repository $repository,
|
||||
private SPP_Proxmox_Client $proxmox,
|
||||
private SPP_Expiration_Service $expiration_service
|
||||
private SPP_Expiration_Service $expiration_service,
|
||||
private SPP_Permissions $permissions
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -43,7 +44,7 @@ final class SPP_REST_Controller
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'create_deployment'],
|
||||
'permission_callback' => [$this, 'can_mutate'],
|
||||
'permission_callback' => [$this, 'can_create_deployments'],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -56,43 +57,97 @@ final class SPP_REST_Controller
|
||||
[
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => [$this, 'delete_deployment'],
|
||||
'permission_callback' => [$this, 'can_mutate'],
|
||||
'permission_callback' => [$this, 'can_delete_deployments'],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/shares', [
|
||||
[
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => [$this, 'list_deployment_shares'],
|
||||
'permission_callback' => [$this, 'can_read'],
|
||||
],
|
||||
[
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'share_deployment'],
|
||||
'permission_callback' => [$this, 'can_read'],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/shares/(?P<user_id>\d+)', [
|
||||
'methods' => WP_REST_Server::DELETABLE,
|
||||
'callback' => [$this, 'unshare_deployment'],
|
||||
'permission_callback' => [$this, 'can_read'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/start', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'start_deployment'],
|
||||
'permission_callback' => [$this, 'can_mutate'],
|
||||
'permission_callback' => [$this, 'can_start_deployments'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/stop', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'stop_deployment'],
|
||||
'permission_callback' => [$this, 'can_mutate'],
|
||||
'permission_callback' => [$this, 'can_stop_deployments'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/prolong', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'prolong_deployment'],
|
||||
'permission_callback' => [$this, 'can_mutate'],
|
||||
'permission_callback' => [$this, 'can_prolong_deployments'],
|
||||
]);
|
||||
|
||||
register_rest_route(self::NAMESPACE, '/deployments/(?P<id>\d+)/refresh-ips', [
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => [$this, 'refresh_deployment_ips'],
|
||||
'permission_callback' => [$this, 'can_mutate'],
|
||||
'permission_callback' => [$this, 'can_refresh_deployment_ips'],
|
||||
]);
|
||||
}
|
||||
|
||||
public function can_read(): bool
|
||||
{
|
||||
return is_user_logged_in() && current_user_can('read');
|
||||
return is_user_logged_in() && $this->permissions->current_user_has(SPP_Permissions::VIEW_PORTAL);
|
||||
}
|
||||
|
||||
public function can_mutate(): bool
|
||||
public function can_create_deployments(): bool
|
||||
{
|
||||
return is_user_logged_in() && current_user_can('edit_posts');
|
||||
return $this->can_use_portal_action(SPP_Permissions::CREATE_DEPLOYMENTS);
|
||||
}
|
||||
|
||||
public function can_start_deployments(): bool
|
||||
{
|
||||
return $this->can_use_portal_action(SPP_Permissions::START_DEPLOYMENTS);
|
||||
}
|
||||
|
||||
public function can_stop_deployments(): bool
|
||||
{
|
||||
return $this->can_use_portal_action(SPP_Permissions::STOP_DEPLOYMENTS);
|
||||
}
|
||||
|
||||
public function can_prolong_deployments(): bool
|
||||
{
|
||||
return $this->can_use_portal_action(SPP_Permissions::PROLONG_DEPLOYMENTS);
|
||||
}
|
||||
|
||||
public function can_refresh_deployment_ips(): bool
|
||||
{
|
||||
return $this->can_use_portal_action(SPP_Permissions::REFRESH_DEPLOYMENT_IPS);
|
||||
}
|
||||
|
||||
public function can_delete_deployments(): bool
|
||||
{
|
||||
return $this->can_use_portal_action(SPP_Permissions::DELETE_DEPLOYMENTS);
|
||||
}
|
||||
|
||||
private function can_use_portal_action(string $permission): bool
|
||||
{
|
||||
return $this->can_read() && $this->permissions->current_user_has($permission);
|
||||
}
|
||||
|
||||
private function can_manage_all_deployments(): bool
|
||||
{
|
||||
return $this->permissions->current_user_has(SPP_Permissions::MANAGE_ALL_DEPLOYMENTS);
|
||||
}
|
||||
|
||||
public function list_templates(): WP_REST_Response
|
||||
@@ -113,19 +168,26 @@ final class SPP_REST_Controller
|
||||
{
|
||||
$this->sync_expirations();
|
||||
|
||||
return rest_ensure_response($this->repository->deployments());
|
||||
return rest_ensure_response($this->repository->deployments(
|
||||
get_current_user_id(),
|
||||
$this->can_manage_all_deployments()
|
||||
));
|
||||
}
|
||||
|
||||
public function get_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
||||
{
|
||||
$this->sync_expirations();
|
||||
$deployment = $this->repository->deployment((int) $request['id']);
|
||||
$deployment = $this->repository->deployment_for_user(
|
||||
(int) $request['id'],
|
||||
get_current_user_id(),
|
||||
$this->can_manage_all_deployments()
|
||||
);
|
||||
|
||||
if ($deployment === null) {
|
||||
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
||||
}
|
||||
|
||||
return rest_ensure_response($deployment);
|
||||
return rest_ensure_response($this->deployment_response($deployment));
|
||||
}
|
||||
|
||||
public function create_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
||||
@@ -182,7 +244,13 @@ final class SPP_REST_Controller
|
||||
return new WP_Error('spp_proxmox_error', $error->getMessage(), ['status' => 502]);
|
||||
}
|
||||
|
||||
return new WP_REST_Response($deployment, 201);
|
||||
$deployment = $this->repository->deployment_for_user(
|
||||
(int) $deployment['id'],
|
||||
get_current_user_id(),
|
||||
$this->can_manage_all_deployments()
|
||||
);
|
||||
|
||||
return new WP_REST_Response($this->deployment_response($deployment), 201);
|
||||
}
|
||||
|
||||
private function validate_memory_quota(int $requested_memory_mb, int $actor_id): ?WP_Error
|
||||
@@ -233,9 +301,13 @@ final class SPP_REST_Controller
|
||||
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
||||
}
|
||||
|
||||
if (!$this->repository->user_can_access_deployment($id, get_current_user_id(), $this->can_manage_all_deployments())) {
|
||||
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
||||
}
|
||||
|
||||
$ttl_hours = (int) $request->get_param('ttlHours');
|
||||
$never_expire = (bool) $request->get_param('neverExpire');
|
||||
$deployment = $this->repository->deployment($id);
|
||||
$deployment = $this->repository->deployment_for_user($id, get_current_user_id(), $this->can_manage_all_deployments());
|
||||
|
||||
if ($deployment === null) {
|
||||
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
||||
@@ -252,13 +324,16 @@ final class SPP_REST_Controller
|
||||
}
|
||||
|
||||
if ($record['status'] === 'EXPIRED') {
|
||||
$quota_error = $this->validate_memory_quota((int) $deployment['memoryMb'], get_current_user_id());
|
||||
$quota_error = $this->validate_memory_quota((int) $deployment['memoryMb'], (int) $record['requested_by']);
|
||||
if ($quota_error instanceof WP_Error) {
|
||||
return $quota_error;
|
||||
}
|
||||
}
|
||||
|
||||
return rest_ensure_response($this->repository->prolong_deployment($id, $ttl_hours, get_current_user_id()));
|
||||
$this->repository->prolong_deployment($id, $ttl_hours, get_current_user_id());
|
||||
$deployment = $this->repository->deployment_for_user($id, get_current_user_id(), $this->can_manage_all_deployments());
|
||||
|
||||
return rest_ensure_response($this->deployment_response($deployment));
|
||||
}
|
||||
|
||||
public function refresh_deployment_ips(WP_REST_Request $request): WP_REST_Response|WP_Error
|
||||
@@ -271,12 +346,16 @@ final class SPP_REST_Controller
|
||||
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
||||
}
|
||||
|
||||
if (!$this->repository->user_can_access_deployment($id, get_current_user_id(), $this->can_manage_all_deployments())) {
|
||||
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
||||
}
|
||||
|
||||
if (empty($record['proxmox_vm_id'])) {
|
||||
return new WP_Error('spp_missing_vm_id', 'Deployment is missing a Proxmox VM id.', ['status' => 409]);
|
||||
}
|
||||
|
||||
try {
|
||||
$deployment = $this->repository->update_deployment_ips(
|
||||
$this->repository->update_deployment_ips(
|
||||
$id,
|
||||
$this->proxmox->get_ip_addresses((int) $record['proxmox_vm_id']),
|
||||
get_current_user_id()
|
||||
@@ -285,7 +364,69 @@ final class SPP_REST_Controller
|
||||
return new WP_Error('spp_proxmox_error', $error->getMessage(), ['status' => 502]);
|
||||
}
|
||||
|
||||
return rest_ensure_response($deployment);
|
||||
$deployment = $this->repository->deployment_for_user($id, get_current_user_id(), $this->can_manage_all_deployments());
|
||||
|
||||
return rest_ensure_response($this->deployment_response($deployment));
|
||||
}
|
||||
|
||||
public function list_deployment_shares(WP_REST_Request $request): WP_REST_Response|WP_Error
|
||||
{
|
||||
$this->sync_expirations();
|
||||
$id = (int) $request['id'];
|
||||
|
||||
if (!$this->user_can_share_deployment($id)) {
|
||||
return new WP_Error('spp_forbidden', 'Only the owner or a deployment manager can view shares.', ['status' => 403]);
|
||||
}
|
||||
|
||||
return rest_ensure_response($this->repository->deployment_shares($id));
|
||||
}
|
||||
|
||||
public function share_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
||||
{
|
||||
$this->sync_expirations();
|
||||
$id = (int) $request['id'];
|
||||
|
||||
if (!$this->user_can_share_deployment($id)) {
|
||||
return new WP_Error('spp_forbidden', 'Only the owner or a deployment manager can share this deployment.', ['status' => 403]);
|
||||
}
|
||||
|
||||
$identifier = sanitize_text_field((string) $request->get_param('user'));
|
||||
$target = $this->find_share_target_user($identifier);
|
||||
|
||||
if (!$target instanceof WP_User) {
|
||||
return new WP_Error('spp_user_not_found', 'User not found.', ['status' => 404]);
|
||||
}
|
||||
|
||||
if ((int) $target->ID === get_current_user_id()) {
|
||||
return new WP_Error('spp_invalid_share_target', 'You already have access to this deployment.', ['status' => 400]);
|
||||
}
|
||||
|
||||
$record = $this->repository->deployment_record($id);
|
||||
if ($record === null || (int) $record['requested_by'] === (int) $target->ID) {
|
||||
return new WP_Error('spp_invalid_share_target', 'The owner already has access to this deployment.', ['status' => 400]);
|
||||
}
|
||||
|
||||
if (!$this->permissions->user_has((int) $target->ID, SPP_Permissions::VIEW_PORTAL)) {
|
||||
return new WP_Error('spp_user_without_portal_access', 'That user does not have portal access yet.', ['status' => 400]);
|
||||
}
|
||||
|
||||
$this->repository->share_deployment($id, (int) $target->ID, get_current_user_id());
|
||||
|
||||
return rest_ensure_response($this->repository->deployment_shares($id));
|
||||
}
|
||||
|
||||
public function unshare_deployment(WP_REST_Request $request): WP_REST_Response|WP_Error
|
||||
{
|
||||
$this->sync_expirations();
|
||||
$id = (int) $request['id'];
|
||||
|
||||
if (!$this->user_can_share_deployment($id)) {
|
||||
return new WP_Error('spp_forbidden', 'Only the owner or a deployment manager can change shares.', ['status' => 403]);
|
||||
}
|
||||
|
||||
$this->repository->unshare_deployment($id, (int) $request['user_id'], get_current_user_id());
|
||||
|
||||
return rest_ensure_response($this->repository->deployment_shares($id));
|
||||
}
|
||||
|
||||
private function apply_lifecycle_action(int $id, string $status, string $audit_action, string $method): WP_REST_Response|WP_Error
|
||||
@@ -297,6 +438,14 @@ final class SPP_REST_Controller
|
||||
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
||||
}
|
||||
|
||||
if (!$this->repository->user_can_access_deployment($id, get_current_user_id(), $this->can_manage_all_deployments())) {
|
||||
return new WP_Error('spp_not_found', 'Deployment not found.', ['status' => 404]);
|
||||
}
|
||||
|
||||
if ($method === 'delete_vm' && !$this->user_can_delete_deployment($id)) {
|
||||
return new WP_Error('spp_forbidden', 'Only the owner or a deployment manager can delete this deployment.', ['status' => 403]);
|
||||
}
|
||||
|
||||
if ($method === 'start_vm' && $record['status'] === 'EXPIRED') {
|
||||
return new WP_Error(
|
||||
'spp_expired_deployment',
|
||||
@@ -312,7 +461,7 @@ final class SPP_REST_Controller
|
||||
try {
|
||||
$this->proxmox->{$method}((int) $record['proxmox_vm_id']);
|
||||
if ($method === 'start_vm') {
|
||||
$deployment = $this->repository->update_deployment_status_and_ips(
|
||||
$this->repository->update_deployment_status_and_ips(
|
||||
$id,
|
||||
$status,
|
||||
$this->safe_ip_addresses((int) $record['proxmox_vm_id']),
|
||||
@@ -320,13 +469,80 @@ final class SPP_REST_Controller
|
||||
get_current_user_id()
|
||||
);
|
||||
} else {
|
||||
$deployment = $this->repository->update_deployment_status($id, $status, $audit_action, get_current_user_id());
|
||||
$this->repository->update_deployment_status($id, $status, $audit_action, get_current_user_id());
|
||||
}
|
||||
} catch (Throwable $error) {
|
||||
return new WP_Error('spp_proxmox_error', $error->getMessage(), ['status' => 502]);
|
||||
}
|
||||
|
||||
return rest_ensure_response($deployment);
|
||||
if ($method === 'delete_vm') {
|
||||
return rest_ensure_response(['deleted' => true]);
|
||||
}
|
||||
|
||||
$deployment = $this->repository->deployment_for_user($id, get_current_user_id(), $this->can_manage_all_deployments());
|
||||
|
||||
return rest_ensure_response($this->deployment_response($deployment));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed>|null $deployment
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function deployment_response(?array $deployment): array
|
||||
{
|
||||
if ($deployment === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$id = (int) $deployment['id'];
|
||||
$deployment['canShare'] = $this->user_can_share_deployment($id);
|
||||
$deployment['canDelete'] = $this->user_can_delete_deployment($id);
|
||||
|
||||
if ($deployment['canShare']) {
|
||||
$deployment['shares'] = $this->repository->deployment_shares($id);
|
||||
}
|
||||
|
||||
return $deployment;
|
||||
}
|
||||
|
||||
private function user_can_share_deployment(int $deployment_id): bool
|
||||
{
|
||||
$record = $this->repository->deployment_record($deployment_id);
|
||||
if ($record === null || $record['status'] === 'DELETED') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->repository->user_owns_deployment($deployment_id, get_current_user_id())
|
||||
|| $this->can_manage_all_deployments();
|
||||
}
|
||||
|
||||
private function user_can_delete_deployment(int $deployment_id): bool
|
||||
{
|
||||
$record = $this->repository->deployment_record($deployment_id);
|
||||
if ($record === null || $record['status'] === 'DELETED') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->permissions->current_user_has(SPP_Permissions::DELETE_DEPLOYMENTS)
|
||||
&& (
|
||||
$this->repository->user_owns_deployment($deployment_id, get_current_user_id())
|
||||
|| $this->can_manage_all_deployments()
|
||||
);
|
||||
}
|
||||
|
||||
private function find_share_target_user(string $identifier): ?WP_User
|
||||
{
|
||||
if ($identifier === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_email($identifier)) {
|
||||
$user = get_user_by('email', $identifier);
|
||||
} else {
|
||||
$user = get_user_by('login', $identifier);
|
||||
}
|
||||
|
||||
return $user instanceof WP_User ? $user : null;
|
||||
}
|
||||
|
||||
private function sync_expirations(): void
|
||||
|
||||
Reference in New Issue
Block a user