Skip to content

Commit d5612b1

Browse files
committed
feature(pedm): SQL initialization
- adds version tracking in the `pedm_schema_version` table - adds table initialization - error checking is strict and initialization only happens if the version table is not found - adds SQLite error handling for microsecond timestamp conversion to/from Chrono - fixes a bug in the libSQL schema where datetimes were stored incorrectly - they were being stored fractionally, instead of the full microseconds since epoch - adds `/about` endpoint to get basic info about the running application - I used this to ensure that the libsql date handling is functional A few other changes are included: - libsql feature set is changed (default is now false, "stream" removed)
1 parent d69e9c0 commit d5612b1

14 files changed

Lines changed: 1318 additions & 1227 deletions

File tree

Cargo.lock

Lines changed: 806 additions & 1105 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: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,58 @@ publish = false
1111

1212
[dependencies]
1313
anyhow = "1.0"
14-
axum = { version = "0.8", default-features = false, features = ["http1", "json", "tokio", "query", "tracing", "tower-log", "form", "original-uri", "matched-path"] }
14+
axum = { version = "0.8", default-features = false, features = [
15+
"http1",
16+
"json",
17+
"tokio",
18+
"query",
19+
"tracing",
20+
"tower-log",
21+
"form",
22+
"original-uri",
23+
"matched-path",
24+
] }
1525
base16ct = { version = "0.2", features = ["std", "alloc"] }
1626
base64 = "0.22"
27+
chrono = { version = "0.4", features = ["serde"] }
1728
digest = "0.10"
1829
hyper = { version = "1.3", features = ["server"] }
1930
hyper-util = { version = "0.1", features = ["tokio"] }
20-
schemars = "0.8"
31+
schemars = { version = "0.8", features = ["chrono"] }
2132
serde = "1.0"
2233
serde_json = "1.0"
2334
sha1 = "0.10"
2435
sha2 = "0.10"
2536
tokio = { version = "1.44", features = ["net", "rt-multi-thread"] }
2637
tower-service = "0.3"
2738
win-api-wrappers = { path = "../win-api-wrappers" }
28-
devolutions-pedm-shared = { path = "../devolutions-pedm-shared", features = ["policy"]}
39+
devolutions-pedm-shared = { path = "../devolutions-pedm-shared", features = [
40+
"policy",
41+
] }
2942
devolutions-gateway-task = { path = "../devolutions-gateway-task" }
3043
devolutions-agent-shared = { path = "../devolutions-agent-shared" }
3144
camino = { version = "1", features = ["serde1"] }
3245
async-trait = "0.1"
3346
tracing = "0.1"
34-
aide = { version = "0.14", features = ["axum", "axum-extra", "axum-json", "axum-tokio"] }
47+
aide = { version = "0.14", features = [
48+
"axum",
49+
"axum-extra",
50+
"axum-json",
51+
"axum-tokio",
52+
] }
3553
tower-http = { version = "0.5", features = ["timeout"] }
3654
parking_lot = "0.12"
3755
cfg-if = "1.0"
3856
uuid = "1"
3957
dunce = "1.0"
4058
tower = "0.5"
4159
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 }
60+
libsql = { version = "0.9", optional = true, default-features = false, features = ["core", "sync"] }
61+
tokio-postgres = { version = "0.7", optional = true, features = [
62+
"with-chrono-0_4",
63+
] }
64+
bb8 = { version = "0.9", optional = true }
65+
bb8-postgres = { version = "0.9", optional = true }
4666

4767
[features]
4868
default = ["libsql"]
Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,39 @@
1-
/* In SQLite, we store time as integer with microsecond precision. This is the same precision used by TIMESTAMPTZ in Postgres. */
1+
/* In SQLite, we store time as an 8-byte integer (i64) with microsecond precision. This matches TIMESTAMPTZ in Postgres.
2+
Use `chrono::DateTime::timestamp_micros` when inserting or fetching timestamps in Rust.
3+
*/
24

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
5+
CREATE TABLE IF NOT EXISTS pedm_schema_version
6+
(
7+
version integer PRIMARY KEY,
8+
updated_at integer NOT NULL DEFAULT (
9+
CAST(strftime('%s', 'now') AS integer) * 1000000 + CAST(strftime('%f', 'now') * 1000000 AS integer) % 1000000
10+
)
711
);
812

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
13+
CREATE TABLE IF NOT EXISTS pedm_run
14+
(
15+
id integer PRIMARY KEY AUTOINCREMENT,
16+
start_time integer NOT NULL DEFAULT (
17+
CAST(strftime('%s', 'now') AS integer) * 1000000 + CAST(strftime('%f', 'now') * 1000000 AS integer) % 1000000
18+
),
19+
pipe_name text NOT NULL
1520
);
1621

