Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 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
262 changes: 248 additions & 14 deletions Cargo.lock

Large diffs are not rendered by default.

17 changes: 14 additions & 3 deletions crates/devolutions-pedm/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ anyhow = "1.0"
axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio", "query", "tracing", "tower-log", "form", "original-uri", "matched-path"] }
base16ct = { version = "0.2", features = ["std", "alloc"] }
base64 = "0.22"
chrono = "0.4"
digest = "0.10"
hyper = { version = "1.3", features = ["server"] }
hyper-util = { version = "0.1", features = ["tokio"] }
Expand All @@ -29,16 +28,28 @@ win-api-wrappers = { path = "../win-api-wrappers" }
devolutions-pedm-shared = { path = "../devolutions-pedm-shared", features = ["policy"]}
devolutions-gateway-task = { path = "../devolutions-gateway-task" }
devolutions-agent-shared = { path = "../devolutions-agent-shared" }
camino = { version = "1" }
camino = { version = "1", features = ["serde1"] }
async-trait = "0.1"
tracing = "0.1"
walkdir = "2.5"
aide = { version = "0.14", features = ["axum", "axum-extra", "axum-json", "axum-tokio"] }
tower-http = { version = "0.5", features = ["timeout"] }
parking_lot = "0.12"
cfg-if = "1.0"
uuid = "1"
dunce = "1.0"
tower = "0.5"
futures-util = "0.3"
toml = "0.8"
libsql = { version = "0.9", optional = true, features = [ "core", "stream"] }
tokio-postgres = { version = "0.7", optional = true }
bb8 = { version = "0.9.0", optional = true }
bb8-postgres = { version = "0.9.0", optional = true }


[features]
default = ["libsql", "postgres"]
libsql = ["dep:libsql"]
postgres = ["dep:tokio-postgres", "dep:bb8", "dep:bb8-postgres"]

[lints]
workspace = true
10 changes: 10 additions & 0 deletions crates/devolutions-pedm/config.example.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
db = "libsql"

[libsql]
path = "C:/ProgramData/Devolutions/Agent/pedm/pedm.sqlite"

[postgres]
host = "192.168.0.100"
dbname = "pedm"
user = "pedm"
password = "pedm"
20 changes: 20 additions & 0 deletions crates/devolutions-pedm/schema/libsql.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* In SQLite, we store time as integer with microsecond precision. This is the same precision used by TIMESTAMPTZ in Postgres. */

CREATE TABLE pedm_run (
id INTEGER PRIMARY KEY AUTOINCREMENT,
start_time INTEGER NOT NULL DEFAULT (CAST(strftime('%f', 'now') * 1000000 AS INTEGER)),
pipe_name TEXT NOT NULL
);

CREATE TABLE http_request (
id INTEGER PRIMARY KEY,
at INTEGER NOT NULL DEFAULT (CAST(strftime('%f', 'now') * 1000000 AS INTEGER)),
method TEXT NOT NULL,
path TEXT NOT NULL,
status_code INTEGER NOT NULL
);

CREATE TABLE elevate_tmp_request (
req_id INTEGER PRIMARY KEY,
seconds INTEGER NOT NULL
);
22 changes: 22 additions & 0 deletions crates/devolutions-pedm/schema/pg.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/* The startup of the server */
CREATE TABLE pedm_run
(
id int PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
start_time timestamptz NOT NULL DEFAULT NOW(),
pipe_name text NOT NULL
);

CREATE TABLE http_request
(
id integer PRIMARY KEY,
at timestamptz NOT NULL DEFAULT NOW(),
method text NOT NULL,
path text NOT NULL,
status_code smallint NOT NULL
);

CREATE TABLE elevate_tmp_request
(
req_id integer PRIMARY KEY, /* this is http_request but the http_request INSERT only executes in middleware after the response, so we don't use a FK */
seconds int NOT NULL
);
13 changes: 10 additions & 3 deletions crates/devolutions-pedm/src/api/elevate_session.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
use std::sync::Arc;

