Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gateway): add API to trigger Devolutions Gateway update #890

Merged
merged 3 commits into from
Jun 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions devolutions-gateway/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ openapi = ["dep:utoipa"]
# In-house
transport = { path = "../crates/transport" }
jmux-proxy = { path = "../crates/jmux-proxy" }
devolutions-agent-shared = { path = "../crates/devolutions-agent-shared" }
devolutions-gateway-task = { path = "../crates/devolutions-gateway-task" }
devolutions-log = { path = "../crates/devolutions-log" }
ironrdp-pdu = { version = "0.1", git = "https://github.com/Devolutions/IronRDP", rev = "4844e77b7f65024d85ba74b1824013eda6eb32b2" }
Expand Down
4 changes: 3 additions & 1 deletion devolutions-gateway/src/api/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ pub mod net;
pub mod rdp;
pub mod session;
pub mod sessions;
pub mod update;
pub mod webapp;

pub fn make_router<S>(state: crate::DgwState) -> axum::Router<S> {
Expand All @@ -27,7 +28,8 @@ pub fn make_router<S>(state: crate::DgwState) -> axum::Router<S> {
.route("/jet/rdp", axum::routing::get(rdp::handler))
.nest("/jet/fwd", fwd::make_router(state.clone()))
.nest("/jet/webapp", webapp::make_router(state.clone()))
.nest("/jet/net", net::make_router(state.clone()));
.nest("/jet/net", net::make_router(state.clone()))
.route("/jet/update", axum::routing::post(update::trigger_update_check));

if state.conf_handle.get_conf().web_app.enabled {
router = router.route(
Expand Down
71 changes: 71 additions & 0 deletions devolutions-gateway/src/api/update.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
use axum::extract::Query;
use axum::Json;
use hyper::StatusCode;

use devolutions_agent_shared::{get_updater_file_path, ProductUpdateInfo, UpdateJson, VersionSpecification};

use crate::extract::UpdateScope;
use crate::http::{HttpError, HttpErrorBuilder};

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct UpdateQueryParam {
version: VersionSpecification,
}

#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
#[derive(Serialize)]
pub(crate) struct UpdateResponse {}

/// Triggers Devolutions Gateway update process.
///
/// This is done via updating `Agent/update.json` file, which is then read by Devolutions Agent
/// when changes are detected. If the version written to `update.json` is indeed higher than the
/// currently installed version, Devolutions Agent will proceed with the update process.
#[cfg_attr(feature = "openapi", utoipa::path(
post,
operation_id = "Update",
tag = "Update",
path = "/jet/update",
responses(
(status = 200, description = "Update request has been processed successfully", body = UpdateResponse),
(status = 400, description = "Bad request"),
(status = 401, description = "Invalid or missing authorization token"),
(status = 403, description = "Insufficient permissions"),
(status = 500, description = "Agent updater service is malfunctioning"),
(status = 503, description = "Agent updater service is unavailable"),
),
security(("scope_token" = ["gateway.update"])),
))]
pub(super) async fn trigger_update_check(
Query(query): Query<UpdateQueryParam>,
_scope: UpdateScope,
) -> Result<Json<UpdateResponse>, HttpError> {
let target_version = query.version;

let updater_file_path = get_updater_file_path();

if !updater_file_path.exists() {
return Err(
HttpErrorBuilder::new(StatusCode::SERVICE_UNAVAILABLE).msg("Agent updater service is not installed")
);
}

let update_json = UpdateJson {
gateway: Some(ProductUpdateInfo { target_version }),
};

let update_json = serde_json::to_string(&update_json).map_err(
HttpError::internal()
.with_msg("failed to serialize the update manifest")
.err(),
)?;

std::fs::write(updater_file_path, update_json).map_err(
HttpError::internal()
.with_msg("failed to write the new `update.json` manifest on disk")
.err(),
)?;

Ok(Json(UpdateResponse {}))
}
19 changes: 19 additions & 0 deletions devolutions-gateway/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,25 @@ where
}
}

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

#[async_trait]
impl<S> FromRequestParts<S> for UpdateScope
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::Update => Ok(Self),
_ => Err(HttpError::forbidden().msg("invalid scope for route")),
}
}
}

#[derive(Clone)]
pub struct WebAppToken(pub WebAppTokenClaims);

Expand Down
2 changes: 2 additions & 0 deletions devolutions-gateway/src/openapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use uuid::Uuid;
crate::api::jrec::pull_recording_file,
crate::api::webapp::sign_app_token,
crate::api::webapp::sign_session_token,
crate::api::update::trigger_update_check,
// crate::api::net::get_net_config,
),
components(schemas(
Expand All @@ -39,6 +40,7 @@ use uuid::Uuid;
crate::token::AccessScope,
crate::api::webapp::AppTokenSignRequest,
crate::api::webapp::AppTokenContentType,
crate::api::update::UpdateResponse,
// crate::api::net::NetworkInterface,
SessionTokenContentType,
SessionTokenSignRequest,
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 @@ -410,6 +410,8 @@ pub enum AccessScope {
RecordingDelete,
#[serde(rename = "gateway.recordings.read")]
RecordingsRead,
#[serde(rename = "gateway.update")]
Update,
}

#[derive(Clone, Deserialize)]
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 @@ -22,6 +22,7 @@ internal AccessScope(string value)
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 static AccessScope GatewayUpdate = new AccessScope("gateway.update");

public override string? ToString()
{
Expand Down