Skip to content

Commit abc57f9

Browse files
authored
feat(pedm): SQL support (#1292)
1 parent 6e5a95c commit abc57f9

25 files changed

+1407
-419
lines changed

Cargo.lock

Lines changed: 286 additions & 53 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/devolutions-pedm/Cargo.toml

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ anyhow = "1.0"
1414
axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio", "query", "tracing", "tower-log", "form", "original-uri", "matched-path"] }
1515
base16ct = { version = "0.2", features = ["std", "alloc"] }
1616
base64 = "0.22"
17-
chrono = "0.4"
1817
digest = "0.10"
1918
hyper = { version = "1.3", features = ["server"] }
2019
hyper-util = { version = "0.1", features = ["tokio"] }
@@ -29,16 +28,26 @@ win-api-wrappers = { path = "../win-api-wrappers" }
2928
devolutions-pedm-shared = { path = "../devolutions-pedm-shared", features = ["policy"]}
3029
devolutions-gateway-task = { path = "../devolutions-gateway-task" }
3130
devolutions-agent-shared = { path = "../devolutions-agent-shared" }
32-
camino = { version = "1" }
31+
camino = { version = "1", features = ["serde1"] }
3332
async-trait = "0.1"
3433
tracing = "0.1"
35-
walkdir = "2.5"
3634
aide = { version = "0.14", features = ["axum", "axum-extra", "axum-json", "axum-tokio"] }
3735
tower-http = { version = "0.5", features = ["timeout"] }
3836
parking_lot = "0.12"
3937
cfg-if = "1.0"
4038
uuid = "1"
4139
dunce = "1.0"
40+
tower = "0.5"
41+
futures-util = "0.3"
42+
libsql = { version = "0.9", optional = true, features = [ "core", "stream"] }
43+
tokio-postgres = { version = "0.7", optional = true }
44+
bb8 = { version = "0.9.0", optional = true }
45+
bb8-postgres = { version = "0.9.0", optional = true }
46+
47+
[features]
48+
default = ["libsql"]
49+
libsql = ["dep:libsql"]
50+
postgres = ["dep:tokio-postgres", "dep:bb8", "dep:bb8-postgres"]
4251

4352
[lints]
4453
workspace = true
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"db": "libsql",
3+
"libsql": {
4+
"path": "C:/ProgramData/Devolutions/Agent/pedm/pedm.sqlite"
5+
},
6+
"postgres": {
7+
"host": "192.168.0.100",
8+
"dbname": "pedm",
9+
"user": "pedm",
10+
"password": "pedm"
11+
}
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* In SQLite, we store time as integer with microsecond precision. This is the same precision used by TIMESTAMPTZ in Postgres. */
2+
3+
CREATE TABLE pedm_run (
4+
id INTEGER PRIMARY KEY AUTOINCREMENT,
5+
start_time INTEGER NOT NULL DEFAULT (CAST(strftime('%f', 'now') * 1000000 AS INTEGER)),
6+
pipe_name TEXT NOT NULL
7+
);
8+
9+
CREATE TABLE http_request (
10+
id INTEGER PRIMARY KEY,
11+
at INTEGER NOT NULL DEFAULT (CAST(strftime('%f', 'now') * 1000000 AS INTEGER)),
12+
method TEXT NOT NULL,
13+
path TEXT NOT NULL,
14+
status_code INTEGER NOT NULL
15+
);
16+
17+
CREATE TABLE elevate_tmp_request (
18+
req_id INTEGER PRIMARY KEY,
19+
seconds INTEGER NOT NULL
20+
);

crates/devolutions-pedm/schema/pg.sql

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/* The startup of the server */
2+
CREATE TABLE pedm_run
3+
(
4+
id int PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
5+
start_time timestamptz NOT NULL DEFAULT NOW(),
6+
pipe_name text NOT NULL
7+
);
8+
9+
CREATE TABLE http_request
10+
(
11+
id integer PRIMARY KEY,
12+
at timestamptz NOT NULL DEFAULT NOW(),
13+
method text NOT NULL,
14+
path text NOT NULL,
15+
status_code smallint NOT NULL
16+
);
17+
18+
CREATE TABLE elevate_tmp_request
19+
(
20+
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 */
21+
seconds int NOT NULL
22+
);

crates/devolutions-pedm/src/api/elevate_session.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
1+
use std::sync::Arc;
2+
3+
use aide::NoApi;
4+
use axum::extract::State;
15
use axum::Extension;
6+
use parking_lot::RwLock;
27
use tracing::info;
38

9+
use crate::elevations;
410
use crate::error::Error;
5-
use crate::{elevations, policy};
11+
use crate::policy::Policy;
612

713
use super::NamedPipeConnectInfo;
814

9-
pub(crate) async fn post_elevate_session(
15+
pub(crate) async fn elevate_session(
1016
Extension(named_pipe_info): Extension<NamedPipeConnectInfo>,
17+
NoApi(State(policy)): NoApi<State<Arc<RwLock<Policy>>>>,
1118
) -> Result<(), Error> {
12-
let policy = policy::policy().read();
19+
let policy = policy.read();
1320

1421
if let Some(profile) = policy.user_current_profile(&named_pipe_info.user) {
1522
if !profile.elevation_settings.session.enabled {
Lines changed: 56 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,85 @@
1+
use std::sync::Arc;
12
use std::time::Duration;
23

4+
use aide::NoApi;
5+
use axum::extract::State;
36
use axum::{Extension, Json};
7+
use hyper::StatusCode;
8+
use parking_lot::RwLock;
49
use schemars::JsonSchema;
510
use serde::{Deserialize, Serialize};
6-
use tracing::info;
711

8-
use crate::error::Error;
9-
use crate::{elevations, policy};
12+
use crate::elevations;
13+
use crate::policy::Policy;
1014

15+
use super::err::HandlerError;
16+
use super::state::Db;
1117
use super::NamedPipeConnectInfo;
1218

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

19-
pub(crate) async fn post_elevate_temporary(
20-
Extension(named_pipe_info): Extension<NamedPipeConnectInfo>,
28+
/// Temporarily elevates the user's session.
29+
pub(crate) async fn elevate_temporary(
30+
Extension(req_id): Extension<i32>,
31+
Extension(info): Extension<NamedPipeConnectInfo>,
32+
NoApi(Db(db)): NoApi<Db>,
33+
NoApi(State(policy)): NoApi<State<Arc<RwLock<Policy>>>>,
2134
Json(payload): Json<ElevateTemporaryPayload>,
22-
) -> Result<(), Error> {
23-
let policy = policy::policy().read();
35+
) -> Result<(), HandlerError> {
36+
// validate input
37+
fn invalid_secs_err() -> HandlerError {
38+
HandlerError::new(
39+
StatusCode::BAD_REQUEST,
40+
Some("number of seconds must be between 1 and 2,147,483,647"),
41+
)
42+
}
43+
let seconds = i32::try_from(payload.seconds).map_err(|_| invalid_secs_err())?;
44+
if seconds < 1 {
45+
return Err(invalid_secs_err());
46+
}
47+
48+
db.insert_elevate_tmp_request(req_id, seconds).await?;
2449

25-
let profile = policy.user_current_profile(&named_pipe_info.user);
26-
if profile.is_none() {
27-
info!(user = ?named_pipe_info.user, "User tried to elevate temporarily, but wasn't assigned to profile");
28-
return Err(Error::AccessDenied);
50+
let policy = policy.read();
51+
if policy.user_current_profile(&info.user).is_none() {
52+
return Err(HandlerError::new(
53+
StatusCode::FORBIDDEN,
54+
Some("user not assigned to profile"),
55+
));
2956
}
3057

3158
let settings = policy
32-
.user_current_profile(&named_pipe_info.user)
59+
.user_current_profile(&info.user)
3360
.map(|p| &p.elevation_settings.temporary)
34-
.ok_or(Error::AccessDenied)?;
61+
.ok_or_else(|| {
62+
HandlerError::new(
63+
StatusCode::FORBIDDEN,
64+
Some("could not get temporary elevation configuration"),
65+
)
66+
})?;
3567

3668
if !settings.enabled {
37-
info!(
38-
user = ?named_pipe_info.user,
39-
"User tried to elevate temporarily, but wasn't allowed",
40-
);
41-
return Err(Error::AccessDenied);
69+
return Err(HandlerError::new(
70+
StatusCode::FORBIDDEN,
71+
Some("temporary elevation is not permitted"),
72+
));
4273
}
4374

44-
let req_duration = Duration::from_secs(payload.seconds);
45-
46-
if Duration::from_secs(settings.maximum_seconds) < req_duration {
47-
info!(
48-
user = ?named_pipe_info.user,
49-
seconds = req_duration.as_secs(),
50-
"User tried to elevate temporarily for too long"
51-
);
52-
return Err(Error::AccessDenied);
75+
let duration = Duration::from_secs(payload.seconds);
76+
if Duration::from_secs(settings.maximum_seconds) < duration {
77+
return Err(HandlerError::new(
78+
StatusCode::FORBIDDEN,
79+
Some("requested duration exceeds maximum"),
80+
));
5381
}
54-
55-
info!(
56-
user = ?named_pipe_info.user,
57-
seconds = req_duration.as_secs(),
58-
"Elevating user"
59-
);
60-
61-
elevations::elevate_temporary(named_pipe_info.user, &req_duration);
82+
elevations::elevate_temporary(info.user, &duration);
6283

6384
Ok(())
6485
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use aide::OperationOutput;
2+
use axum::response::{IntoResponse, Response};
3+
use axum::Json;
4+
use hyper::StatusCode;
5+
6+
use crate::db::DbError;
7+
8+
/// An error type for route handlers.
9+
#[derive(Debug)]
10+
pub(crate) struct HandlerError(StatusCode, Option<String>);
11+
12+
impl HandlerError {
13+
/// Creates a handler error.
14+
///
15+
/// The input message should start with a lowercase letter.
16+
/// It will be capitalized in the response.
17+
pub(crate) fn new(status_code: StatusCode, msg: Option<&str>) -> Self {
18+
Self(
19+
status_code,
20+
msg.map(|s| {
21+
// capitalize first letter
22+
let mut t = s
23+
.chars()
24+
.next()
25+
.expect("handler error messaged contained empty string")
26+
.to_uppercase()
27+
.to_string();
28+
t.push_str(&s[1..]);
29+
t
30+
}),
31+
)
32+
}
33+
}
34+
35+
impl IntoResponse for HandlerError {
36+
fn into_response(self) -> Response {
37+
(self.0, self.1.unwrap_or_default()).into_response()
38+
}
39+
}
40+
41+
// for Aide
42+
impl OperationOutput for HandlerError {
43+
type Inner = (StatusCode, Json<HandlerError>);
44+
}
45+
46+
impl From<DbError> for HandlerError {
47+
fn from(e: DbError) -> Self {
48+
Self(StatusCode::INTERNAL_SERVER_ERROR, Some(e.to_string()))
49+
}
50+
}
51+
52+
#[cfg(feature = "postgres")]
53+
impl From<tokio_postgres::Error> for HandlerError {
54+
fn from(e: tokio_postgres::Error) -> Self {
55+
Self(StatusCode::INTERNAL_SERVER_ERROR, Some(e.to_string()))
56+
}
57+
}

crates/devolutions-pedm/src/api/launch.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
use std::path::{Path, PathBuf};
2+
use std::sync::Arc;
23

4+
use aide::NoApi;
5+
use axum::extract::State;
36
use axum::{Extension, Json};
7+
use parking_lot::RwLock;
48
use schemars::JsonSchema;
59
use serde::{Deserialize, Serialize};
610
use tracing::info;
11+
712
use win_api_wrappers::identity::sid::Sid;
813
use win_api_wrappers::process::{Process, StartupInfo};
914
use win_api_wrappers::raw::Win32::Security::{WinLocalSystemSid, TOKEN_QUERY};
@@ -16,6 +21,7 @@ use win_api_wrappers::utils::{environment_block, expand_environment_path, Comman
1621

1722
use crate::elevator;
1823
use crate::error::Error;
24+
use crate::policy::Policy;
1925

2026
use super::NamedPipeConnectInfo;
2127

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

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

128135
let proc_info = elevator::try_start_elevated(
136+
&policy,
129137
&named_pipe_info.token,
130138
parent_pid,
131139
payload.executable_path.as_deref(),

crates/devolutions-pedm/src/api/logs.rs

Lines changed: 0 additions & 16 deletions
This file was deleted.

0 commit comments

Comments
 (0)