use aide::NoApi;
use axum::extract::State;
use axum::Extension;
use parking_lot::RwLock;
use tracing::info;

use crate::elevations;
use crate::error::Error;
use crate::{elevations, policy};
use crate::policy::Policy;

use super::NamedPipeConnectInfo;

pub(crate) async fn post_elevate_session(
pub(crate) async fn elevate_session(
Extension(named_pipe_info): Extension<NamedPipeConnectInfo>,
NoApi(State(policy)): NoApi<State<Arc<RwLock<Policy>>>>,
) -> Result<(), Error> {
let policy = policy::policy().read();
let policy = policy.read();

if let Some(profile) = policy.user_current_profile(&named_pipe_info.user) {
if !profile.elevation_settings.session.enabled {
Expand Down
91 changes: 56 additions & 35 deletions crates/devolutions-pedm/src/api/elevate_temporary.rs
Original file line number Diff line number Diff line change
@@ -1,64 +1,85 @@
use std::sync::Arc;
use std::time::Duration;

use aide::NoApi;
use axum::extract::State;
use axum::{Extension, Json};
use hyper::StatusCode;
use parking_lot::RwLock;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tracing::info;

use crate::error::Error;
use crate::{elevations, policy};
use crate::elevations;
use crate::policy::Policy;

use super::err::HandlerError;
use super::state::Db;
use super::NamedPipeConnectInfo;

#[derive(Deserialize, Serialize, JsonSchema, Debug)]
#[serde(rename_all = "PascalCase")]
pub(crate) struct ElevateTemporaryPayload {
/// The number of seconds to elevate the user for.
///
/// This must be between 1 and `i32::MAX`.
pub(crate) seconds: u64,
}

pub(crate) async fn post_elevate_temporary(
Extension(named_pipe_info): Extension<NamedPipeConnectInfo>,
/// Temporarily elevates the user's session.
pub(crate) async fn elevate_temporary(
Extension(req_id): Extension<i32>,
Extension(info): Extension<NamedPipeConnectInfo>,
NoApi(Db(db)): NoApi<Db>,
NoApi(State(policy)): NoApi<State<Arc<RwLock<Policy>>>>,
Json(payload): Json<ElevateTemporaryPayload>,
) -> Result<(), Error> {
let policy = policy::policy().read();
) -> Result<(), HandlerError> {
// validate input
fn invalid_secs_err() -> HandlerError {
HandlerError::new(
StatusCode::BAD_REQUEST,
Some("number of seconds must be between 1 and 2,147,483,647"),
)
}
let seconds = i32::try_from(payload.seconds).map_err(|_| invalid_secs_err())?;
if seconds < 1 {
return Err(invalid_secs_err());
}

db.insert_elevate_tmp_request(req_id, seconds).await?;

let profile = policy.user_current_profile(&named_pipe_info.user);
if profile.is_none() {
info!(user = ?named_pipe_info.user, "User tried to elevate temporarily, but wasn't assigned to profile");
return Err(Error::AccessDenied);
let policy = policy.read();
if policy.user_current_profile(&info.user).is_none() {
return Err(HandlerError::new(
StatusCode::FORBIDDEN,
Some("user not assigned to profile"),
));
}

let settings = policy
.user_current_profile(&named_pipe_info.user)
.user_current_profile(&info.user)
.map(|p| &p.elevation_settings.temporary)
.ok_or(Error::AccessDenied)?;
.ok_or_else(|| {
HandlerError::new(
StatusCode::FORBIDDEN,
Some("could not get temporary elevation configuration"),
)
})?;

if !settings.enabled {
info!(
user = ?named_pipe_info.user,
"User tried to elevate temporarily, but wasn't allowed",
);
return Err(Error::AccessDenied);
return Err(HandlerError::new(
StatusCode::FORBIDDEN,
Some("temporary elevation is not permitted"),
));
}

let req_duration = Duration::from_secs(payload.seconds);

if Duration::from_secs(settings.maximum_seconds) < req_duration {
info!(
user = ?named_pipe_info.user,
seconds = req_duration.as_secs(),
"User tried to elevate temporarily for too long"
);
return Err(Error::AccessDenied);
let duration = Duration::from_secs(payload.seconds);
if Duration::from_secs(settings.maximum_seconds) < duration {
return Err(HandlerError::new(
StatusCode::FORBIDDEN,
Some("requested duration exceeds maximum"),
));
}

info!(
user = ?named_pipe_info.user,
seconds = req_duration.as_secs(),
"Elevating user"
);

elevations::elevate_temporary(named_pipe_info.user, &req_duration);
elevations::elevate_temporary(info.user, &duration);

Ok(())
}
55 changes: 55 additions & 0 deletions crates/devolutions-pedm/src/api/err.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use aide::OperationOutput;
use axum::response::{IntoResponse, Response};
use axum::Json;
use hyper::StatusCode;

use crate::db::DbError;

/// An error type for route handlers.
#[derive(Debug)]
pub(crate) struct HandlerError(StatusCode, Option<String>);

impl HandlerError {
/// Creates a handler error.
///
/// The input message should start with a lowercase letter.
/// It will be capitalized in the response.
pub(crate) fn new(status_code: StatusCode, msg: Option<&str>) -> Self {
Self(
status_code,
msg.map(|s| {
// capitalize first letter
let mut t = s
.chars()
.next()
.expect("handler error messaged contained empty string")
.to_uppercase()
.to_string();
t.push_str(&s[1..]);
t
}),
)
}
}

impl IntoResponse for HandlerError {
fn into_response(self) -> Response {
(self.0, self.1.unwrap_or_default()).into_response()
}
}

// for Aide
impl OperationOutput for HandlerError {
type Inner = (StatusCode, Json<HandlerError>);
}

impl From<tokio_postgres::Error> for HandlerError {
fn from(e: tokio_postgres::Error) -> Self {
Self(StatusCode::INTERNAL_SERVER_ERROR, Some(e.to_string()))
}
}
impl From<DbError> for HandlerError {
fn from(e: DbError) -> Self {
Self(StatusCode::INTERNAL_SERVER_ERROR, Some(e.to_string()))
}
}
8 changes: 8 additions & 0 deletions crates/devolutions-pedm/src/api/launch.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
use std::path::{Path, PathBuf};
use std::sync::Arc;

use aide::NoApi;
use axum::extract::State;
use axum::{Extension, Json};
use parking_lot::RwLock;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use tracing::info;

use win_api_wrappers::identity::sid::Sid;
use win_api_wrappers::process::{Process, StartupInfo};
use win_api_wrappers::raw::Win32::Security::{WinLocalSystemSid, TOKEN_QUERY};
Expand All @@ -16,6 +21,7 @@ use win_api_wrappers::utils::{environment_block, expand_environment_path, Comman

use crate::elevator;
use crate::error::Error;
use crate::policy::Policy;

use super::NamedPipeConnectInfo;

Expand Down Expand Up @@ -81,6 +87,7 @@ fn win_canonicalize(path: &Path, token: Option<&Token>) -> Result<PathBuf, Error

pub(crate) async fn post_launch(
Extension(named_pipe_info): Extension<NamedPipeConnectInfo>,
NoApi(State(policy)): NoApi<State<Arc<RwLock<Policy>>>>,
Json(mut payload): Json<LaunchPayload>,
) -> Result<Json<LaunchResponse>, Error> {
payload.executable_path = payload
Expand Down Expand Up @@ -126,6 +133,7 @@ pub(crate) async fn post_launch(
startup_info.attribute_list = Some(Some(attributes.raw()));

let proc_info = elevator::try_start_elevated(
&policy,
&named_pipe_info.token,
parent_pid,
payload.executable_path.as_deref(),
Expand Down
16 changes: 0 additions & 16 deletions crates/devolutions-pedm/src/api/logs.rs

This file was deleted.

Loading