17-
CREATE TABLE elevate_tmp_request (
18-
req_id INTEGER PRIMARY KEY,
19-
seconds INTEGER NOT NULL
22+
CREATE TABLE IF NOT EXISTS http_request
23+
(
24+
id integer PRIMARY KEY,
25+
at integer NOT NULL DEFAULT (
26+
CAST(strftime('%s', 'now') AS integer) * 1000000 + CAST(strftime('%f', 'now') * 1000000 AS integer) % 1000000
27+
),
28+
method text NOT NULL,
29+
path text NOT NULL,
30+
status_code integer NOT NULL
2031
);
32+
33+
CREATE TABLE IF NOT EXISTS elevate_tmp_request
34+
(
35+
req_id integer PRIMARY KEY,
36+
seconds integer NOT NULL
37+
);
38+
39+
INSERT INTO pedm_schema_version (version) VALUES (1) ON CONFLICT DO NOTHING;
Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
1+
CREATE TABLE IF NOT EXISTS pedm_schema_version
2+
(
3+
version smallint PRIMARY KEY,
4+
add_time timestamptz NOT NULL DEFAULT NOW()
5+
);
6+
17
/* The startup of the server */
2-
CREATE TABLE pedm_run
8+
CREATE TABLE IF NOT EXISTS pedm_run
39
(
410
id int PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
511
start_time timestamptz NOT NULL DEFAULT NOW(),
612
pipe_name text NOT NULL
713
);
814

9-
CREATE TABLE http_request
15+
CREATE TABLE IF NOT EXISTS http_request
1016
(
1117
id integer PRIMARY KEY,
1218
at timestamptz NOT NULL DEFAULT NOW(),
1319
method text NOT NULL,
14-
path text NOT NULL,
20+
path text NOT NULL,
1521
status_code smallint NOT NULL
1622
);
1723

18-
CREATE TABLE elevate_tmp_request
24+
/* The request ID is `http_request(id)` but the http_request INSERT only executes in middleware after the response, so we don't use a FK. */
25+
CREATE TABLE IF NOT EXISTS elevate_tmp_request
1926
(
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 */
27+
req_id integer PRIMARY KEY,
2128
seconds int NOT NULL
22-
);
29+
);
30+
31+
INSERT INTO pedm_schema_version (version) VALUES (1) ON CONFLICT DO NOTHING;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
use std::sync::atomic::Ordering;
2+
3+
use aide::NoApi;
4+
use axum::extract::State;
5+
use axum::Json;
6+
7+
use super::err::HandlerError;
8+
use super::state::AppState;
9+
use crate::db::Db;
10+
use crate::model::AboutData;
11+
12+
/// Gets info about the current state of the application.
13+
pub(crate) async fn about(
14+
NoApi(State(state)): NoApi<State<AppState>>,
15+
NoApi(Db(db)): NoApi<Db>,
16+
) -> Result<Json<AboutData>, HandlerError> {
17+
let requests_received = state.req_counter.load(Ordering::Relaxed) - state.startup_info.request_count;
18+
19+
Ok(Json(AboutData {
20+
startup_info: state.startup_info,
21+
requests_received,
22+
last_request_time: db.get_last_request_time().await?,
23+
}))
24+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ use parking_lot::RwLock;
99
use schemars::JsonSchema;
1010
use serde::{Deserialize, Serialize};
1111

12+
use crate::db::Db;
1213
use crate::elevations;
1314
use crate::policy::Policy;
1415

1516
use super::err::HandlerError;
16-
use super::state::Db;
1717
use super::NamedPipeConnectInfo;
1818

1919
#[derive(Deserialize, Serialize, JsonSchema, Debug)]

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

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,18 +32,12 @@ use win_api_wrappers::token::Token;
3232
use win_api_wrappers::undoc::PIPE_ACCESS_FULL_CONTROL;
3333
use win_api_wrappers::utils::Pipe;
3434

35-
use elevate_session::elevate_session;
36-
use elevate_temporary::elevate_temporary;
37-
use launch::post_launch;
38-
use revoke::post_revoke;
39-
use state::{AppState, AppStateError};
40-
use status::get_status;
41-
4235
use crate::config::Config;
43-
use crate::db::DbError;
36+
use crate::db::{Db, DbError, InitSchemaError};
4437
use crate::error::{Error, ErrorResponse};
4538
use crate::utils::AccountExt;
4639

40+
mod about;
4741
mod elevate_session;
4842
mod elevate_temporary;
4943
mod err;
@@ -53,6 +47,14 @@ mod revoke;
5347
pub(crate) mod state;
5448
mod status;
5549

50+
use about::about;
51+
use elevate_session::elevate_session;
52+
use elevate_temporary::elevate_temporary;
53+
use launch::post_launch;
54+
use revoke::post_revoke;
55+
use state::{AppState, AppStateError};
56+
use status::get_status;
57+
5658
#[derive(Debug, Clone)]
5759
struct NamedPipeConnectInfo {
5860
pub(crate) user: User,
@@ -132,6 +134,7 @@ fn create_pipe(pipe_name: &str) -> anyhow::Result<NamedPipeServer> {
132134

133135
pub(crate) fn api_router() -> ApiRouter<AppState> {
134136
ApiRouter::new()
137+
.api_route("/about", aide::axum::routing::get(about))
135138
.api_route("/elevate/temporary", aide::axum::routing::post(elevate_temporary))
136139
.api_route("/elevate/session", aide::axum::routing::post(elevate_session))
137140
.api_route("/launch", aide::axum::routing::post(post_launch))
@@ -165,8 +168,12 @@ async fn health_check() -> &'static str {
165168
"OK"
166169
}
167170

171+
/// Initializes the appliation and starts the named pipe server.
168172
pub async fn serve(config: Config) -> Result<(), ServeError> {
169-
let state = AppState::load(&config).await?;
173+
let db = Db::new(&config).await?;
174+
db.init_schema().await?;
175+
176+
let state = AppState::new(db, &config.pipe_name).await?;
170177

171178
// a plain Axum router
172179
let hello_router = Router::new().route("/health", axum::routing::get(health_check));
@@ -182,11 +189,11 @@ pub async fn serve(config: Config) -> Result<(), ServeError> {
182189
let mut server = create_pipe(pipe_name)?;
183190

184191
// Log the server startup.
185-
let run_id = state.db.log_server_startup(pipe_name).await?;
186-
info!("Started server at {pipe_name} with run ID {run_id}");
192+
info!("Started named pipe server with name `{pipe_name}`");
187193
info!(
188-
"Starting request ID counter at {}",
189-
state.req_counter.load(Ordering::Relaxed)
194+
"Run ID is {run_id}, request ID counter is {req_count}",
195+
run_id = state.startup_info.run_id,
196+
req_count = state.req_counter.load(Ordering::Relaxed)
190197
);
191198

192199
loop {
@@ -219,6 +226,7 @@ pub enum ServeError {
219226
TokioIo(tokio::io::Error),
220227
AppState(AppStateError),
221228
Db(DbError),
229+
InitSchema(InitSchemaError),
222230
Other(anyhow::Error),
223231
}
224232

@@ -228,6 +236,7 @@ impl core::error::Error for ServeError {
228236
Self::TokioIo(e) => Some(e),
229237
Self::AppState(e) => Some(e),
230238
Self::Db(e) => Some(e),
239+
Self::InitSchema(e) => Some(e),
231240
Self::Other(e) => Some(e.as_ref()),
232241
}
233242
}
@@ -239,6 +248,7 @@ impl fmt::Display for ServeError {
239248
Self::TokioIo(e) => e.fmt(f),
240249
Self::AppState(e) => e.fmt(f),
241250
Self::Db(e) => e.fmt(f),
251+
Self::InitSchema(e) => e.fmt(f),
242252
Self::Other(e) => e.fmt(f),
243253
}
244254
}
@@ -259,6 +269,11 @@ impl From<DbError> for ServeError {
259269
Self::Db(e)
260270
}
261271
}
272+
impl From<InitSchemaError> for ServeError {
273+
fn from(e: InitSchemaError) -> Self {
274+
Self::InitSchema(e)
275+
}
276+
}
262277
impl From<anyhow::Error> for ServeError {
263278
fn from(e: anyhow::Error) -> Self {
264279
Self::Other(e)

0 commit comments

Comments
 (0)