remove sqlite extensions to fix docker issue

UUIDs are now generated in the backend before insertion
This commit is contained in:
2025-09-07 17:19:16 +02:00
parent a1c9832515
commit 6fa26b3ddb
33 changed files with 754 additions and 308 deletions

View File

@@ -12,3 +12,4 @@ frontend/node_modules
backend/target
backend/.gitignore
backend/data

View File

@@ -13,19 +13,9 @@ COPY frontend/ ./
RUN npm run generate
FROM alpine:3 AS sqlite-extension-compiler
WORKDIR /var/lib/warren
RUN apk add sqlite-libs sqlite-dev build-base
COPY backend/sqlite_extensions sqlite_extensions
RUN gcc -g -fPIC -shared sqlite_extensions/uuid.c -o sqlite_extensions/uuid
FROM rust:alpine AS backend-builder
WORKDIR /usr/src/warren
RUN apk add sqlite sqlite-dev build-base
COPY backend/Cargo.toml backend/Cargo.lock ./
RUN mkdir -p src/bin/backend && mkdir src/lib && echo "fn main() {}" > src/bin/backend/main.rs && echo "" > src/lib/lib.rs
RUN apk add --no-cache pkgconfig openssl openssl-dev libc-dev openssl-libs-static
@@ -38,8 +28,6 @@ RUN cargo build --release
FROM alpine:3
WORKDIR /var/lib/warren
COPY --from=sqlite-extension-compiler /var/lib/warren/sqlite_extensions/uuid /var/lib/warren/sqlite_extensions/uuid
COPY --from=backend-builder /usr/src/warren/target/release/warren_backend /usr/bin/warren
COPY --from=frontend-builder /usr/src/warren/dist ./frontend

1
backend/.gitignore vendored
View File

@@ -1,3 +1,4 @@
target
serve
.env
data

1
backend/Cargo.lock generated
View File

