Pārlūkot izejas kodu

Remove config storage from database and move to external TOML file.

Kestrel 10 mēneši atpakaļ
vecāks
revīzija
a744a24ec7
13 mainītis faili ar 124 papildinājumiem un 754 dzēšanām
  1. 4 0
      .cargo/config.toml
  2. 3 2
      Cargo.lock
  3. 1 1
      Cargo.toml
  4. 15 0
      Containerfile
  5. 13 12
      README.md
  6. 25 22
      src/cli.rs
  7. 8 65
      src/config.rs
  8. 0 621
      src/config/helper.rs
  9. 5 1
      src/main.rs
  10. 39 28
      src/schema.rs
  11. 9 1
      src/server/oidc.rs
  12. 0 1
      src/server/session.rs
  13. 2 0
      uidc.toml

+ 4 - 0
.cargo/config.toml

@@ -0,0 +1,4 @@
+[target.aarch64-unknown-linux-gnu]
+linker = "aarch64-linux-gnu-gcc" 
+[target.aarch64-unknown-linux-musl]
+linker = "aarch64-linux-musl-gcc" 

+ 3 - 2
Cargo.lock

@@ -1356,6 +1356,7 @@ version = "0.28.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
 dependencies = [
+ "cc",
  "pkg-config",
  "vcpkg",
 ]
@@ -1400,9 +1401,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
 
 [[package]]
 name = "microrm"
