Skip to content

Commit

Permalink
feat(dgw): add /jet/jrec/delete/<ID> endpoint (#834)
Browse files Browse the repository at this point in the history
This new endpoint is used for deleting recordings and allow the
service provider (e.g.: DVLS) to delete them according to its
policy.

Issue: DGW-96
  • Loading branch information
CBenoit authored May 2, 2024
1 parent 5008ca8 commit 0965f4e
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 21 deletions.
90 changes: 69 additions & 21 deletions devolutions-gateway/src/api/jrec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,23 @@ use anyhow::Context as _;
use axum::extract::ws::WebSocket;
use axum::extract::{self, ConnectInfo, Query, State, WebSocketUpgrade};
use axum::response::Response;
use axum::routing::get;
use axum::routing::{delete, get};
use axum::{Json, Router};
use devolutions_gateway_task::ShutdownSignal;
use hyper::StatusCode;
use tracing::Instrument as _;
use uuid::Uuid;

use crate::extract::{JrecToken, RecordingsReadScope};
use crate::http::HttpError;
use crate::extract::{JrecToken, RecordingDeleteScope, RecordingsReadScope};
use crate::http::{HttpError, HttpErrorBuilder};
use crate::recording::RecordingMessageSender;
use crate::token::{JrecTokenClaims, RecordingFileType, RecordingOperation};
use crate::DgwState;

pub fn make_router<S>(state: DgwState) -> Router<S> {
Router::new()
.route("/push/:id", get(jrec_push))
.route("/delete/:id", delete(jrec_delete))
.route("/list", get(list_recordings))
.route("/pull/:id/:filename", get(pull_recording_file))
.route("/play", get(get_player))
Expand Down Expand Up @@ -93,23 +95,50 @@ async fn handle_jrec_push(
}
}

fn list_uuid_dirs(dir_path: &Path) -> anyhow::Result<Vec<Uuid>> {
let read_dir = fs::read_dir(dir_path).context("couldn’t read directory")?;

let list = read_dir
.filter_map(|entry| {
let path = entry.ok()?.path();
if path.is_dir() {
let file_name = path.file_name()?.to_str()?;
let uuid = Uuid::parse_str(file_name).ok()?;
Some(uuid)
} else {
None
}
})
.collect();

Ok(list)
/// Lists all recordings stored on this instance
#[cfg_attr(feature = "openapi", utoipa::path(
delete,
operation_id = "DeleteRecording",
tag = "Jrec",
path = "/jet/jrec/delete/{id}",
responses(
(status = 200, description = "Recording matching the ID in the path has been deleted"),
(status = 400, description = "Bad request"),
(status = 401, description = "Invalid or missing authorization token"),
(status = 403, description = "Insufficient permissions"),
(status = 406, description = "The recording is still ongoing and can't be deleted yet"),
),
security(("scope_token" = ["gateway.recording.delete"])),
))]
async fn jrec_delete(
State(DgwState {
conf_handle,
recordings,
..
}): State<DgwState>,
_scope: RecordingDeleteScope,
extract::Path(session_id): extract::Path<Uuid>,
) -> Result<(), HttpError> {
let state = recordings
.get_state(session_id)
.await
.map_err(HttpError::internal().with_msg("failed recording listing").err())?;

if state.is_some() {
return Err(
HttpErrorBuilder::new(StatusCode::CONFLICT).msg("Attempted to delete a recording for an ongoing session")
);
}

let recording_path = conf_handle.get_conf().recording_path.join(session_id.to_string());

debug!(%recording_path, "Delete recording");

tokio::fs::remove_dir_all(recording_path)
.await
.map_err(HttpError::internal().with_msg("failed to delete recording").err())?;

Ok(())
}

/// Lists all recordings stored on this instance
Expand Down Expand Up @@ -140,7 +169,26 @@ pub(crate) async fn list_recordings(
Vec::new()
};

Ok(Json(dirs))
return Ok(Json(dirs));

fn list_uuid_dirs(dir_path: &Path) -> anyhow::Result<Vec<Uuid>> {
let read_dir = fs::read_dir(dir_path).context("couldn’t read directory")?;

let list = read_dir
.filter_map(|entry| {
let path = entry.ok()?.path();
if path.is_dir() {
let file_name = path.file_name()?.to_str()?;
let uuid = Uuid::parse_str(file_name).ok()?;
Some(uuid)
} else {
None
}
})
.collect();

Ok(list)
}
}

/// Retrieves a recording file for a given session
Expand Down
19 changes: 19 additions & 0 deletions devolutions-gateway/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,25 @@ where
}
}

#[derive(Clone, Copy)]
pub struct RecordingDeleteScope;

#[async_trait]
impl<S> FromRequestParts<S> for RecordingDeleteScope
where
S: Send + Sync,
{
type Rejection = HttpError;

async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
match ScopeToken::from_request_parts(parts, state).await?.0.scope {
AccessScope::Wildcard => Ok(Self),
AccessScope::RecordingDelete => Ok(Self),
_ => Err(HttpError::forbidden().msg("invalid scope for route")),
}
}
}

#[derive(Clone, Copy)]
pub struct RecordingsReadScope;

Expand Down
2 changes: 2 additions & 0 deletions devolutions-gateway/src/token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,8 @@ pub enum AccessScope {
ConfigWrite,
#[serde(rename = "gateway.heartbeat.read")]
HeartbeatRead,
#[serde(rename = "gateway.recording.delete")]
RecordingDelete,
#[serde(rename = "gateway.recordings.read")]
RecordingsRead,
}
Expand Down
1 change: 1 addition & 0 deletions utils/dotnet/Devolutions.Gateway.Utils/src/AccessScope.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ internal AccessScope(string value)
public static AccessScope GatewayJrlRead = new AccessScope("gateway.jrl.read");
public static AccessScope GatewayConfigWrite = new AccessScope("gateway.config.write");
public static AccessScope GatewayHeartbeatRead = new AccessScope("gateway.heartbeat.read");
public static AccessScope GatewayRecordingDelete = new AccessScope("gateway.recording.delete");
public static AccessScope GatewayRecordingsRead = new AccessScope("gateway.recordings.read");

public override string? ToString()
Expand Down

0 comments on commit 0965f4e

Please sign in to comment.