@@ -2721,6 +2721,7 @@ version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
dependencies = [
"getrandom 0.3.3",
"js-sys",
"serde",
"wasm-bindgen",

View File

@@ -39,5 +39,5 @@ tower-http = { version = "0.6.6", features = ["cors", "fs", "trace"] }
tracing = "0.1.41"
tracing-subscriber = "0.3.19"
url = "2.5.4"
uuid = { version = "1.17.0", features = ["serde"] }
uuid = { version = "1.17.0", features = ["serde", "v4"] }
zip = "4.5.0"

View File

@@ -1,5 +1,5 @@
CREATE TABLE users (
id BLOB NOT NULL PRIMARY KEY DEFAULT (uuid_blob(uuid())),
id BLOB NOT NULL PRIMARY KEY,
oidc_sub TEXT UNIQUE,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
@@ -10,7 +10,7 @@ CREATE TABLE users (
);
CREATE TABLE warrens (
id BLOB NOT NULL PRIMARY KEY DEFAULT (uuid_blob(uuid())),
id BLOB NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
path TEXT NOT NULL UNIQUE,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
@@ -31,7 +31,7 @@ CREATE TABLE user_warrens (
);
CREATE TABLE shares (
id BLOB NOT NULL PRIMARY KEY DEFAULT (uuid_blob(uuid())),
id BLOB NOT NULL PRIMARY KEY,
creator_id BLOB NOT NULL REFERENCES users(id) ON DELETE CASCADE,
warren_id BLOB NOT NULL REFERENCES warrens(id) ON DELETE CASCADE,
path TEXT NOT NULL,
@@ -48,3 +48,8 @@ CREATE TABLE auth_sessions (
expires_at DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE application_options (
key TEXT NOT NULL PRIMARY KEY,
value TEXT NOT NULL
);

View File

@@ -1,11 +0,0 @@
INSERT INTO users (
name,
email,
hash,
admin
) VALUES (
'admin',
'admin@example.com',
'$argon2id$v=19$m=19456,t=2,p=1$H1WsElL4921/WD5oPkY7JQ$aHudNG8z0ns3pRULfuDpuEkxPUbGxq9AHC4QGyt5odc',
true
);

Binary file not shown.

View File

@@ -1,231 +0,0 @@
/*
** 2019-10-23
**
** The author disclaims copyright to this source code. In place of
** a legal notice, here is a blessing:
**
** May you do good and not evil.
** May you find forgiveness for yourself and forgive others.
** May you share freely, never taking more than you give.
**
******************************************************************************
**
** This SQLite extension implements functions that handling RFC-4122 UUIDs
** Three SQL functions are implemented:
**
** uuid() - generate a version 4 UUID as a string
** uuid_str(X) - convert a UUID X into a well-formed UUID string
** uuid_blob(X) - convert a UUID X into a 16-byte blob
**
** The output from uuid() and uuid_str(X) are always well-formed RFC-4122
** UUID strings in this format:
**
** xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx
**
** All of the 'x', 'M', and 'N' values are lower-case hexadecimal digits.
** The M digit indicates the "version". For uuid()-generated UUIDs, the
** version is always "4" (a random UUID). The upper three bits of N digit
** are the "variant". This library only supports variant 1 (indicated
** by values of N between '8' and 'b') as those are overwhelming the most
** common. Other variants are for legacy compatibility only.
**
** The output of uuid_blob(X) is always a 16-byte blob. The UUID input
** string is converted in network byte order (big-endian) in accordance
** with RFC-4122 specifications for variant-1 UUIDs. Note that network
** byte order is *always* used, even if the input self-identifies as a
** variant-2 UUID.
**
** The input X to the uuid_str() and uuid_blob() functions can be either
** a string or a BLOB. If it is a BLOB it must be exactly 16 bytes in
** length or else a NULL is returned. If the input is a string it must
** consist of 32 hexadecimal digits, upper or lower case, optionally
** surrounded by {...} and with optional "-" characters interposed in the
** middle. The flexibility of input is inspired by the PostgreSQL
** implementation of UUID functions that accept in all of the following
** formats:
**
** A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11
** {a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}
** a0eebc999c0b4ef8bb6d6bb9bd380a11
** a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11
** {a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}
**
** If any of the above inputs are passed into uuid_str(), the output will
** always be in the canonical RFC-4122 format:
**
** a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11
**
** If the X input string has too few or too many digits or contains
** stray characters other than {, }, or -, then NULL is returned.
*/
#include "sqlite3ext.h"
SQLITE_EXTENSION_INIT1
#include <assert.h>
#include <ctype.h>
#include <string.h>
#if !defined(SQLITE_ASCII) && !defined(SQLITE_EBCDIC)
#define SQLITE_ASCII 1
#endif
/*
** Translate a single byte of Hex into an integer.
** This routine only works if h really is a valid hexadecimal
** character: 0..9a..fA..F
*/
static unsigned char sqlite3UuidHexToInt(int h) {
assert((h >= '0' && h <= '9') || (h >= 'a' && h <= 'f') ||
(h >= 'A' && h <= 'F'));
#ifdef SQLITE_ASCII
h += 9 * (1 & (h >> 6));
#endif
#ifdef SQLITE_EBCDIC
h += 9 * (1 & ~(h >> 4));
#endif
return (unsigned char)(h & 0xf);
}
/*
** Convert a 16-byte BLOB into a well-formed RFC-4122 UUID. The output
** buffer zStr should be at least 37 bytes in length. The output will
** be zero-terminated.
*/
static void sqlite3UuidBlobToStr(const unsigned char *aBlob, /* Input blob */
unsigned char *zStr /* Write the answer here */
) {
static const char zDigits[] = "0123456789abcdef";
int i, k;
unsigned char x;
k = 0;
for (i = 0, k = 0x550; i < 16; i++, k = k >> 1) {
if (k & 1) {
zStr[0] = '-';
zStr++;
}
x = aBlob[i];
zStr[0] = zDigits[x >> 4];
zStr[1] = zDigits[x & 0xf];
zStr += 2;
}
*zStr = 0;
}
/*
** Attempt to parse a zero-terminated input string zStr into a binary
** UUID. Return 0 on success, or non-zero if the input string is not
** parsable.
*/
static int sqlite3UuidStrToBlob(const unsigned char *zStr, /* Input string */
unsigned char *aBlob /* Write results here */
) {
int i;
if (zStr[0] == '{')
zStr++;
for (i = 0; i < 16; i++) {
if (zStr[0] == '-')
zStr++;
if (isxdigit(zStr[0]) && isxdigit(zStr[1])) {
aBlob[i] = (sqlite3UuidHexToInt(zStr[0]) << 4) +
sqlite3UuidHexToInt(zStr[1]);
zStr += 2;
} else {
return 1;
}
}
if (zStr[0] == '}')
zStr++;
return zStr[0] != 0;
}
/*
** Render sqlite3_value pIn as a 16-byte UUID blob. Return a pointer
** to the blob, or NULL if the input is not well-formed.
*/
static const unsigned char *
sqlite3UuidInputToBlob(sqlite3_value *pIn, /* Input text */
unsigned char *pBuf /* output buffer */
) {
switch (sqlite3_value_type(pIn)) {
case SQLITE_TEXT: {
const unsigned char *z = sqlite3_value_text(pIn);
if (sqlite3UuidStrToBlob(z, pBuf))
return 0;
return pBuf;
}
case SQLITE_BLOB: {
int n = sqlite3_value_bytes(pIn);
return n == 16 ? sqlite3_value_blob(pIn) : 0;
}
default: {
return 0;
}
}
}
/* Implementation of uuid() */
static void sqlite3UuidFunc(sqlite3_context *context, int argc,
sqlite3_value **argv) {
unsigned char aBlob[16];
unsigned char zStr[37];
(void)argc;
(void)argv;
sqlite3_randomness(16, aBlob);
aBlob[6] = (aBlob[6] & 0x0f) + 0x40;
aBlob[8] = (aBlob[8] & 0x3f) + 0x80;
sqlite3UuidBlobToStr(aBlob, zStr);
sqlite3_result_text(context, (char *)zStr, 36, SQLITE_TRANSIENT);
}
/* Implementation of uuid_str() */
static void sqlite3UuidStrFunc(sqlite3_context *context, int argc,
sqlite3_value **argv) {
unsigned char aBlob[16];
unsigned char zStr[37];
const unsigned char *pBlob;
(void)argc;
pBlob = sqlite3UuidInputToBlob(argv[0], aBlob);
if (pBlob == 0)
return;
sqlite3UuidBlobToStr(pBlob, zStr);
sqlite3_result_text(context, (char *)zStr, 36, SQLITE_TRANSIENT);
}
/* Implementation of uuid_blob() */
static void sqlite3UuidBlobFunc(sqlite3_context *context, int argc,
sqlite3_value **argv) {
unsigned char aBlob[16];
const unsigned char *pBlob;
(void)argc;
pBlob = sqlite3UuidInputToBlob(argv[0], aBlob);
if (pBlob == 0)
return;
sqlite3_result_blob(context, pBlob, 16, SQLITE_TRANSIENT);
}
#ifdef _WIN32
__declspec(dllexport)
#endif
int sqlite3_uuid_init(
sqlite3 *db,
char **pzErrMsg,
const sqlite3_api_routines *pApi
){
int rc = SQLITE_OK;
SQLITE_EXTENSION_INIT2(pApi);
(void)pzErrMsg; /* Unused parameter */
rc = sqlite3_create_function(db, "uuid", 0, SQLITE_UTF8 | SQLITE_INNOCUOUS,
0, sqlite3UuidFunc, 0, 0);
if (rc == SQLITE_OK) {
rc = sqlite3_create_function(db, "uuid_str", 1,
SQLITE_UTF8 | SQLITE_INNOCUOUS |
SQLITE_DETERMINISTIC,
0, sqlite3UuidStrFunc, 0, 0);
}
if (rc == SQLITE_OK) {
rc = sqlite3_create_function(db, "uuid_blob", 1,
SQLITE_UTF8 | SQLITE_INNOCUOUS |
SQLITE_DETERMINISTIC,
0, sqlite3UuidBlobFunc, 0, 0);
}
return rc;
}

View File

@@ -46,13 +46,18 @@ async fn main() -> anyhow::Result<()> {
None
};
let option_service =
domain::warren::service::option::Service::new(sqlite.clone(), metrics, notifier);
let auth_service = domain::warren::service::auth::Service::new(
sqlite,
metrics,
notifier,
config.auth,
oidc_service,
);
option_service,
)
.await?;
let server_config = HttpServerConfig::new(
&config.server_address,

View File

@@ -1,5 +1,6 @@
pub mod auth_session;
pub mod file;
pub mod option;
pub mod share;
pub mod user;
pub mod user_warren;

View File

@@ -0,0 +1,74 @@
mod requests;
use derive_more::Display;
pub use requests::*;
use thiserror::Error;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Display)]
pub struct OptionKey(String);
#[derive(Debug, Error)]
pub enum OptionKeyError {
#[error("An OptionKey must not be empty")]
Empty,
}
impl OptionKey {
pub fn new(raw: &str) -> Result<Self, OptionKeyError> {
let raw = raw.trim();
if raw.is_empty() {
return Err(OptionKeyError::Empty);
}
Ok(Self(raw.to_string()))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Clone, Debug)]
pub struct OptionValue<T>(T)
where
T: OptionType;
impl<T> OptionValue<T>
where
T: OptionType,
{
pub fn new(value: T) -> Self {
Self(value)
}
pub fn inner(&self) -> &T {
&self.0
}
pub fn get_inner(self) -> T {
self.0
}
}
pub trait OptionType: std::fmt::Debug + Clone + Send + Sync {
type Error: std::fmt::Debug;
fn parse(raw: &str) -> Result<Self, Self::Error>;
fn to_string(&self) -> String;
}
impl OptionType for bool {
type Error = anyhow::Error;
fn parse(raw: &str) -> Result<Self, Self::Error> {
Ok(match raw.to_lowercase().as_str() {
"true" => true,
"false" => false,
_ => anyhow::bail!("Expected 'true' or 'false': {raw}"),
})
}
fn to_string(&self) -> String {
if *self { "true" } else { "false" }.to_string()
}
}

View File

@@ -0,0 +1,47 @@
use thiserror::Error;
use crate::domain::warren::models::option::OptionKey;
#[derive(Clone, Debug)]
pub struct DeleteOptionRequest {
key: OptionKey,
}
impl DeleteOptionRequest {
pub fn new(key: OptionKey) -> Self {
Self { key }
}
pub fn key(&self) -> &OptionKey {
&self.key
}
}
impl From<DeleteOptionRequest> for OptionKey {
fn from(value: DeleteOptionRequest) -> Self {
value.key
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DeleteOptionResponse {
key: OptionKey,
}
impl DeleteOptionResponse {
pub fn new(key: OptionKey) -> Self {
Self { key }
}
pub fn key(&self) -> &OptionKey {
&self.key
}
}
#[derive(Debug, Error)]
pub enum DeleteOptionError {
#[error("Could not find option with key: {0}")]
NotFound(OptionKey),
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}

View File

@@ -0,0 +1,57 @@
use thiserror::Error;
use crate::domain::warren::models::option::{OptionKey, OptionType};
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct GetOptionRequest {
key: OptionKey,
}
impl GetOptionRequest {
pub fn new(key: OptionKey) -> Self {
Self { key }
}
pub fn key(&self) -> &OptionKey {
&self.key
}
}
impl From<GetOptionRequest> for OptionKey {
fn from(value: GetOptionRequest) -> Self {
value.key
}
}
#[derive(Clone, Debug)]
pub struct GetOptionResponse<T: OptionType> {
key: OptionKey,
value: T,
}
impl<T> GetOptionResponse<T>
where
T: OptionType,
{
pub fn new(key: OptionKey, value: T) -> Self {
Self { key, value }
}
pub fn key(&self) -> &OptionKey {
&self.key
}
pub fn value(&self) -> &T {
&self.value
}
}
#[derive(Debug, Error)]
pub enum GetOptionError {
#[error("Could not find option with key: {0}")]
NotFound(OptionKey),
#[error("Could not parse the option value with the specified type")]
Parse,
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}

View File

@@ -0,0 +1,6 @@
mod delete;
mod get;
mod set;
pub use delete::*;
pub use get::*;
pub use set::*;

View File

@@ -0,0 +1,83 @@
use thiserror::Error;
use crate::domain::warren::models::option::{OptionKey, OptionType, OptionValue};
#[derive(Clone, Debug)]
pub struct SetOptionRequest<T>
where
T: OptionType,
{
key: OptionKey,
value: OptionValue<T>,
}
impl<T> SetOptionRequest<T>
where
T: OptionType,
{
pub fn new(key: OptionKey, value: T) -> Self {
Self {
key,
value: OptionValue::new(value),
}
}
pub fn key(&self) -> &OptionKey {
&self.key
}
pub fn value(&self) -> &OptionValue<T> {
&self.value
}
pub fn unpack(self) -> (OptionKey, OptionValue<T>) {
(self.key, self.value)
}
}
impl<T> From<SetOptionRequest<T>> for OptionKey
where
T: OptionType,
{
fn from(value: SetOptionRequest<T>) -> Self {
value.key
}
}
impl<T> From<SetOptionRequest<T>> for OptionValue<T>
where
T: OptionType,
{
fn from(value: SetOptionRequest<T>) -> Self {
value.value
}
}
#[derive(Clone, Debug)]
pub struct SetOptionResponse<T: OptionType> {
key: OptionKey,
value: T,
}
impl<T> SetOptionResponse<T>
where
T: OptionType,
{
pub fn new(key: OptionKey, value: T) -> Self {
Self { key, value }
}
pub fn key(&self) -> &OptionKey {
&self.key
}
pub fn value(&self) -> &T {
&self.value
}
}
#[derive(Debug, Error)]
pub enum SetOptionError {
#[error(transparent)]
Unknown(#[from] anyhow::Error),
}

View File

@@ -9,6 +9,8 @@ pub struct RegisterUserRequest {
name: UserName,
email: UserEmail,
password: UserPassword,
bypass_registration_flag: bool,
admin: bool,
}
impl RegisterUserRequest {
@@ -17,6 +19,23 @@ impl RegisterUserRequest {
name,
email,
password,
bypass_registration_flag: false,
admin: false,
}
}
pub fn new_bypass_flag(
name: UserName,
email: UserEmail,
password: UserPassword,
admin: bool,
) -> Self {
Self {
name,
email,
password,
bypass_registration_flag: true,
admin,
}
}
@@ -31,11 +50,19 @@ impl RegisterUserRequest {
pub fn password(&self) -> &UserPassword {
&self.password
}
pub fn admin(&self) -> bool {
self.admin
}
pub fn bypass_registration_flag(&self) -> bool {
self.bypass_registration_flag
}
}
impl From<RegisterUserRequest> for CreateUserRequest {
fn from(value: RegisterUserRequest) -> Self {
Self::new(value.name, value.email, value.password, false)
Self::new(value.name, value.email, value.password, value.admin)
}
}

View File

@@ -185,3 +185,14 @@ pub trait AuthMetrics: Clone + Send + Sync + 'static {
fn record_auth_share_deletion_success(&self) -> impl Future<Output = ()> + Send;
fn record_auth_share_deletion_failure(&self) -> impl Future<Output = ()> + Send;
}
pub trait OptionMetrics: Clone + Send + Sync + 'static {
fn record_option_get_success(&self) -> impl Future<Output = ()> + Send;
fn record_option_get_failure(&self) -> impl Future<Output = ()> + Send;
fn record_option_set_success(&self) -> impl Future<Output = ()> + Send;
fn record_option_set_failure(&self) -> impl Future<Output = ()> + Send;
fn record_option_delete_success(&self) -> impl Future<Output = ()> + Send;
fn record_option_delete_failure(&self) -> impl Future<Output = ()> + Send;
}

View File

@@ -20,6 +20,11 @@ use super::models::{
RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
TouchError, TouchRequest,
},
option::{
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
GetOptionRequest, GetOptionResponse, OptionType, SetOptionError, SetOptionRequest,
SetOptionResponse,
},
share::{
CreateShareBaseRequest, CreateShareError, CreateShareRequest, CreateShareResponse,
DeleteShareError, DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest,
@@ -337,3 +342,18 @@ pub trait AuthService: Clone + Send + Sync + 'static {
warren_service: &WS,
) -> impl Future<Output = Result<DeleteShareResponse, AuthError<DeleteShareError>>> + Send;
}
pub trait OptionService: Clone + Send + Sync + 'static {
fn get_option<T: OptionType>(
&self,
request: GetOptionRequest,
) -> impl Future<Output = Result<GetOptionResponse<T>, GetOptionError>> + Send;
fn set_option<T: OptionType>(
&self,
request: SetOptionRequest<T>,
) -> impl Future<Output = Result<SetOptionResponse<T>, SetOptionError>> + Send;
fn delete_option(
&self,
request: DeleteOptionRequest,
) -> impl Future<Output = Result<DeleteOptionResponse, DeleteOptionError>> + Send;
}

View File

@@ -3,6 +3,7 @@ use uuid::Uuid;
use crate::domain::warren::models::{
auth_session::requests::FetchAuthSessionResponse,
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
option::{DeleteOptionResponse, GetOptionResponse, OptionType, SetOptionResponse},
share::{
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
ShareCatResponse, ShareLsResponse, VerifySharePasswordResponse,
@@ -219,3 +220,15 @@ pub trait AuthNotifier: Clone + Send + Sync + 'static {
response: &DeleteShareResponse,
) -> impl Future<Output = ()> + Send;
}
pub trait OptionNotifier: Clone + Send + Sync + 'static {
fn got_option<T: OptionType>(
&self,
response: &GetOptionResponse<T>,
) -> impl Future<Output = ()> + Send;
fn set_option<T: OptionType>(
&self,
response: &SetOptionResponse<T>,
) -> impl Future<Output = ()> + Send;
fn deleted_option(&self, response: &DeleteOptionResponse) -> impl Future<Output = ()> + Send;
}

View File

@@ -12,6 +12,11 @@ use crate::domain::warren::models::{
RmRequest, SaveError, SaveRequest, SaveResponse, StatError, StatRequest, StatResponse,
TouchError, TouchRequest,
},
option::{
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
GetOptionRequest, GetOptionResponse, OptionType, SetOptionError, SetOptionRequest,
SetOptionResponse,
},
share::{
CreateShareError, CreateShareRequest, CreateShareResponse, DeleteShareError,
DeleteShareRequest, DeleteShareResponse, GetShareError, GetShareRequest, ListSharesError,
@@ -188,3 +193,18 @@ pub trait AuthRepository: Clone + Send + Sync + 'static {
request: FetchUserWarrenRequest,
) -> impl Future<Output = Result<UserWarren, FetchUserWarrenError>> + Send;
}
pub trait OptionRepository: Clone + Send + Sync + 'static {
fn get_option<T: OptionType>(
&self,
request: GetOptionRequest,
) -> impl Future<Output = Result<GetOptionResponse<T>, GetOptionError>> + Send;
fn set_option<T: OptionType>(
&self,
request: SetOptionRequest<T>,
) -> impl Future<Output = Result<SetOptionResponse<T>, SetOptionError>> + Send;
fn delete_option(
&self,
request: DeleteOptionRequest,
) -> impl Future<Output = Result<DeleteOptionResponse, DeleteOptionError>> + Send;
}