-version = "0.4.0"
+version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3622a907b71b6b3a9133ecccf67cd0d4539fad7ba60653557cae5335b86d8b3d"
+checksum = "54994d17404c1d372b867d24a60f455dc1cfd9a80cb938016ddc9a96365f64c9"
 dependencies = [
  "clap",
  "itertools",

+ 1 - 1
Cargo.toml

@@ -28,7 +28,7 @@ bincode = "1.3"
 toml = "0.8.2"
 
 # Data storage dependencies
-microrm = { version = "0.4.0", features = ["clap"] }
+microrm = { version = "0.4.1", features = ["clap", "bundled_sqlite"] }
 serde_bytes = { version = "0.11.6" }
 
 # Public API/server dependencies

+ 15 - 0
Containerfile

@@ -0,0 +1,15 @@
+FROM arm64v8/busybox:musl
+
+ENV UIDC_DB /data/uidc.db
+EXPOSE 8080
+
+VOLUME /data
+RUN mkdir /uidc
+
+COPY target/aarch64-unknown-linux-musl/release/uidc /uidc/
+COPY static/ /uidc/static/
+COPY tmpl/ /uidc/tmpl/
+
+WORKDIR /uidc/
+CMD ["server", "--bind", "0.0.0.0", "--port", "8080"]
+ENTRYPOINT ["/uidc/uidc"]

+ 13 - 12
README.md

@@ -18,7 +18,7 @@ authentication flows, look elsewhere. But, if you want:
 - lightweight (runtime memory usage is less than 20MB, even under moderate load, and CPU usage is minimal)
 - simple (single statically-linked executable and minimal supporting files)
 - ready to use out of the box (as long as your authentication needs fall into the first 95% of use cases)
-- easily configured (configuration is mostly done via CLI, with tab-completion available, and designed for interactive use)
+- easily configured (configuration is done via a single TOML file and a CLI with tab completion)
 - flexible, within limits (implements generic role-based access control)
 
 ... then uidc might be what you're looking for.
@@ -29,25 +29,26 @@ authentication flows, look elsewhere. But, if you want:
 
 The general format of a `uidc` invocation runs something like the following:
 ```shell
-./path/to/uidc --db $PATH_TO_DB <noun> <verb> <options>
+./path/to/uidc --config-path $PATH_TO_CONFIG <noun> <verb> <options>
 ```
 
-If a database is not explicitly passed via `--db`, it will default to using a
-file called `uidc.db` in the current directory; you can also set the
-environment variable `UIDC_DB` if that's more convenient. The database path is
+If a config file is not explicitly passed via `--config-path`, it will default to using a
+file called `uidc.toml` in the current directory; you can also set the
+environment variable `UIDC_CONFIG` if that's more convenient. The config path is
 elided from the following examples for brevity.
 
 #### Initial setup ####
 
-First thing is to initialize the database and create a signing key:
-```shell
-$UIDC init
-$UIDC key generate
+Start with a very simple configuration file, something like the following:
+```toml
+db_path = "uidc.db"
+base_url = "https://externally-visible-url"
 ```
 
-Next, set some basic information:
+Then initialize the database and create a signing key:
 ```shell
-$UIDC config set base_url "https://externally-visible-url"
+$UIDC init
+$UIDC key generate rsa2048
 ```
 
 Create yourself a user and add a password (and 2FA if you want, by passing a `-pt` instead of `-p`):
@@ -66,7 +67,7 @@ And then run the server! By default, it listens on port 2114, but that can be
 changed with `--port`.
 
 ```shell
-$UIDC server
+$UIDC serve
 ```
 
 #### Realms ####

+ 25 - 22
src/cli.rs

@@ -1,5 +1,5 @@
 use crate::{
-    config,
+    config::Config,
     key::{self, KeyType},
     realm::RealmHelper,
     schema::{self, UIDCDatabase},
@@ -24,12 +24,12 @@ impl microrm::cli::CLIError for UIDCError {
 #[derive(Debug, Parser)]
 #[clap(author, version, about, long_about = None)]
 struct RootArgs {
-    #[clap(short, long, env = "UIDC_DB", default_value_t = String::from("uidc.db"))]
-    /// Database path
-    db: String,
+    #[clap(short, long, env = "UIDC_CONFIG", default_value_t = String::from("uidc.toml"))]
+    /// Configuration file path
+    config_path: String,
 
     #[clap(short, long, default_value_t = String::from("primary"))]
-    /// Which realm to use, for non-server only
+    /// Which realm to use, for non-serve commands only
     realm: String,
 
     #[clap(subcommand)]
@@ -45,8 +45,8 @@ enum Command {
         #[clap(subcommand)]
         cmd: Autogenerate<client::ClientInterface>,
     },
-    /// general configuration
-    Config(ConfigArgs),
+    // /// general configuration
+    // Config(ConfigArgs),
     /// permissions grouping management
     Group {
         #[clap(subcommand)]
@@ -60,7 +60,7 @@ enum Command {
         cmd: Autogenerate<scope::ScopeInterface>,
     },
     /// run the actual OIDC server
-    Server(ServerArgs),
+    Serve(ServerArgs),
     /// manual token generation and inspection
     Token {
         #[clap(subcommand)]
@@ -79,17 +79,21 @@ enum Command {
 }
 
 struct RunArgs {
+    config: Config,
     db: UIDCDatabase,
     realm: Stored<schema::Realm>,
 }
 
 impl RootArgs {
     async fn run(self) -> Result<(), UIDCError> {
+        let config_contents = std::fs::read_to_string(self.config_path.as_str()).expect("couldn't open configuration file");
+        let config : Config = toml::from_str(config_contents.as_str()).expect("couldn't parse configuration file");
+
         if let Command::Init = self.command {
-            return self.init().await;
+            return self.init(config).await;
         }
 
-        let db = UIDCDatabase::open_path(&self.db)
+        let db = UIDCDatabase::open_path(&config.db_path)
             .map_err(|e| UIDCError::AbortString(format!("Error accessing database: {:?}", e)))?;
 
         let realm = db
@@ -98,25 +102,25 @@ impl RootArgs {
             .get()?
             .ok_or(UIDCError::Abort("no such realm"))?;
 
-        let ra = RunArgs { db, realm };
+        let ra = RunArgs { config, db, realm };
 
         match self.command {
             Command::Init => unreachable!(),
-            Command::Config(v) => v.run(ra).await,
+            // Command::Config(v) => v.run(ra).await,
             Command::Key(v) => v.run(ra).await,
             Command::Client { cmd } => cmd.perform(&ra.realm, &ra.realm.clients),
             Command::Scope { cmd } => cmd.perform(&ra.realm, &ra.realm.scopes),
             Command::Group { cmd } => cmd.perform(&ra.realm, &ra.realm.groups),
-            Command::Server(v) => v.run(ra).await,
+            Command::Serve(v) => v.run(ra).await,
             Command::Token { cmd } => cmd.run(ra).await,
             Command::Role { cmd } => cmd.perform(&ra.realm, &ra.realm.roles),
             Command::User { cmd } => cmd.perform(&ra.realm, &ra.realm.users),
         }
     }
 
-    async fn init(&self) -> Result<(), UIDCError> {
+    async fn init(&self, config: Config) -> Result<(), UIDCError> {
         // first check to see if the database is already vaguely set up
-        let db = UIDCDatabase::open_path(&self.db)
+        let db = UIDCDatabase::open_path(&config.db_path)
             .map_err(|e| UIDCError::AbortString(format!("Error accessing database: {:?}", e)))?;
 
         log::info!("Initializing!");
@@ -176,6 +180,7 @@ impl KeyArgs {
     }
 }
 
+/*
 #[derive(Debug, Subcommand)]
 enum ConfigCommand {
     Dump,
@@ -211,6 +216,7 @@ impl ConfigArgs {
         Ok(())
     }
 }
+*/
 
 #[derive(Debug, Parser)]
 struct ServerArgs {
@@ -222,10 +228,9 @@ struct ServerArgs {
 
 impl ServerArgs {
     async fn run(self, args: RunArgs) -> Result<(), UIDCError> {
-        let config = config::Config::build_from(&args.db, None);
         server::run_server(
             args.db,
-            config,
+            args.config,
             self.bind.as_deref().unwrap_or("127.0.0.1"),
             self.port.unwrap_or(2114),
         )
@@ -258,8 +263,6 @@ enum TokenCommand {
 
 impl TokenCommand {
     async fn run(self, args: RunArgs) -> Result<(), UIDCError> {
-        let config = config::Config::build_from(&args.db, None);
-
         let get_stored = |client_name: &str, user_name: &str| {
             let stored_client = args
                 .realm
@@ -285,7 +288,7 @@ impl TokenCommand {
                 scopes,
             } => {
                 let (stored_client, stored_user) = get_stored(client.as_str(), username.as_str())?;
-                let realm = RealmHelper::new(config, args.realm);
+                let realm = RealmHelper::new(args.config, args.realm);
                 let token =
                     realm.generate_access_token(&stored_client, &stored_user, scopes.split(' '))?;
                 println!("{}", token);
@@ -297,7 +300,7 @@ impl TokenCommand {
                 scopes,
             } => {
                 let (stored_client, stored_user) = get_stored(client.as_str(), username.as_str())?;
-                let realm = RealmHelper::new(config, args.realm);
+                let realm = RealmHelper::new(args.config, args.realm);
                 let token = realm.generate_refresh_token(
                     &stored_client,
                     &stored_user,
@@ -306,7 +309,7 @@ impl TokenCommand {
                 println!("{}", token);
                 Ok(())
             }
-            TokenCommand::Inspect { token } => {
+            TokenCommand::Inspect { token: _ } => {
                 todo!()
                 // token_management::inspect_token(&config, &args.realm, token.as_ref())
             }

+ 8 - 65
src/config.rs

@@ -1,8 +1,10 @@
-use crate::schema;
-use microrm::prelude::*;
 use serde::{Deserialize, Serialize};
 
-mod helper;
+#[derive(Debug, Clone, Serialize, Deserialize)]
+pub struct GithubConfig {
+    pub client_id: String,
+    pub client_secret: String,
+}
 
 fn default_auth_token_expiry() -> u64 {
     600
@@ -18,6 +20,8 @@ fn default_refresh_token_expiry() -> u64 {
 
 #[derive(Debug, Clone, Serialize, Deserialize)]
 pub struct Config {
+    pub db_path: String,
+
     pub base_url: String,
 
     #[serde(default = "default_auth_token_expiry")]
@@ -28,67 +32,6 @@ pub struct Config {
 
     #[serde(default = "default_refresh_token_expiry")]
     pub refresh_token_expiry: u64,
-}
-
-impl Config {
-    pub fn build_from(db: &schema::UIDCDatabase, cfile: Option<&str>) -> Self {
-        let mut config_map = std::collections::HashMap::<String, String>::new();
-        // load config keys from database
-        let db_pcs = db
-            .persistent_config
-            .get()
-            .expect("could't get config keys from database");
-        config_map.extend(db_pcs.into_iter().map(
-            |pc: microrm::schema::Stored<schema::PersistentConfig>| {
-                let pc = pc.wrapped();
-                (pc.key, pc.value)
-            },
-        ));
-
-        if let Some(path) = cfile {
-            match std::fs::read(path) {
-                Ok(data) => {
-                    log::info!("Loading config from {path}...");
-                    let toml_table: toml::Table = toml::from_str(
-                        std::str::from_utf8(data.as_slice())
-                            .expect("couldn't read config file contents as utf-8"),
-                    )
-                    .expect("couldn't parse config toml");
-
-                    for val in toml_table {
-                        log::trace!("using config key {} from TOML config...", val.0);
-                        if val.1.is_str() {
-                            config_map.insert(val.0, val.1.as_str().unwrap().to_string());
-                        } else {
-                            config_map.insert(val.0, val.1.to_string());
-                        }
-                    }
-                }
-                Err(e) => {
-                    log::error!("Could not open {path} for reading: {e}");
-                }
-            }
-        }
-
-        let mut deser = helper::ConfigDeserializer {
-            config_map: &config_map,
-            prefix: "".to_string(),
-        };
-
-        let config = Config::deserialize(&mut deser).expect("couldn't load configuration");
-
-        log::trace!("final configuration: {:?}", config);
-
-        config
-    }
-
-    pub fn save<'config>(&'config self, db: &'config schema::UIDCDatabase) {
-        let ser = helper::ConfigSerializer {
-            config: self,
-            db,
-            prefix: String::new(),
-        };
 
-        let _ = self.serialize(&ser);
-    }
+    pub github: Option<GithubConfig>,
 }

+ 0 - 621
src/config/helper.rs

@@ -1,621 +0,0 @@
-#![allow(unused_variables)]
-
-use crate::schema;
-use microrm::prelude::*;
-
-use super::Config;
-
-struct ValueToStringSerializer {}
-
-impl<'l> serde::Serializer for &'l ValueToStringSerializer {
-    type Ok = Option<String>;
-    type Error = ConfigError;
-
-    type SerializeSeq = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeMap = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeTuple = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeStruct = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeTupleStruct = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeTupleVariant = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeStructVariant = serde::ser::Impossible<Self::Ok, Self::Error>;
-
-    fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error> {
-        self.serialize_i64(v as i64)
-    }
-    fn serialize_i16(self, v: i16) -> Result<Self::Ok, Self::Error> {
-        self.serialize_i64(v as i64)
-    }
-    fn serialize_i32(self, v: i32) -> Result<Self::Ok, Self::Error> {
-        self.serialize_i64(v as i64)
-    }
-    fn serialize_i64(self, v: i64) -> Result<Self::Ok, Self::Error> {
-        Ok(Some(v.to_string()))
-    }
-    fn serialize_i128(self, v: i128) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error> {
-        self.serialize_u64(v as u64)
-    }
-    fn serialize_u16(self, v: u16) -> Result<Self::Ok, Self::Error> {
-        self.serialize_u64(v as u64)
-    }
-    fn serialize_u32(self, v: u32) -> Result<Self::Ok, Self::Error> {
-        self.serialize_u64(v as u64)
-    }
-    fn serialize_u64(self, v: u64) -> Result<Self::Ok, Self::Error> {
-        Ok(Some(v.to_string()))
-    }
-    fn serialize_u128(self, v: u128) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_f32(self, v: f32) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-    fn serialize_f64(self, v: f64) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
-        Ok(Some(v.into()))
-    }
-
-    fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
-        Ok(None)
-    }
-
-    fn serialize_some<T: ?Sized + serde::Serialize>(
-        self,
-        value: &T,
-    ) -> Result<Self::Ok, Self::Error> {
-        value.serialize(self)
-    }
-
-    fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
-        unreachable!()
-    }
-    fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
-        unreachable!()
-    }
-    fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
-        unreachable!()
-    }
-    fn serialize_struct(
-        self,
-        _name: &'static str,
-        _len: usize,
-    ) -> Result<Self::SerializeStruct, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_unit_variant(
-        self,
-        name: &'static str,
-        variant_index: u32,
-        variant: &'static str,
-    ) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_tuple_struct(
-        self,
-        name: &'static str,
-        len: usize,
-    ) -> Result<Self::SerializeTupleStruct, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_tuple_variant(
-        self,
-        name: &'static str,
-        variant_index: u32,
-        variant: &'static str,
-        len: usize,
-    ) -> Result<Self::SerializeTupleVariant, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_newtype_struct<T: ?Sized + serde::Serialize>(
-        self,
-        name: &'static str,
-        value: &T,
-    ) -> Result<Self::Ok, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_struct_variant(
-        self,
-        name: &'static str,
-        variant_index: u32,
-        variant: &'static str,
-        len: usize,
-    ) -> Result<Self::SerializeStructVariant, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_newtype_variant<T: ?Sized + serde::Serialize>(
-        self,
-        name: &'static str,
-        variant_index: u32,
-        variant: &'static str,
-        value: &T,
-    ) -> Result<Self::Ok, Self::Error> {
-        todo!()
-    }
-}
-
-pub struct ConfigSerializer<'r, 's> {
-    pub config: &'r Config,
-    pub db: &'s schema::UIDCDatabase,
-    pub prefix: String,
-}
-
-impl<'r, 's> ConfigSerializer<'r, 's> {
-    fn update(&self, key: &str, value: String) {
-        // TODO: delete old config value
-        // self.db.persistent_config.delete(schema::PersistentConfig { key: key.into(), value }).expect("couldn't update config");
-        self.db
-            .persistent_config
-            .insert(schema::PersistentConfig {
-                key: key.into(),
-                value,
-            })
-            .expect("couldn't update config");
-    }
-}
-
-impl<'r, 's> serde::ser::SerializeStruct for ConfigSerializer<'r, 's> {
-    type Ok = ();
-    type Error = ConfigError;
-
-    fn serialize_field<T: ?Sized + serde::Serialize>(
-        &mut self,
-        key: &'static str,
-        value: &T,
-    ) -> Result<(), Self::Error> {
-        let key = format!("{}{}", self.prefix, key);
-
-        let value = value.serialize(&ValueToStringSerializer {})?;
-        if let Some(value) = value {
-            log::trace!("saving config {} = {}", key, value);
-            self.update(key.as_str(), value);
-        }
-
-        Ok(())
-    }
-
-    fn end(self) -> Result<Self::Ok, Self::Error> {
-        Ok(())
-    }
-}
-
-impl<'r, 's> serde::Serializer for &'r ConfigSerializer<'r, 's> {
-    type Ok = ();
-    type Error = ConfigError;
-
-    type SerializeSeq = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeMap = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeTuple = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeStruct = ConfigSerializer<'r, 's>;
-    type SerializeTupleStruct = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeTupleVariant = serde::ser::Impossible<Self::Ok, Self::Error>;
-    type SerializeStructVariant = serde::ser::Impossible<Self::Ok, Self::Error>;
-
-    fn serialize_bool(self, v: bool) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_i8(self, v: i8) -> Result<Self::Ok, Self::Error> {
-        self.serialize_i64(v as i64)
-    }
-    fn serialize_i16(self, v: i16) -> Result<Self::Ok, Self::Error> {
-        self.serialize_i64(v as i64)
-    }
-    fn serialize_i32(self, v: i32) -> Result<Self::Ok, Self::Error> {
-        self.serialize_i64(v as i64)
-    }
-    fn serialize_i64(self, v: i64) -> Result<Self::Ok, Self::Error> {
-        todo!()
-    }
-    fn serialize_i128(self, v: i128) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error> {
-        self.serialize_u64(v as u64)
-    }
-    fn serialize_u16(self, v: u16) -> Result<Self::Ok, Self::Error> {
-        self.serialize_u64(v as u64)
-    }
-    fn serialize_u32(self, v: u32) -> Result<Self::Ok, Self::Error> {
-        self.serialize_u64(v as u64)
-    }
-    fn serialize_u64(self, v: u64) -> Result<Self::Ok, Self::Error> {
-        todo!()
-    }
-    fn serialize_u128(self, v: u128) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_f32(self, v: f32) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-    fn serialize_f64(self, v: f64) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
-        Ok(())
-    }
-
-    fn serialize_some<T: ?Sized + serde::Serialize>(
-        self,
-        value: &T,
-    ) -> Result<Self::Ok, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
-        unreachable!()
-    }
-
-    fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
-        todo!()
-    }
-    fn serialize_map(self, len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
-        todo!()
-    }
-    fn serialize_tuple(self, len: usize) -> Result<Self::SerializeTuple, Self::Error> {
-        todo!()
-    }
-    fn serialize_struct(
-        self,
-        name: &'static str,
-        _len: usize,
-    ) -> Result<Self::SerializeStruct, Self::Error> {
-        log::trace!("name: {name}");
-
-        let new_prefix =
-            // are we at the root?
-            if name == "Config" {
-                String::new()
-            }
-            else {
-                format!("{}{}.", self.prefix, name)
-            };
-
-        let subser = ConfigSerializer {
-            config: self.config,
-            db: self.db,
-            prefix: new_prefix,
-        };
-
-        Ok(subser)
-    }
-
-    fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_unit_variant(
-        self,
-        name: &'static str,
-        variant_index: u32,
-        variant: &'static str,
-    ) -> Result<Self::Ok, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_tuple_struct(
-        self,
-        name: &'static str,
-        len: usize,
-    ) -> Result<Self::SerializeTupleStruct, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_tuple_variant(
-        self,
-        name: &'static str,
-        variant_index: u32,
-        variant: &'static str,
-        len: usize,
-    ) -> Result<Self::SerializeTupleVariant, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_newtype_struct<T: ?Sized + serde::Serialize>(
-        self,
-        name: &'static str,
-        value: &T,
-    ) -> Result<Self::Ok, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_struct_variant(
-        self,
-        name: &'static str,
-        variant_index: u32,
-        variant: &'static str,
-        len: usize,
-    ) -> Result<Self::SerializeStructVariant, Self::Error> {
-        todo!()
-    }
-
-    fn serialize_newtype_variant<T: ?Sized + serde::Serialize>(
-        self,
-        name: &'static str,
-        variant_index: u32,
-        variant: &'static str,
-        value: &T,
-    ) -> Result<Self::Ok, Self::Error> {
-        todo!()
-    }
-}
-
-pub struct ConfigDeserializer<'de> {
-    pub config_map: &'de std::collections::HashMap<String, String>,
-    pub prefix: String,
-}
-
-#[derive(Debug)]
-pub enum ConfigError {
-    Missing(String),
-    InvalidType(String),
-    CustomError(String),
-}
-
-impl std::fmt::Display for ConfigError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            Self::Missing(what) => f.write_fmt(format_args!(
-                "Missing required config entry: {}",
-                what.as_str()
-            )),
-            Self::InvalidType(what) => f.write_fmt(format_args!(
-                "Could not parse config entry '{}'",
-                what.as_str()
-            )),
-            Self::CustomError(what) => {
-                f.write_fmt(format_args!("Custom error '{}'", what.as_str()))
-            }
-        }
-    }
-}
-
-impl std::error::Error for ConfigError {}
-
-impl serde::ser::Error for ConfigError {
-    fn custom<T>(msg: T) -> Self
-    where
-        T: std::fmt::Display,
-    {
-        Self::CustomError(msg.to_string())
-    }
-}
-
-impl serde::de::Error for ConfigError {
-    fn custom<T>(msg: T) -> Self
-    where
-        T: std::fmt::Display,
-    {
-        Self::CustomError(msg.to_string())
-    }
-
-    fn invalid_type(_unexp: serde::de::Unexpected, _exp: &dyn serde::de::Expected) -> Self {
-        Self::InvalidType("".into())
-    }
-
-    fn missing_field(field: &'static str) -> Self {
-        Self::Missing(field.into())
-    }
-}
-
-impl<'de> serde::Deserializer<'de> for &'de mut ConfigDeserializer<'de> {
-    type Error = ConfigError;
-
-    fn deserialize_any<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        unreachable!("deserialize_any needs context")
-    }
-
-    fn deserialize_struct<V>(
-        self,
-        _name: &'static str,
-        _fields: &'static [&'static str],
-        visitor: V,
-    ) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        self.deserialize_map(visitor)
-    }
-
-    fn deserialize_seq<V>(self, _visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        todo!("deserialize_seq")
-    }
-
-    fn deserialize_map<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        let mut map_access = ConfigDeserializerIterator {
-            it: self
-                .config_map
-                .iter()
-                .filter(|e| {
-                    e.0.starts_with(&self.prefix) && !e.0[self.prefix.len()..].contains('.')
-                })
-                .peekable(),
-        };
-
-        visitor.visit_map(&mut map_access)
-    }
-
-    fn deserialize_enum<V>(
-        self,
-        _name: &'static str,
-        _variants: &'static [&'static str],
-        _visitor: V,
-    ) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        todo!("deserialize_enum")
-    }
-
-    fn deserialize_tuple<V>(self, _len: usize, _visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        todo!("deserialize_tuple")
-    }
-
-    fn deserialize_tuple_struct<V>(
-        self,
-        _name: &'static str,
-        _len: usize,
-        _visitor: V,
-    ) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        todo!("deserialize_tuple_struct")
-    }
-
-    serde::forward_to_deserialize_any!(
-        i8 u8 i16 u16 i32 u32 i64 u64 i128 u128 str string bytes
-        bool f32 f64 char byte_buf option unit unit_struct
-        newtype_struct identifier ignored_any
-    );
-}
-
-struct AtomicForwarder<'de> {
-    to_fwd: &'de str,
-}
-
-impl<'de> serde::Deserializer<'de> for AtomicForwarder<'de> {
-    type Error = ConfigError;
-    fn deserialize_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_unit()
-    }
-
-    fn deserialize_u64<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_u64(
-            self.to_fwd
-                .parse()
-                .map_err(|_| ConfigError::InvalidType(String::new()))?,
-        )
-    }
-
-    fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_str(self.to_fwd)
-    }
-
-    fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_str(self.to_fwd)
-    }
-
-    fn deserialize_identifier<V>(self, visitor: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::Visitor<'de>,
-    {
-        visitor.visit_str(self.to_fwd)
-    }
-
-    serde::forward_to_deserialize_any!(
-        i8 u8 i16 u16 i32 u32 i64 i128 u128 bytes
-        bool f32 f64 char byte_buf unit unit_struct option
-        newtype_struct ignored_any struct tuple tuple_struct
-        seq map enum
-    );
-}
-
-struct ConfigDeserializerIterator<'de, I: Iterator<Item = (&'de String, &'de String)>> {
-    it: std::iter::Peekable<I>,
-}
-
-impl<'de, I: Iterator<Item = (&'de String, &'de String)>> serde::de::MapAccess<'de>
-    for ConfigDeserializerIterator<'de, I>
-{
-    type Error = ConfigError;
-
-    fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Self::Error>
-    where
-        K: serde::de::DeserializeSeed<'de>,
-    {
-        if let Some(e) = self.it.peek() {
-            let de = AtomicForwarder {
-                to_fwd: e.0.as_str(),
-            };
-            Ok(seed.deserialize(de).ok())
-        } else {
-            Ok(None)
-        }
-    }
-
-    fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
-    where
-        V: serde::de::DeserializeSeed<'de>,
-    {
-        let value = self.it.next().unwrap();
-
-        let de = AtomicForwarder {
-            to_fwd: value.1.as_str(),
-        };
-
-        seed.deserialize(de)
-            .map_err(|e| ConfigError::InvalidType(e.to_string()))
-    }
-}

+ 5 - 1
src/main.rs

@@ -14,7 +14,11 @@ mod user;
 pub use error::UIDCError;
 
 fn main() {
-    pretty_env_logger::init_timed();
+    pretty_env_logger::formatted_timed_builder()
+        .filter_level(log::LevelFilter::Warn)
+        .filter(Some(module_path!()), log::LevelFilter::Info)
+        .parse_default_env()
+        .init();
 
     cli::invoked();
 }

+ 39 - 28
src/schema.rs

@@ -3,18 +3,6 @@ use serde::{Deserialize, Serialize};
 
 use crate::key::KeyType;
 
-// ----------------------------------------------------------------------
-// uidc internal types
-// ----------------------------------------------------------------------
-
-/// Simple key-value store for persistent configuration
-#[derive(Entity)]
-pub struct PersistentConfig {
-    #[key]
-    pub key: String,
-    pub value: String,
-}
-
 // ----------------------------------------------------------------------
 // Session types
 // ----------------------------------------------------------------------
@@ -77,20 +65,6 @@ impl microrm::Relation for GroupRoleRelation {
     const NAME: &'static str = "GroupRole";
 }
 
-#[derive(Clone, Default, Entity)]
-pub struct Realm {
-    #[key]
-    pub shortname: String,
-
-    pub clients: microrm::RelationMap<Client>,
-    pub groups: microrm::RelationMap<Group>,
-    pub keys: microrm::RelationMap<Key>,
-    pub roles: microrm::RelationMap<Role>,
-    pub scopes: microrm::RelationMap<Scope>,
-    pub users: microrm::RelationMap<User>,
-    pub auth_codes: microrm::RelationMap<AuthCode>,
-}
-
 #[derive(serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
 pub enum KeyState {
     /// Key can be used without restrictions for signing and verification.
@@ -194,10 +168,47 @@ pub struct Scope {
     pub roles: microrm::RelationMap<Role>,
 }
 
+// ----------------------------------------------------------------------
+// External (social) authentication
+// ----------------------------------------------------------------------
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub enum ExternalAuthProvider {
+    Github,
+}
+
+#[derive(Clone, Entity)]
+pub struct ExternalAuthMap {
+    #[key]
+    pub external_user_id: String,
+    #[key]
+    pub provider: microrm::Serialized<ExternalAuthProvider>,
+
+    pub internal_user_id: UserID,
+}
+
+// ----------------------------------------------------------------------
+// Global container types
+// ----------------------------------------------------------------------
+
+#[derive(Clone, Default, Entity)]
+pub struct Realm {
+    #[key]
+    pub shortname: String,
+
+    pub clients: microrm::RelationMap<Client>,
+    pub groups: microrm::RelationMap<Group>,
+    pub keys: microrm::RelationMap<Key>,
+    pub roles: microrm::RelationMap<Role>,
+    pub scopes: microrm::RelationMap<Scope>,
+    pub users: microrm::RelationMap<User>,
+    pub auth_codes: microrm::RelationMap<AuthCode>,
+
+    pub external_auth: microrm::RelationMap<ExternalAuthMap>,
+}
+
 #[derive(Clone, Database)]
 pub struct UIDCDatabase {
-    pub persistent_config: microrm::IDMap<PersistentConfig>,
-
     pub realms: microrm::IDMap<Realm>,
 
     pub sessions: microrm::IDMap<Session>,

+ 9 - 1
src/server/oidc.rs

@@ -133,12 +133,20 @@ async fn jwks(request: Request) -> tide::Result<tide::Response> {
 
 async fn discovery_config(request: Request) -> tide::Result<tide::Response> {
     let server_config = &request.state().core.config;
+    let realm_name = request.param("realm").unwrap();
     let base_url = format!(
         "{}/{}",
         server_config.base_url,
-        request.param("realm").unwrap()
+        realm_name
     );
 
+    let Some(realm) = &request.state().core.db.realms.keyed(realm_name).first().get()? else {
+        return Ok(tide::Response::builder(404)
+            .header(tide::http::headers::ACCESS_CONTROL_ALLOW_ORIGIN, "*")
+            .body("No such realm")
+            .build())
+    };
+
     let config_response = serde_json::json!({
         "issuer": base_url,
         "authorization_endpoint": format!("{}/{}", base_url, AUTHORIZE_PATH),

+ 0 - 1
src/server/session.rs

@@ -39,7 +39,6 @@ impl<'l> SessionHelper<'l> {
             .expose();
         let session_id = base64::encode_config(session_id, base64::URL_SAFE_NO_PAD);
 
-        // XXX: replace with in-place insertion once support for that is added to microrm
         let session = self.db.sessions.insert_and_return(schema::Session {
             session_id: session_id.clone(),
             auth: Default::default(),

+ 2 - 0
uidc.toml

@@ -0,0 +1,2 @@
+db_path = "uidc.db"
+base_url = "http://localhost:2114"