View File

@@ -12,6 +12,7 @@ use crate::{
},
},
file::FileStream,
option::{GetOptionError, GetOptionRequest, OptionKey, SetOptionRequest},
share::{
CreateShareBaseRequest, CreateShareError, CreateShareResponse,
DeleteShareError, DeleteShareRequest, DeleteShareResponse, ListSharesError,
@@ -24,7 +25,7 @@ use crate::{
ListAllUsersAndWarrensRequest, ListAllUsersAndWarrensResponse, ListUsersError,
ListUsersRequest, LoginUserError, LoginUserOidcError, LoginUserOidcRequest,
LoginUserOidcResponse, LoginUserRequest, LoginUserResponse, RegisterUserError,
RegisterUserRequest, User,
RegisterUserRequest, User, UserEmail, UserName, UserPassword,
},
user_warren::{
UserWarren,
@@ -46,7 +47,10 @@ use crate::{
WarrenTouchResponse,
},
},
ports::{AuthMetrics, AuthNotifier, AuthRepository, AuthService, WarrenService},
ports::{
AuthMetrics, AuthNotifier, AuthRepository, AuthService, OptionService,
WarrenService,
},
},
},
};
@@ -97,54 +101,100 @@ impl AuthConfig {
}
#[derive(Debug, Clone)]
pub struct Service<R, M, N, OIDC>
pub struct Service<R, M, N, OIDC, O>
where
R: AuthRepository,
M: AuthMetrics,
N: AuthNotifier,
OIDC: OidcService,
O: OptionService,
{
repository: R,
metrics: M,
notifier: N,
oidc: Option<OIDC>,
option_service: O,
config: AuthConfig,
}
impl<R, M, N, OIDC> Service<R, M, N, OIDC>
impl<R, M, N, OIDC, O> Service<R, M, N, OIDC, O>
where
R: AuthRepository,
M: AuthMetrics,
N: AuthNotifier,
OIDC: OidcService,
O: OptionService,
{
pub fn new(
pub async fn new(
repository: R,
metrics: M,
notifier: N,
config: AuthConfig,
oidc: Option<OIDC>,
) -> Self {
Self {
option_service: O,
) -> anyhow::Result<Self> {
let service = Self {
repository,
metrics,
notifier,
config,
oidc,
}
option_service,
};
service.init().await?;
Ok(service)
}
pub fn oidc(&self) -> Option<&OIDC> {
self.oidc.as_ref()
}
async fn init(&self) -> anyhow::Result<()> {
self.create_admin_user_if_init().await?;
Ok(())
}
async fn create_admin_user_if_init(&self) -> anyhow::Result<()> {
const CREATED_ADMIN_USER_KEY: &str = "CREATED_ADMIN_USER";
let key = OptionKey::new(CREATED_ADMIN_USER_KEY)?;
let request = GetOptionRequest::new(key.clone());
match self.option_service.get_option::<bool>(request).await {
// If the option is already set and true we don't have to do anything anymore
Ok(opt) if *opt.value() => return Ok(()),
Err(e) => match e {
// The option is not yet set so we proceed with the admin user creation
GetOptionError::NotFound(_) => (),
_ => return Err(e.into()),
},
// The option was set but it was false so we proceed with the admin user creation
_ => (),
}
let name = UserName::new("admin")?;
let email = UserEmail::new("admin@example.com")?;
let password = UserPassword::new("admin1234567")?;
let request = RegisterUserRequest::new_bypass_flag(name, email, password, true);
self.register_user(request).await?;
self.option_service
.set_option(SetOptionRequest::new(key, true))
.await?;
Ok(())
}
}
impl<R, M, N, OIDC> AuthService for Service<R, M, N, OIDC>
impl<R, M, N, OIDC, O> AuthService for Service<R, M, N, OIDC, O>
where
R: AuthRepository,
M: AuthMetrics,
N: AuthNotifier,
OIDC: OidcService,
O: OptionService,
{
async fn create_warren<WS: WarrenService>(
&self,
@@ -253,7 +303,7 @@ where
}
async fn register_user(&self, request: RegisterUserRequest) -> Result<User, RegisterUserError> {
if !self.config.allow_registration {
if !self.config.allow_registration && !request.bypass_registration_flag() {
self.metrics.record_user_registration_failure().await;
return Err(RegisterUserError::Disabled);
}
@@ -972,12 +1022,13 @@ where
}
}
impl<R, M, N, OIDC> Service<R, M, N, OIDC>
impl<R, M, N, OIDC, O> Service<R, M, N, OIDC, O>
where
R: AuthRepository,
M: AuthMetrics,
N: AuthNotifier,
OIDC: OidcService,
O: OptionService,
{
/// A helper to get a [UserWarren], [User] and the underlying request from an [AuthRequest]
async fn get_session_data_and_user_warren<T, E>(

View File

@@ -1,3 +1,4 @@
pub mod auth;
pub mod file_system;
pub mod option;
pub mod warren;

View File

@@ -0,0 +1,90 @@
use crate::domain::warren::{
models::option::{
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
GetOptionRequest, GetOptionResponse, OptionType, SetOptionError, SetOptionRequest,
SetOptionResponse,
},
ports::{OptionMetrics, OptionNotifier, OptionRepository, OptionService},
};
#[derive(Debug, Clone)]
pub struct Service<R, M, N>
where
R: OptionRepository,
M: OptionMetrics,
N: OptionNotifier,
{
repository: R,
metrics: M,
notifier: N,
}
impl<R, M, N> Service<R, M, N>
where
R: OptionRepository,
M: OptionMetrics,
N: OptionNotifier,
{
pub fn new(repository: R, metrics: M, notifier: N) -> Self {
Self {
repository,
metrics,
notifier,
}
}
}
impl<R, M, N> OptionService for Service<R, M, N>
where
R: OptionRepository,
M: OptionMetrics,
N: OptionNotifier,
{
async fn get_option<T: OptionType>(
&self,
request: GetOptionRequest,
) -> Result<GetOptionResponse<T>, GetOptionError> {
let result = self.repository.get_option(request).await;
if let Ok(response) = result.as_ref() {
self.metrics.record_option_get_success().await;
self.notifier.got_option(response).await;
} else {
self.metrics.record_option_get_failure().await;
}
result
}
async fn set_option<T: OptionType>(
&self,
request: SetOptionRequest<T>,
) -> Result<SetOptionResponse<T>, SetOptionError> {
let result = self.repository.set_option(request).await;
if let Ok(response) = result.as_ref() {
self.metrics.record_option_set_success().await;
self.notifier.set_option(response).await;
} else {
self.metrics.record_option_set_failure().await;
}
result
}
async fn delete_option(
&self,
request: DeleteOptionRequest,
) -> Result<DeleteOptionResponse, DeleteOptionError> {
let result = self.repository.delete_option(request).await;
if let Ok(response) = result.as_ref() {
self.metrics.record_option_delete_success().await;
self.notifier.deleted_option(response).await;
} else {
self.metrics.record_option_delete_failure().await;
}
result
}
}

View File

@@ -1,6 +1,6 @@
use crate::domain::{
oidc::ports::OidcMetrics,
warren::ports::{AuthMetrics, FileSystemMetrics, WarrenMetrics},
warren::ports::{AuthMetrics, FileSystemMetrics, OptionMetrics, WarrenMetrics},
};
#[derive(Debug, Clone, Copy)]
@@ -452,3 +452,26 @@ impl OidcMetrics for MetricsDebugLogger {
tracing::debug!("[Metrics] OIDC get user info failed");
}
}
impl OptionMetrics for MetricsDebugLogger {
async fn record_option_get_success(&self) {
tracing::debug!("[Metrics] Get option succeeded");
}
async fn record_option_get_failure(&self) {
tracing::debug!("[Metrics] Get option failed");
}
async fn record_option_set_success(&self) {
tracing::debug!("[Metrics] Set option succeeded");
}
async fn record_option_set_failure(&self) {
tracing::debug!("[Metrics] Set option failed");
}
async fn record_option_delete_success(&self) {
tracing::debug!("[Metrics] Delete option succeeded");
}
async fn record_option_delete_failure(&self) {
tracing::debug!("[Metrics] Delete option failed");
}
}

View File

@@ -9,6 +9,7 @@ use crate::domain::{
models::{
auth_session::requests::FetchAuthSessionResponse,
file::{AbsoluteFilePath, AbsoluteFilePathList, LsResponse},
option::{DeleteOptionResponse, GetOptionResponse, OptionType, SetOptionResponse},
share::{
CreateShareResponse, DeleteShareResponse, GetShareResponse, ListSharesResponse,
ShareCatResponse, ShareLsResponse, VerifySharePasswordResponse,
@@ -22,7 +23,7 @@ use crate::domain::{
WarrenRmResponse, WarrenSaveResponse, WarrenTouchResponse,
},
},
ports::{AuthNotifier, FileSystemNotifier, WarrenNotifier},
ports::{AuthNotifier, FileSystemNotifier, OptionNotifier, WarrenNotifier},
},
};
@@ -515,3 +516,25 @@ impl OidcNotifier for NotifierDebugLogger {
);
}
}
impl OptionNotifier for NotifierDebugLogger {
async fn got_option<T: OptionType>(&self, response: &GetOptionResponse<T>) {
tracing::debug!(
"[Notifier] Got option {}: {}",
response.key().to_string(),
response.value().to_string(),
);
}
async fn set_option<T: OptionType>(&self, response: &SetOptionResponse<T>) {
tracing::debug!(
"[Notifier] Set option {} to {}",
response.key().to_string(),
response.value().to_string(),
);
}
async fn deleted_option(&self, response: &DeleteOptionResponse) {
tracing::debug!("[Notifier] Deleted option {}", response.key().to_string());
}
}

View File

@@ -48,7 +48,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let user = self
.create_user(
@@ -72,7 +72,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let user = self
.create_or_update_user(
@@ -93,7 +93,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let user = self
.edit_user(
@@ -115,7 +115,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
self.delete_user_from_database(&mut connection, request.user_id())
.await
@@ -136,7 +136,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let user = self
.get_user_from_email(&mut connection, request.email())
@@ -166,7 +166,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let session = self
.create_session(&mut connection, request.user(), request.expiration())
@@ -184,7 +184,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let session = self
.get_auth_session(&mut connection, request.session_id())
@@ -212,7 +212,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let user_warren = self
.add_user_to_warren(&mut connection, request.user_warren())
@@ -230,7 +230,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let user_warren = self
.update_user_warren(&mut connection, request.user_warren())
@@ -248,7 +248,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let user_warren = self
.remove_user_from_warren(&mut connection, request.user_id(), request.warren_id())
@@ -272,7 +272,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let user_warrens = self
.get_user_warrens(&mut connection, request.user_id())
@@ -290,7 +290,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let user_warrens = self
.get_all_user_warrens(&mut connection)
@@ -308,7 +308,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
self.get_user_warren(&mut connection, request.user_id(), request.warren_id())
.await
@@ -326,7 +326,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let users = self
.fetch_users(&mut connection)
@@ -345,7 +345,7 @@ impl AuthRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let users = self
.fetch_users(&mut connection)
@@ -402,6 +402,7 @@ impl Sqlite {
let user: User = sqlx::query_as(
"INSERT INTO users (
id,
name,
email,
hash,
@@ -411,12 +412,14 @@ impl Sqlite {
$1,
$2,
$3,
$4
$4,
$5
)
RETURNING
*
",
)
.bind(Uuid::new_v4())
.bind(name)
.bind(email)
.bind(password_hash)

View File

@@ -6,6 +6,7 @@ use sqlx::{
};
use tokio::task::JoinHandle;
pub mod auth;
pub mod options;
pub mod share;
pub mod warrens;
@@ -29,10 +30,6 @@ impl Sqlite {
pub async fn new(config: SqliteConfig) -> anyhow::Result<Self> {
let opts = SqliteConnectOptions::from_str(&config.database_url)?
.create_if_missing(true)
.extension_with_entrypoint(
"/var/lib/warren/sqlite_extensions/uuid",
"sqlite3_uuid_init",
)
.disable_statement_logging();
let pool = SqlitePoolOptions::new().connect_with(opts).await?;

View File

@@ -0,0 +1,134 @@
use anyhow::Context;
use sqlx::FromRow;
use crate::domain::warren::{
models::option::{
DeleteOptionError, DeleteOptionRequest, DeleteOptionResponse, GetOptionError,
GetOptionRequest, GetOptionResponse, OptionKey, OptionType, SetOptionError,
SetOptionRequest, SetOptionResponse,
},
ports::OptionRepository,
};
use super::{Sqlite, is_not_found_error};
#[derive(Debug, FromRow)]
struct OptionRow {
key: String,
value: String,
}
impl OptionRepository for Sqlite {
async fn get_option<T: OptionType>(
&self,
request: GetOptionRequest,
) -> Result<GetOptionResponse<T>, GetOptionError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a Sqlite connection")?;
let key: OptionKey = request.into();
let row: OptionRow = sqlx::query_as(
"
SELECT
key,
value
FROM
application_options
WHERE
key = $1",
)
.bind(key.as_str())
.fetch_one(&mut *connection)
.await
.map_err(|e| {
if is_not_found_error(&e) {
GetOptionError::NotFound(key)
} else {
GetOptionError::Unknown(e.into())
}
})?;
let parsed = T::parse(&row.value).map_err(|_| GetOptionError::Parse)?;
Ok(GetOptionResponse::new(
OptionKey::new(&row.key).unwrap(),
parsed,
))
}
async fn set_option<T: OptionType>(
&self,
request: SetOptionRequest<T>,
) -> Result<SetOptionResponse<T>, SetOptionError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a Sqlite connection")?;
let (key, value) = request.unpack();
sqlx::query_as::<_, OptionRow>(
"
INSERT INTO application_options (
key,
value
) VALUES (
$1,
$2
)
RETURNING
key,
value
",
)
.bind(key.as_str())
.bind(value.inner().to_string())
.fetch_one(&mut *connection)
.await
.map_err(|e| SetOptionError::Unknown(e.into()))?;
Ok(SetOptionResponse::new(key, value.get_inner()))
}
async fn delete_option(
&self,
request: DeleteOptionRequest,
) -> Result<DeleteOptionResponse, DeleteOptionError> {
let mut connection = self
.pool
.acquire()
.await
.context("Failed to get a Sqlite connection")?;
let key: OptionKey = request.into();
sqlx::query_as::<_, OptionRow>(
"
DELETE FROM
application_options
WHERE
key = $1
RETURNING
key,
value
",
)
.bind(key.as_str())
.fetch_one(&mut *connection)
.await
.map_err(|e| {
if is_not_found_error(&e) {
DeleteOptionError::NotFound(key.clone())
} else {
DeleteOptionError::Unknown(e.into())
}
})?;
Ok(DeleteOptionResponse::new(key))
}
}

View File

@@ -154,6 +154,7 @@ pub(super) async fn create_share(
let share: ShareRow = sqlx::query_as(
"
INSERT INTO shares (
id,
creator_id,
warren_id,
path,
@@ -164,12 +165,14 @@ pub(super) async fn create_share(
$2,
$3,
$4,
datetime($5, 'unixepoch')
$5,
datetime($6, 'unixepoch')
)
RETURNING
*
",
)
.bind(Uuid::new_v4())
.bind(request.creator_id())
.bind(request.warren_id())
.bind(request.base().path())

View File

@@ -32,7 +32,7 @@ impl WarrenRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let warren = self
.create_warren(&mut connection, request.name(), request.path())
@@ -47,7 +47,7 @@ impl WarrenRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let warren = self
.edit_warren(
@@ -70,7 +70,7 @@ impl WarrenRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let warren = self
.delete_warren(&mut connection, request.id())
@@ -88,7 +88,7 @@ impl WarrenRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let warrens = self
.fetch_warrens(&mut connection, request.ids())
@@ -106,7 +106,7 @@ impl WarrenRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let warrens = self
.fetch_all_warrens(&mut connection)
@@ -121,7 +121,7 @@ impl WarrenRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let warren = self
.get_warren(&mut connection, request.id())
@@ -144,7 +144,7 @@ impl WarrenRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
super::share::get_share(&mut connection, request)
.await
@@ -159,7 +159,7 @@ impl WarrenRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
super::share::create_share(&mut connection, request)
.await
@@ -177,7 +177,7 @@ impl WarrenRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
let path = request.path().clone();
@@ -195,7 +195,7 @@ impl WarrenRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
super::share::delete_share(&mut connection, request)
.await
@@ -211,7 +211,7 @@ impl WarrenRepository for Sqlite {
.pool
.acquire()
.await
.context("Failed to get a PostgreSQL connection")?;
.context("Failed to get a Sqlite connection")?;
super::share::verify_password(&mut connection, request)
.await
@@ -232,16 +232,19 @@ impl Sqlite {
let warren: Warren = sqlx::query_as(
"
INSERT INTO warrens (
id,
name,
path
) VALUES (
$1,
$2
$2,
$3
)
RETURNING
*
",
)
.bind(Uuid::new_v4())
.bind(name)
.bind(path)
.fetch_one(&mut *tx)

Binary file not shown.

View File

@@ -11,7 +11,7 @@ services:
environment:
- 'SERVER_ADDRESS=0.0.0.0'
- 'SERVER_PORT=8080'
- 'DATABASE_URL=sqlite:///var/lib/warren/warren.db'
- 'DATABASE_URL=sqlite:///var/lib/warren/data/warren.db'
- 'SERVE_DIRECTORY=/serve'
- 'CORS_ALLOW_ORIGIN=http://localhost:8081'
- 'LOG_LEVEL=debug'
@@ -19,7 +19,7 @@ services:
- 'ZIP_READ_BUFFER_BYTES=4096'
volumes:
- './backend/serve:/serve:rw'
- './backend/warren.db:/var/lib/warren/warren.db:rw'
- './backend/data:/var/lib/warren/data:rw'
networks:
warren-net:
name: 'warren-net'