aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore3
-rw-r--r--Cargo.lock134
-rw-r--r--crates/iam/Cargo.toml2
-rw-r--r--crates/iam/src/api.rs98
-rw-r--r--crates/iam/src/main.rs143
-rw-r--r--crates/secd/Cargo.toml6
-rw-r--r--crates/secd/build.rs3
-rw-r--r--crates/secd/src/auth/n.rs203
-rw-r--r--crates/secd/src/auth/z/graph.rs416
-rw-r--r--crates/secd/src/auth/z/mod.rs4
-rw-r--r--crates/secd/src/client/email/mod.rs18
-rw-r--r--crates/secd/src/client/spice/mod.rs21
-rw-r--r--crates/secd/src/client/store/mod.rs84
-rw-r--r--crates/secd/src/client/store/sql_db.rs102
-rw-r--r--crates/secd/src/lib.rs170
-rw-r--r--crates/secd/src/util/crypter.rs47
-rw-r--r--crates/secd/src/util/from.rs2
-rw-r--r--crates/secd/src/util/mod.rs275
-rw-r--r--crates/secd/store/pg/migrations/20221222002434_bootstrap.sql5
-rw-r--r--crates/secd/store/pg/sql/find_credential.sql2
-rw-r--r--crates/secd/store/pg/sql/find_identity.sql2
-rw-r--r--crates/secd/store/pg/sql/find_session.sql11
-rw-r--r--crates/secd/store/pg/sql/write_credential.sql4
-rw-r--r--crates/secd/store/pg/sql/write_identity.sql3
-rw-r--r--crates/secd/store/pg/sql/write_session.sql11
25 files changed, 1029 insertions, 740 deletions
diff --git a/.gitignore b/.gitignore
index 2c45399..ab62126 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
target/
-node_modules/ \ No newline at end of file
+node_modules/
+config.*toml \ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index d134d39..c9c6f8a 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -74,9 +74,9 @@ checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800"
[[package]]
name = "argon2"
-version = "0.4.1"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73"
+checksum = "95c2fcf79ad1932ac6269a738109997a83c227c09b75842ae564dc8ede6a861c"
dependencies = [
"base64ct",
"blake2",
@@ -480,6 +480,25 @@ dependencies = [
]
[[package]]
+name = "config"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7"
+dependencies = [
+ "async-trait",
+ "json5",
+ "lazy_static",
+ "nom",
+ "pathdiff",
+ "ron",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "toml",
+ "yaml-rust",
+]
+
+[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -704,6 +723,12 @@ dependencies = [
]
[[package]]
+name = "dlv-list"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
+
+[[package]]
name = "dotenvy"
version = "0.15.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1206,6 +1231,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"async-std",
+ "base64 0.21.0",
"clap",
"colored",
"email_address",
@@ -1219,6 +1245,7 @@ dependencies = [
"strum",
"strum_macros",
"thiserror",
+ "time",
"tiny_http",
"tokio",
"toml",
@@ -1349,6 +1376,17 @@ dependencies = [
]
[[package]]
+name = "json5"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1"
+dependencies = [
+ "pest",
+ "pest_derive",
+ "serde",
+]
+
+[[package]]
name = "kv-log-macro"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1413,6 +1451,12 @@ dependencies = [
]
[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
+[[package]]
name = "linux-raw-sys"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1604,6 +1648,16 @@ dependencies = [
]
[[package]]
+name = "ordered-multimap"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
+dependencies = [
+ "dlv-list",
+ "hashbrown",
+]
+
+[[package]]
name = "os_str_bytes"
version = "6.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1665,9 +1719,9 @@ dependencies = [
[[package]]
name = "password-hash"
-version = "0.4.2"
+version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700"
+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
dependencies = [
"base64ct",
"rand_core",
@@ -1681,6 +1735,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d01a5bd0424d00070b0098dd17ebca6f961a959dead1dbcbbbc1d1cd8d3deeba"
[[package]]
+name = "pathdiff"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd"
+
+[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1697,6 +1757,40 @@ dependencies = [
]
[[package]]
+name = "pest_derive"
+version = "2.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a81186863f3d0a27340815be8f2078dd8050b14cd71913db9fbda795e5f707d7"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75a1ef20bf3193c15ac345acb32e26b3dc3223aff4d77ae4fc5359567683796b"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e3b284b1f13a20dc5ebc90aff59a51b8d7137c221131b52a7260c08cbc1cc80"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha2",
+]
+
+[[package]]
name = "petgraph"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1982,6 +2076,27 @@ dependencies = [
]
[[package]]
+name = "ron"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a"
+dependencies = [
+ "base64 0.13.1",
+ "bitflags",
+ "serde",
+]
+
+[[package]]
+name = "rust-ini"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+]
+
+[[package]]
name = "rustc_version"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2045,7 +2160,9 @@ dependencies = [
"anyhow",
"argon2",
"async-trait",
+ "base64 0.21.0",
"clap",
+ "config",
"derive_more",
"email_address",
"glob",
@@ -3095,3 +3212,12 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d"
dependencies = [
"winapi",
]
+
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
diff --git a/crates/iam/Cargo.toml b/crates/iam/Cargo.toml
index 966f341..2ee6fc6 100644
--- a/crates/iam/Cargo.toml
+++ b/crates/iam/Cargo.toml
@@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
anyhow = "1.0"
async-std = { version = "1.12.0", features = [ "attributes" ] }
+base64 = "0.21.0"
clap = { version = "4.0.29", features = ["derive"] }
colored = "2.0.0"
email_address = "0.2"
@@ -18,6 +19,7 @@ serde = "1"
serde_json = { version = "1.0", features = ["raw_value"] }
strum = "0.24.1"
strum_macros = "0.24"
+time = { version = "0.3", features = [ "serde" ] }
tiny_http = "0.12"
tokio = { version = "1.23.0", features = ["full"] }
toml = "0.5.9"
diff --git a/crates/iam/src/api.rs b/crates/iam/src/api.rs
index af175a7..c662e0c 100644
--- a/crates/iam/src/api.rs
+++ b/crates/iam/src/api.rs
@@ -97,6 +97,14 @@ pub enum Command {
#[command(subcommand)]
object: UpdateObject,
},
+ #[command(
+ about = "Validate an IAM credential, optionally returning a session from the validation",
+ long_about = "Validate\n\nCredentials which have been created for identities may be validated and optionally exchanged for sessions."
+ )]
+ Validate {
+ #[command(subcommand)]
+ object: ValidateObject,
+ },
}
#[derive(Subcommand)]
@@ -248,11 +256,15 @@ pub enum DevObject {
#[derive(Subcommand)]
pub enum CredentialMethod {
- /// A
+ ApiToken {
+ #[arg(long)]
+ expires_at: Option<i64>,
+ },
+ /// Unique username and passphrase credential. Each username may have at most one Passprhase credential.
Passphrase {
- /// B
+ /// The username associated with this credential
username: String,
- /// C
+ /// The secret passphrase for this credential
passphrase: String,
},
}
@@ -274,60 +286,13 @@ pub enum ValidationMethod {
#[derive(Subcommand)]
pub enum GetObject {
- ApiKey {
- /// Public key associated with this api key set
- public_key: String,
- },
- Group {
- /// Unique group name
- name: String,
- /// Unique group id
- #[arg(long, short)]
- id: Option<Uuid>,
- },
Identity {
/// The unique id corresponding to this identity.
#[arg(long, short)]
identity_id: Option<Uuid>,
- /// Any session corresponding to this identity.
- #[arg(long, short)]
- session_token: Option<String>,
- },
- Permission {
- /// Unique permission name
- name: String,
- /// Unique permission id
- #[arg(long, short)]
- id: Option<Uuid>,
- },
- Role {
- /// Unique role name
- name: String,
- /// Unique role id
- #[arg(long, short)]
- id: Option<Uuid>,
- },
- Session {
- /// The plaintext token which uniquely identifies the session
- secret: String,
- },
- Service {
- /// Unique service name
- name: String,
- /// Unique service id
- #[arg(long, short)]
- id: Option<Uuid>,
- },
- ServiceAction {
- /// Unique service action name
- name: String,
- /// Unique service action id
- #[arg(long, short)]
- id: Option<Uuid>,
- },
- Validation {
- /// Unique validation request id
- id: Uuid,
+ /// The credential corresponding to this identity.
+ #[command(subcommand)]
+ credential: Option<ValidateObject>,
},
}
@@ -340,6 +305,33 @@ pub enum UpdateObject {
#[arg(long, short)]
metadata: Option<String>,
},
+ Credential {
+ /// Unique identifier for this credential.
+ /// Note: You can validate the credential to find it's id.
+ id: Uuid,
+ /// Whether to revoke this credential. Once revoked, the credential may no longer be used
+ /// and may not be un-revoked.
+ #[arg(long, short, action)]
+ revoke: bool,
+ },
+}
+
+#[derive(Subcommand)]
+pub enum ValidateObject {
+ ApiToken {
+ /// Api token to validate
+ token: String,
+ },
+ Passphrase {
+ /// The username associated with this credential
+ username: String,
+ /// The secret passphrase for this credential
+ passphrase: String,
+ },
+ Session {
+ /// The secret token associated with this session.
+ token: String,
+ },
}
#[derive(Serialize, Deserialize)]
diff --git a/crates/iam/src/main.rs b/crates/iam/src/main.rs
index c679af6..28f4e4c 100644
--- a/crates/iam/src/main.rs
+++ b/crates/iam/src/main.rs
@@ -2,19 +2,19 @@ mod api;
mod command;
mod util;
+use crate::api::ValidationMethod;
use anyhow::bail;
-use api::{AdminAction, Args, CliError, Command, CreateObject, DevObject, GetObject, UpdateObject};
+use api::{
+ AdminAction, Args, CliError, Command, CreateObject, DevObject, GetObject, UpdateObject,
+ ValidateObject,
+};
+
use clap::Parser;
use command::dev_oauth2_listen;
use env_logger::Env;
-use secd::{
- auth::z, Credential, CredentialType, Secd, ENV_AUTH_STORE_CONN_STRING, ENV_SPICE_SECRET,
- ENV_SPICE_SERVER,
-};
-use util::{error_detail, Result};
-use uuid::Uuid;
-
-use crate::api::ValidationMethod;
+use secd::{CredentialType, Secd};
+use time::OffsetDateTime;
+use util::Result;
const CONFIG_DIR_NAME: &str = "secdiam";
const CONFIG_PROFILE_FILE: &str = "profiles.toml";
@@ -49,15 +49,7 @@ async fn exec() -> Result<Option<String>> {
rest @ _ => {
// let cfg = util::read_config(args.profile).map_err(|_| CliError::InvalidProfile)?;
- std::env::set_var(
- ENV_AUTH_STORE_CONN_STRING,
- // "sqlite:///home/benj/.config/secdiam/34wxC.sql?mode=rwc",
- "postgresql://secduser:p4ssw0rd@localhost:5412/secd",
- );
- std::env::set_var(ENV_SPICE_SECRET, "sup3rs3cr3tk3y");
- std::env::set_var(ENV_SPICE_SERVER, "http://[::1]:50051");
-
- let secd = Secd::init(None)
+ let secd = Secd::init(None, None)
.await
.map_err(|e| CliError::SecdInitializationFailure(e.to_string()))?;
@@ -70,6 +62,7 @@ async fn exec() -> Result<Option<String>> {
unimplemented!()
}
Command::Update { object } => update(&secd, object).await?,
+ Command::Validate { object } => validate(&secd, object).await?,
}
}
})
@@ -101,18 +94,27 @@ async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> {
method,
identity_id,
} => {
- let t = match method {
+ let t = match &method {
api::CredentialMethod::Passphrase {
username,
passphrase,
} => CredentialType::Passphrase {
- key: username,
- value: passphrase,
+ key: username.clone(),
+ value: passphrase.clone(),
},
+ api::CredentialMethod::ApiToken { .. } => CredentialType::new_api_token()?,
+ };
+
+ let expires_at = match method {
+ api::CredentialMethod::ApiToken { expires_at, .. } => expires_at.map(|t| {
+ OffsetDateTime::from_unix_timestamp(t)
+ .expect("The provided value is an invalid unix timestamp")
+ }),
+ api::CredentialMethod::Passphrase { .. } => None,
};
- let credential = secd.create_credential(t, identity_id).await?;
- Some(serde_json::ser::to_string(&credential)?.to_string())
+ let credential = secd.create_credential(t, identity_id, expires_at).await?;
+ Some(serde_json::ser::to_string_pretty(&credential)?.to_string())
}
CreateObject::Validation {
method,
@@ -121,7 +123,7 @@ async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> {
ValidationMethod::Email { address } => {
let validation = secd.validate_email(&address, identity_id).await?;
- Some(serde_json::ser::to_string(&validation)?.to_string())
+ Some(serde_json::ser::to_string_pretty(&validation)?.to_string())
}
_ => unimplemented!(),
},
@@ -136,7 +138,7 @@ async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> {
let session = secd
.complete_address_validation(&validation_id, token, code)
.await?;
- Some(serde_json::ser::to_string(&session)?.to_string())
+ Some(serde_json::ser::to_string_pretty(&session)?.to_string())
}
})
}
@@ -149,44 +151,31 @@ async fn dev(cmd: DevObject) -> Result<Option<String>> {
async fn get(secd: &Secd, cmd: GetObject) -> Result<Option<String>> {
Ok(match cmd {
- GetObject::ApiKey { public_key } => {
- println!("get object api key");
- None
- }
- GetObject::Group { name, id } => {
- println!("get object group");
- None
- }
GetObject::Identity {
identity_id,
- session_token,
- } => Some(
- serde_json::ser::to_string(&secd.get_identity(identity_id, session_token).await?)?
- .to_string(),
- ),
-
- GetObject::Permission { name, id } => {
- println!("get object permission");
- None
- }
- GetObject::Role { name, id } => {
- println!("get object role");
- None
- }
- GetObject::Service { name, id } => {
- println!("get object service");
- None
- }
- GetObject::ServiceAction { name, id } => {
- println!("get object service action");
- None
- }
- GetObject::Session { secret } => {
- Some(serde_json::ser::to_string(&secd.get_session(&secret).await?)?.to_string())
- }
- GetObject::Validation { id } => {
- println!("get object validation");
- None
+
+ credential,
+ } => {
+ let t = credential.map(|cred| match cred {
+ ValidateObject::ApiToken { token } => {
+ CredentialType::api_token_from_str(&token).expect("failed to build api token")
+ }
+ ValidateObject::Passphrase {
+ username,
+ passphrase,
+ } => CredentialType::Passphrase {
+ key: username,
+ value: passphrase,
+ },
+ ValidateObject::Session { token } => {
+ CredentialType::session_from_str(&token).expect("failed to build session")
+ }
+ });
+
+ Some(
+ serde_json::ser::to_string_pretty(&secd.get_identity(identity_id, t).await?)?
+ .to_string(),
+ )
}
})
}
@@ -202,5 +191,37 @@ async fn update(secd: &Secd, cmd: UpdateObject) -> Result<Option<String>> {
Some(serde_json::to_string(&identity)?.to_string())
}
+ UpdateObject::Credential { id, revoke } => {
+ if revoke {
+ secd.revoke_credential(id).await?;
+ }
+
+ Some("Ok".to_string())
+ }
})
}
+
+async fn validate(secd: &Secd, cmd: ValidateObject) -> Result<Option<String>> {
+ let credential = match cmd {
+ ValidateObject::ApiToken { token } => {
+ secd.validate_credential(CredentialType::api_token_from_str(&token)?)
+ .await?
+ }
+ ValidateObject::Passphrase {
+ username,
+ passphrase,
+ } => {
+ secd.validate_credential(CredentialType::Passphrase {
+ key: username,
+ value: passphrase,
+ })
+ .await?
+ }
+ ValidateObject::Session { token } => {
+ secd.validate_credential(CredentialType::session_from_str(&token)?)
+ .await?
+ }
+ };
+
+ Ok(Some(serde_json::to_string_pretty(&credential)?.to_string()))
+}
diff --git a/crates/secd/Cargo.toml b/crates/secd/Cargo.toml
index 94ae43c..2631934 100644
--- a/crates/secd/Cargo.toml
+++ b/crates/secd/Cargo.toml
@@ -6,10 +6,12 @@ edition = "2021"
[dependencies]
aes-gcm = "0.10.1"
-argon2 = "0.4.1"
+argon2 = "0.5.0"
async-trait = "0.1.59"
anyhow = "1.0"
+base64 = "0.21.0"
clap = { version = "4.0.29", features = ["derive"] }
+config = "0.13.3"
derive_more = "0.99"
email_address = "0.2"
hex = "0.4"
@@ -25,7 +27,7 @@ reqwest = { version = "0.11.13", features = ["json"] }
sendgrid = { version = "0.18", features = ["async"] }
serde = "1"
serde_json = { version = "1.0", features = ["raw_value"] }
-serde_with = { version = "2.1", features = ["hex"] }
+serde_with = { version = "2.1", features = ["base64", "hex"] }
sha2 = "0.10.6"
strum = "0.24.1"
strum_macros = "0.24"
diff --git a/crates/secd/build.rs b/crates/secd/build.rs
index 8471105..e6f3362 100644
--- a/crates/secd/build.rs
+++ b/crates/secd/build.rs
@@ -8,7 +8,6 @@ fn main() {
let proto_files: Vec<PathBuf> = glob("proto/**/*.proto")
.unwrap()
- .into_iter()
.filter_map(Result::ok)
.collect();
@@ -22,7 +21,7 @@ fn main() {
tonic_build::configure()
.server_mod_attribute("attrs", "#[cfg(feature = \"server\")]")
.client_mod_attribute("attrs", "#[cfg(feature = \"client\")]")
- .file_descriptor_set_path(&descriptor_path)
+ .file_descriptor_set_path(descriptor_path)
.compile(&proto_files, &["proto"])
.unwrap();
}
diff --git a/crates/secd/src/auth/n.rs b/crates/secd/src/auth/n.rs
index 1f32fd6..dde6e7d 100644
--- a/crates/secd/src/auth/n.rs
+++ b/crates/secd/src/auth/n.rs
@@ -5,12 +5,12 @@ use crate::{
DEFAULT_SIGNUP_EMAIL,
},
store::{
- AddressLens, AddressValidationLens, CredentialLens, IdentityLens, SessionLens,
- Storable, StoreError,
+ AddressLens, AddressValidationLens, CredentialLens, IdentityLens, Storable, StoreError,
},
},
- util, Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod,
- Credential, CredentialType, Identity, IdentityId, Secd, SecdError, Session, SessionToken,
+ util::{self, ErrorContext},
+ Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod,
+ Credential, CredentialId, CredentialType, Identity, IdentityId, Secd, SecdError,
ADDRESSS_VALIDATION_CODE_SIZE, ADDRESS_VALIDATION_ALLOWS_ATTEMPTS,
ADDRESS_VALIDATION_IDENTITY_SURJECTION, EMAIL_VALIDATION_DURATION,
};
@@ -84,8 +84,8 @@ impl Secd {
revoked_at: None,
validated_at: None,
attempts: 0,
- hashed_token: util::hash(&secret.as_bytes()),
- hashed_code: util::hash(&code.as_bytes()),
+ hashed_token: util::hash(secret.as_bytes()),
+ hashed_code: util::hash(code.as_bytes()),
};
validation.write(self.store.clone()).await?;
@@ -95,11 +95,13 @@ impl Secd {
.cfg
.email_address_from
.clone()
+ .and_then(|s| s.parse().ok())
.unwrap_or("SecD <noreply@secd.com>".parse().unwrap()),
replyto_address: self
.cfg
.email_address_replyto
.clone()
+ .and_then(|s| s.parse().ok())
.unwrap_or("SecD <noreply@secd.com>".parse().unwrap()),
recipient: email_address.clone(),
subject: "Login Request".into(),
@@ -129,7 +131,7 @@ impl Secd {
validation_id: &AddressValidationId,
plaintext_token: Option<String>,
plaintext_code: Option<String>,
- ) -> Result<Session, SecdError> {
+ ) -> Result<Credential, SecdError> {
let mut validation = AddressValidation::find(
self.store.clone(),
&AddressValidationLens {
@@ -193,7 +195,6 @@ impl Secd {
id: None,
address_type: Some(&validation.address.t),
validated_address: Some(true),
- session_token_hash: None,
},
)
.await?;
@@ -210,6 +211,7 @@ impl Secd {
id: Uuid::new_v4(),
address_validations: vec![],
credentials: vec![],
+ new_credentials: vec![],
rules: vec![],
metadata: None,
created_at: OffsetDateTime::now_utc(),
@@ -235,30 +237,72 @@ impl Secd {
validation.validated_at = Some(OffsetDateTime::now_utc());
validation.write(self.store.clone()).await?;
- let session = Session::new(validation.identity_id.expect("unreachable d3ded289-72eb-4a42-a37d-f5c9c697cc61 [assert(identity.is_some()) prevents this]"))?;
+ let mut session = Credential::new_session(validation.identity_id.expect("unreachable d3ded289-72eb-4a42-a37d-f5c9c697cc61 [assert(identity.is_some()) prevents this]"))?;
+ let plaintext_type = session.t.clone();
+
+ session.hash(&self.crypter)?;
session.write(self.store.clone()).await?;
+ session.t = plaintext_type;
+
Ok(session)
}
+ pub async fn create_identity_with_credential(
+ &self,
+ t: CredentialType,
+ identity_id: IdentityId,
+ metadata: Option<String>,
+ ) -> Result<Identity, SecdError> {
+ let identity = Identity::find(
+ self.store.clone(),
+ &IdentityLens {
+ id: Some(&identity_id),
+ address_type: None,
+ validated_address: None,
+ },
+ )
+ .await?;
+
+ if !identity.is_empty() {
+ log::error!("identity was found while creating a new identity with a credential");
+ return Err(SecdError::IdentityAlreadyExists);
+ }
+
+ Identity {
+ id: identity_id,
+ address_validations: vec![],
+ credentials: vec![],
+ new_credentials: vec![],
+ rules: vec![],
+ metadata,
+ created_at: OffsetDateTime::now_utc(),
+ deleted_at: None,
+ }
+ .write(self.store.clone())
+ .await?;
+
+ self.create_credential(t, Some(identity_id), None).await
+ }
+
pub async fn create_credential(
&self,
t: CredentialType,
identity_id: Option<IdentityId>,
+ expires_at: Option<OffsetDateTime>,
) -> Result<Identity, SecdError> {
- let identity = match identity_id {
+ let mut identity = match identity_id {
Some(id) => Identity::find(
self.store.clone(),
&IdentityLens {
id: Some(&id),
address_type: None,
validated_address: None,
- session_token_hash: None,
},
)
.await?
.into_iter()
- .nth(0)
+ .next()
.ok_or(SecdError::IdentityNotFound)?,
None => {
@@ -266,6 +310,7 @@ impl Secd {
id: Uuid::new_v4(),
address_validations: vec![],
credentials: vec![],
+ new_credentials: vec![],
rules: vec![],
metadata: None,
created_at: OffsetDateTime::now_utc(),
@@ -282,7 +327,6 @@ impl Secd {
id: None,
identity_id: Some(identity.id),
t: Some(&t),
- restrict_by_key: Some(false),
},
)
.await?[..]
@@ -290,9 +334,9 @@ impl Secd {
[] => Credential {
id: Uuid::new_v4(),
identity_id: identity.id,
- t,
+ t: t.clone(),
created_at: OffsetDateTime::now_utc(),
- revoked_at: None,
+ revoked_at: expires_at,
deleted_at: None,
},
_ => return Err(SecdError::CredentialAlreadyExists),
@@ -307,57 +351,100 @@ impl Secd {
err => SecdError::StoreError(err),
})?;
- Ok(identity)
- }
+ identity.new_credentials.push(Credential {
+ id: credential.id,
+ identity_id: credential.identity_id,
+ t,
+ created_at: credential.created_at,
+ revoked_at: credential.revoked_at,
+ deleted_at: credential.deleted_at,
+ });
- pub async fn validate_credential(
- &self,
- // t: CredentialType,
- // key: String,
- // value: Option<String>,
- ) -> Result<Session, SecdError> {
- // Credential::find(store, lens) use key here as unique index
- todo!()
+ Ok(identity)
}
- pub async fn get_session(&self, t: &SessionToken) -> Result<Session, SecdError> {
- let token = hex::decode(t)?;
- let mut session = Session::find(
+ pub async fn validate_credential(&self, t: CredentialType) -> Result<Credential, SecdError> {
+ let mut retrieved = Credential::find(
self.store.clone(),
- &SessionLens {
- token_hash: Some(&util::hash(&token)),
+ &CredentialLens {
+ id: None,
identity_id: None,
+ t: Some(&t),
},
)
- .await?;
- assert!(session.len() <= 1, "get session failed: multiple sessions found for a single token. This is very _very_ bad.");
+ .await?
+ .into_iter()
+ .next()
+ .ok_or(SecdError::InvalidCredential)?;
- if session.is_empty() {
- return Err(SecdError::InvalidSession);
- } else {
- let mut session = session.swap_remove(0);
- session.token = token;
- Ok(session)
- }
+ match retrieved.revoked_at {
+ Some(t) if t <= OffsetDateTime::now_utc() => {
+ log::debug!("credential was revoked");
+ Err(SecdError::InvalidCredential)
+ }
+ _ => Ok(()),
+ }?;
+
+ match retrieved.deleted_at {
+ Some(t) if t <= OffsetDateTime::now_utc() => {
+ log::debug!("credential was deleted");
+ Err(SecdError::InvalidCredential)
+ }
+ _ => Ok(()),
+ }?;
+
+ retrieved.hash_compare(&t, &self.crypter)?;
+
+ // Return the initially provided plaintext credential since it's valid
+ retrieved.t = t;
+
+ Ok(retrieved)
}
pub async fn get_identity(
&self,
i: Option<IdentityId>,
- t: Option<SessionToken>,
+ t: Option<CredentialType>,
) -> Result<Identity, SecdError> {
- let token_hash = match t {
- Some(tok) => Some(util::hash(&hex::decode(&tok)?)),
- None => None,
- };
+ if i.is_none() && t.is_none() {
+ log::error!("get_identity expects that at least one of IdentityId or CredentialType is provided. None were found.");
+ return Err(SecdError::IdentityNotFound);
+ }
+
+ let c = Credential::find(
+ self.store.clone(),
+ &CredentialLens {
+ id: None,
+ identity_id: i,
+ t: t.as_ref(),
+ },
+ )
+ .await?;
+
+ assert!(
+ c.len() <= 1,
+ "The provided credential refers to more than one identity. This is very _very_ bad."
+ );
+ let identity_id = c
+ .into_iter()
+ .next()
+ .ok_or(SecdError::InvalidCredential)
+ .ctx("No identities were found for the provided identity_id and credential_type")?
+ .identity_id;
+
+ if i.is_some() && i != Some(identity_id) {
+ log::error!(
+ "The provided identity does not match the identity associated with this credential"
+ );
+ return Err(SecdError::InvalidCredential);
+ }
let mut i = Identity::find(
self.store.clone(),
&IdentityLens {
- id: i.as_ref(),
+ id: Some(&identity_id),
address_type: None,
validated_address: None,
- session_token_hash: token_hash,
},
)
.await?;
@@ -368,7 +455,7 @@ impl Secd {
);
if i.is_empty() {
- return Err(SecdError::IdentityNotFound);
+ Err(SecdError::IdentityNotFound)
} else {
Ok(i.swap_remove(0))
}
@@ -385,12 +472,11 @@ impl Secd {
id: Some(&i),
address_type: None,
validated_address: None,
- session_token_hash: None,
},
)
.await?
.into_iter()
- .nth(0)
+ .next()
.ok_or(SecdError::IdentityNotFound)?;
identity.metadata = Some(md);
@@ -399,9 +485,22 @@ impl Secd {
Ok(identity)
}
- pub async fn revoke_session(&self, session: &mut Session) -> Result<(), SecdError> {
- session.revoked_at = Some(OffsetDateTime::now_utc());
- session.write(self.store.clone()).await?;
+ pub async fn revoke_credential(&self, credential_id: CredentialId) -> Result<(), SecdError> {
+ let mut credential = Credential::find(
+ self.store.clone(),
+ &CredentialLens {
+ id: Some(credential_id),
+ identity_id: None,
+ t: None,
+ },
+ )
+ .await?
+ .into_iter()
+ .next()
+ .ok_or(SecdError::InvalidCredential)?;
+
+ credential.revoked_at = Some(OffsetDateTime::now_utc());
+ credential.write(self.store.clone()).await?;
Ok(())
}
}
diff --git a/crates/secd/src/auth/z/graph.rs b/crates/secd/src/auth/z/graph.rs
index 9ca045b..f28c901 100644
--- a/crates/secd/src/auth/z/graph.rs
+++ b/crates/secd/src/auth/z/graph.rs
@@ -1,209 +1,207 @@
-use std::collections::{HashSet, VecDeque};
-
-type NodeIdx = usize;
-type EdgeIdx = usize;
-
-struct RelationGraph {
- nodes: Vec<Node>,
- edges: Vec<Edge>,
-}
-
-#[derive(Hash, PartialEq, Eq)]
-struct Node {
- id: u64,
- is_namespace: bool,
- edge_list_head: Option<EdgeIdx>,
-}
-
-struct Edge {
- dst: NodeIdx,
- op: Option<Operator>,
- next_edge: Option<EdgeIdx>,
-}
-
-enum Operator {
- And,
- Or,
- Difference,
- Complement,
- AndAll,
- OrAll,
- DifferenceAll,
- ComplementAll,
-}
-
-impl RelationGraph {
- pub fn new() -> Self {
- // As I think about how to serialize this graph, when reading from the DB
- // I should chunk up the namespaces so that I can read-on-demand when I
- // encounter a namespace that has not yet been loaded, and in this way
- // I don't need to deal with the entire graph...
- //
- // but that's a totally unnecessary fun optimization.
- RelationGraph {
- nodes: vec![],
- edges: vec![],
- }
- }
-
- pub fn add_node(&mut self, id: u64, is_namespace: bool) -> NodeIdx {
- let idx = self.nodes.len();
- self.nodes.push(Node {
- id,
- is_namespace,
- edge_list_head: None,
- });
- idx
- }
-
- pub fn add_edge(
- &mut self,
- src: NodeIdx,
- dst: NodeIdx,
- op: Option<Operator>,
- follow: Option<NodeIdx>,
- ) {
- let edge_idx = self.edges.len();
- let node = &mut self.nodes[src];
- // TODO: dupe check
- self.edges.push(Edge {
- dst,
- op,
- next_edge: node.edge_list_head,
- });
- node.edge_list_head = Some(edge_idx);
- }
-
- // doc:2#viewer@user:1
- pub fn find_leaves(&self, src: NodeIdx, filterIdx: Option<NodeIdx>) -> Vec<NodeIdx> {
- let mut all_leaves = vec![];
- // let mut and_leaves = vec![];
- // let mut exc_leaves = vec![];
-
- // the output should basically be leaves to look up
- // for example
- // the leaves of doc might be (user), <(organization, admin)>, <(role, foo)>
- // and since (organization, admin) has leaves (user) as well
- // if the user is in that final set, then they are valid
- // and those leaves would be queried as relation tuples saved in the DB
-
- println!("HERE I AM");
-
- let start = self.nodes.get(src);
- let mut seen = HashSet::new();
- let mut q = VecDeque::new();
-
- seen.insert(start);
- q.push_back(start);
-
- // I need to build a chain of ops
- // [AND, OR, OR, AND, OR, ...]
- // at each stage, I need to apply the operator chain
- while let Some(&Some(node)) = q.iter().next() {
- if node.is_namespace {
- all_leaves.push(node);
- }
-
- // for edge in edge list
-
- println!("IN QUEUE\n");
- println!("node: {:?}\n", node.id);
- q.pop_front();
- }
- println!("DONE");
-
- // task...starting at doc_viewer, return all leaf nodes
- // to do this....
- // check all children
- // if child is OR
- // if child is leaf, return child
- // else return all of these children
- // if child is AND
- // if child is leaf, return if also present in all other nodes
- // else return all children which are also present in other nodes
- // if child is Difference
- // if child is leaf, do not include and remove if it part of the user set
- // else remove all of these children
-
- // TOTAL pool of leafs
- // Intersection leaves (i.e. ONLY leaves which MUST exist)
- // exclusion leaves (i.e. leaves which CANNOT exist)
-
- // bfs build query...
- // execute query
-
- // start at nodeIdx: doc_viewer
- // expand until I find all leaf nodes.
- // we are interested in leaf nodes with nodeIdx: user
- // return all leaf nodeIdx that match the filter. If no filter provided, return all leaf idx.
-
- // TODO: optionally build the expansion path by pushing every intermediate step as the bfs is walked.
-
- // with each step of the bfs, perform the operator.
- // e.g. if first step is doc_viewer -> user with operator OR then we have leaf user with operator chain [OR]
- // the next step might be doc_viewer -> doc_editor with step OR and doc_editor -> user with step OR, so we have leaf user with chain [OR, OR]
- // the next step might be doc_viewer -> doc_auditor with OR
- // then doc_auditor -> doc_editor with OR
- // then doc_editor -> doc_user with OR
- // then doc_editor -> doc_owner
- // then doc_auditor -> doc_owner with Difference
- //
-
- todo!()
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
-
- #[test]
- fn build_graph_test() {
- let mut g = RelationGraph::new();
- let user = g.add_node(1, true);
- let role = g.add_node(2, true);
- let group = g.add_node(3, true);
- let doc = g.add_node(4, true);
-
- let role_member = g.add_node(5, false);
-
- let group_member = g.add_node(6, false);
- let group_admin = g.add_node(7, false);
-
- let doc_owner = g.add_node(8, false);
- let doc_editor = g.add_node(9, false);
- let doc_viewer = g.add_node(10, false);
- let doc_auditor = g.add_node(11, false);
- let doc_parent = g.add_node(12, false);
-
- g.add_edge(role, role_member, None, None);
- g.add_edge(role_member, user, Some(Operator::Or), None);
- g.add_edge(role_member, group_member, Some(Operator::Or), None);
-
- g.add_edge(group, group_member, None, None);
- g.add_edge(group, group_admin, None, None);
-
- g.add_edge(group_member, user, Some(Operator::Or), None);
- g.add_edge(group_member, group_admin, Some(Operator::Or), None);
- g.add_edge(group_admin, user, Some(Operator::Or), None);
-
- g.add_edge(doc, doc_owner, None, None);
- g.add_edge(doc, doc_editor, None, None);
- g.add_edge(doc, doc_viewer, None, None);
- g.add_edge(doc, doc_auditor, None, None);
- g.add_edge(doc, doc_parent, None, None);
-
- g.add_edge(doc_owner, user, Some(Operator::Or), None);
- g.add_edge(doc_editor, user, Some(Operator::Or), None);
- g.add_edge(doc_editor, doc_owner, Some(Operator::Or), None);
- g.add_edge(doc_viewer, user, Some(Operator::Or), None);
- g.add_edge(doc_viewer, doc_editor, Some(Operator::Or), None);
- g.add_edge(doc_viewer, doc_parent, Some(Operator::Or), Some(doc_viewer));
- g.add_edge(doc_viewer, user, Some(Operator::OrAll), None);
- g.add_edge(doc_viewer, doc_auditor, Some(Operator::Or), None);
- g.add_edge(doc_auditor, doc_editor, Some(Operator::Difference), None);
-
- g.find_leaves(doc_viewer, None);
- assert_eq!(1, 2);
- }
-}
+// use std::collections::{HashSet, VecDeque};
+
+// type NodeIdx = usize;
+// type EdgeIdx = usize;
+
+// struct RelationGraph {
+// nodes: Vec<Node>,
+// edges: Vec<Edge>,
+// }
+
+// #[derive(Hash, PartialEq, Eq)]
+// struct Node {
+// id: u64,
+// is_namespace: bool,
+// edge_list_head: Option<EdgeIdx>,
+// }
+
+// struct Edge {
+// dst: NodeIdx,
+// op: Option<Operator>,
+// next_edge: Option<EdgeIdx>,
+// }
+
+// enum Operator {
+// And,
+// Or,
+// Difference,
+// Complement,
+// AndAll,
+// OrAll,
+// DifferenceAll,
+// ComplementAll,
+// }
+
+// impl RelationGraph {
+// pub fn new() -> Self {
+// // As I think about how to serialize this graph, when reading from the DB
+// // I should chunk up the namespaces so that I can read-on-demand when I
+// // encounter a namespace that has not yet been loaded, and in this way
+// // I don't need to deal with the entire graph...
+// //
+// // but that's a totally unnecessary fun optimization.
+// RelationGraph {
+// nodes: vec![],
+// edges: vec![],
+// }
+// }
+
+// pub fn add_node(&mut self, id: u64, is_namespace: bool) -> NodeIdx {
+// let idx = self.nodes.len();
+// self.nodes.push(Node {
+// id,
+// is_namespace,
+// edge_list_head: None,
+// });
+// idx
+// }
+
+// pub fn add_edge(
+// &mut self,
+// src: NodeIdx,
+// dst: NodeIdx,
+// op: Option<Operator>,
+// follow: Option<NodeIdx>,
+// ) {
+// let edge_idx = self.edges.len();
+// let node = &mut self.nodes[src];
+// // TODO: dupe check
+// self.edges.push(Edge {
+// dst,
+// op,
+// next_edge: node.edge_list_head,
+// });
+// node.edge_list_head = Some(edge_idx);
+// }
+
+// // doc:2#viewer@user:1
+// pub fn find_leaves(&self, src: NodeIdx, filterIdx: Option<NodeIdx>) -> Vec<NodeIdx> {
+// let mut all_leaves = vec![];
+// // let mut and_leaves = vec![];
+// // let mut exc_leaves = vec![];
+
+// // the output should basically be leaves to look up
+// // for example
+// // the leaves of doc might be (user), <(organization, admin)>, <(role, foo)>
+// // and since (organization, admin) has leaves (user) as well
+// // if the user is in that final set, then they are valid
+// // and those leaves would be queried as relation tuples saved in the DB
+
+// let start = self.nodes.get(src);
+// let mut seen = HashSet::new();
+// let mut q = VecDeque::new();
+
+// seen.insert(start);
+// q.push_back(start);
+
+// // I need to build a chain of ops
+// // [AND, OR, OR, AND, OR, ...]
+// // at each stage, I need to apply the operator chain
+// while let Some(&Some(node)) = q.iter().next() {
+// if node.is_namespace {
+// all_leaves.push(node);
+// }
+
+// // for edge in edge list
+
+// println!("IN QUEUE\n");
+// println!("node: {:?}\n", node.id);
+// q.pop_front();
+// }
+// println!("DONE");
+
+// // task...starting at doc_viewer, return all leaf nodes
+// // to do this....
+// // check all children
+// // if child is OR
+// // if child is leaf, return child
+// // else return all of these children
+// // if child is AND
+// // if child is leaf, return if also present in all other nodes
+// // else return all children which are also present in other nodes
+// // if child is Difference
+// // if child is leaf, do not include and remove if it part of the user set
+// // else remove all of these children
+
+// // TOTAL pool of leafs
+// // Intersection leaves (i.e. ONLY leaves which MUST exist)
+// // exclusion leaves (i.e. leaves which CANNOT exist)
+
+// // bfs build query...
+// // execute query
+
+// // start at nodeIdx: doc_viewer
+// // expand until I find all leaf nodes.
+// // we are interested in leaf nodes with nodeIdx: user
+// // return all leaf nodeIdx that match the filter. If no filter provided, return all leaf idx.
+
+// // TODO: optionally build the expansion path by pushing every intermediate step as the bfs is walked.
+
+// // with each step of the bfs, perform the operator.
+// // e.g. if first step is doc_viewer -> user with operator OR then we have leaf user with operator chain [OR]
+// // the next step might be doc_viewer -> doc_editor with step OR and doc_editor -> user with step OR, so we have leaf user with chain [OR, OR]
+// // the next step might be doc_viewer -> doc_auditor with OR
+// // then doc_auditor -> doc_editor with OR
+// // then doc_editor -> doc_user with OR
+// // then doc_editor -> doc_owner
+// // then doc_auditor -> doc_owner with Difference
+// //
+
+// todo!()
+// }
+// }
+
+// #[cfg(test)]
+// mod test {
+// use super::*;
+
+// #[test]
+// fn build_graph_test() {
+// let mut g = RelationGraph::new();
+// let user = g.add_node(1, true);
+// let role = g.add_node(2, true);
+// let group = g.add_node(3, true);
+// let doc = g.add_node(4, true);
+
+// let role_member = g.add_node(5, false);
+
+// let group_member = g.add_node(6, false);
+// let group_admin = g.add_node(7, false);
+
+// let doc_owner = g.add_node(8, false);
+// let doc_editor = g.add_node(9, false);
+// let doc_viewer = g.add_node(10, false);
+// let doc_auditor = g.add_node(11, false);
+// let doc_parent = g.add_node(12, false);
+
+// g.add_edge(role, role_member, None, None);
+// g.add_edge(role_member, user, Some(Operator::Or), None);
+// g.add_edge(role_member, group_member, Some(Operator::Or), None);
+
+// g.add_edge(group, group_member, None, None);
+// g.add_edge(group, group_admin, None, None);
+
+// g.add_edge(group_member, user, Some(Operator::Or), None);
+// g.add_edge(group_member, group_admin, Some(Operator::Or), None);
+// g.add_edge(group_admin, user, Some(Operator::Or), None);
+
+// g.add_edge(doc, doc_owner, None, None);
+// g.add_edge(doc, doc_editor, None, None);
+// g.add_edge(doc, doc_viewer, None, None);
+// g.add_edge(doc, doc_auditor, None, None);
+// g.add_edge(doc, doc_parent, None, None);
+
+// g.add_edge(doc_owner, user, Some(Operator::Or), None);
+// g.add_edge(doc_editor, user, Some(Operator::Or), None);
+// g.add_edge(doc_editor, doc_owner, Some(Operator::Or), None);
+// g.add_edge(doc_viewer, user, Some(Operator::Or), None);
+// g.add_edge(doc_viewer, doc_editor, Some(Operator::Or), None);
+// g.add_edge(doc_viewer, doc_parent, Some(Operator::Or), Some(doc_viewer));
+// g.add_edge(doc_viewer, user, Some(Operator::OrAll), None);
+// g.add_edge(doc_viewer, doc_auditor, Some(Operator::Or), None);
+// g.add_edge(doc_auditor, doc_editor, Some(Operator::Difference), None);
+
+// g.find_leaves(doc_viewer, None);
+// assert_eq!(1, 2);
+// }
+// }
diff --git a/crates/secd/src/auth/z/mod.rs b/crates/secd/src/auth/z/mod.rs
index b364583..d663e65 100644
--- a/crates/secd/src/auth/z/mod.rs
+++ b/crates/secd/src/auth/z/mod.rs
@@ -1,3 +1,5 @@
+#![allow(dead_code)] // TODO: Remove when implemented
+#![allow(unused_variables)]
mod graph;
use crate::{Authorization, Secd, SecdError};
@@ -50,7 +52,7 @@ impl Authorization for Secd {
// they are "relationships" rather than what spice calls permissions
spice
.write_relationship(
- &ts.into_iter()
+ &ts.iter()
.map(|r| Relationship {
subject: r.subject.clone(),
object: r.object.clone(),
diff --git a/crates/secd/src/client/email/mod.rs b/crates/secd/src/client/email/mod.rs
index 7c7b233..9e591ba 100644
--- a/crates/secd/src/client/email/mod.rs
+++ b/crates/secd/src/client/email/mod.rs
@@ -42,7 +42,7 @@ pub enum MessengerType {
pub(crate) struct LocalMailer {}
impl LocalMailer {
- pub fn new() -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
+ pub fn new_ref() -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
warn!("You are using the local mailer, which will not work in production!");
Arc::new(LocalMailer {})
}
@@ -59,7 +59,7 @@ pub(crate) struct Sendgrid {
pub api_key: String,
}
impl Sendgrid {
- pub fn new(api_key: String) -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
+ pub fn new_ref(api_key: String) -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
Arc::new(Sendgrid { api_key })
}
}
@@ -89,7 +89,7 @@ impl Sendable for EmailValidationMessage {
.subject(self.subject.clone())
.multipart(MultiPart::alternative_plain_html(
"".to_string(),
- String::from(self.body.clone()),
+ self.body.clone(),
))?;
let mailer = lettre::SmtpTransport::unencrypted_localhost();
@@ -135,7 +135,7 @@ pub(crate) fn parse_email_template(
validation_secret: Option<String>,
validation_code: Option<String>,
) -> Result<String, EmailMessengerError> {
- let mut t = template.clone().to_string();
+ let mut t = String::from(template);
// We do not allow substutions for a variety of reasons, but mainly security ones.
// The only things we want to share are those which secd allows. In this case, that
// means we only send an email with static content as provided by the filter, except
@@ -143,8 +143,14 @@ pub(crate) fn parse_email_template(
// present in the email.
t = t.replace("{{secd::validation_id}}", &validation_id.to_string());
- validation_secret.map(|secret| t = t.replace("{{secd::validation_secret}}", &secret));
- validation_code.map(|code| t = t.replace("{{secd::validation_code}}", &code));
+
+ if let Some(secret) = validation_secret {
+ t = t.replace("{{secd::validation_secret}}", &secret);
+ }
+
+ if let Some(code) = validation_code {
+ t = t.replace("{{secd::validation_code}}", &code);
+ }
Ok(t)
}
diff --git a/crates/secd/src/client/spice/mod.rs b/crates/secd/src/client/spice/mod.rs
index d3ca30d..67965d7 100644
--- a/crates/secd/src/client/spice/mod.rs
+++ b/crates/secd/src/client/spice/mod.rs
@@ -3,6 +3,7 @@
// favor of a light weight solution that leverages the Zanzibar API but disregards the
// scaling part.
+#[allow(clippy::module_inception)]
pub mod spice {
tonic::include_proto!("authzed.api.v1");
}
@@ -10,7 +11,7 @@ pub mod spice {
use spice::permissions_service_client::PermissionsServiceClient;
use spice::schema_service_client::SchemaServiceClient;
use spice::WriteSchemaRequest;
-use std::env::var;
+use std::matches;
use tonic::metadata::MetadataValue;
use tonic::transport::Channel;
use tonic::{Request, Status};
@@ -19,7 +20,6 @@ use crate::auth::z::{self, Subject};
use crate::client::spice::spice::{
relationship_update, ObjectReference, Relationship, RelationshipUpdate, SubjectReference,
};
-use crate::{ENV_SPICE_SECRET, ENV_SPICE_SERVER};
use self::spice::check_permission_response::Permissionship;
use self::spice::{consistency, CheckPermissionRequest, Consistency, WriteRelationshipsRequest};
@@ -36,12 +36,7 @@ pub(crate) struct Spice {
}
impl Spice {
- pub async fn new() -> Self {
- let secret =
- var(ENV_SPICE_SECRET).expect("initialization error: Failed to find SPICE_SECRET");
- let server =
- var(ENV_SPICE_SERVER).expect("initialization error: Failed to find SPICE_SERVER");
-
+ pub async fn new(secret: String, server: String) -> Self {
let channel = Channel::from_shared(server)
.expect("invalid SPICE_SERVER uri")
.connect()
@@ -69,10 +64,10 @@ impl Spice {
let response = client.check_permission(request).await?.into_inner();
- Ok(match Permissionship::from_i32(response.permissionship) {
- Some(Permissionship::HasPermission) => true,
- _ => false,
- })
+ Ok(matches!(
+ Permissionship::from_i32(response.permissionship),
+ Some(Permissionship::HasPermission)
+ ))
}
pub async fn write_relationship(&self, rs: &[z::Relationship]) -> Result<(), SpiceError> {
@@ -83,7 +78,7 @@ impl Spice {
let request = tonic::Request::new(WriteRelationshipsRequest {
updates: rs
- .into_iter()
+ .iter()
.map(|t| RelationshipUpdate {
operation: (relationship_update::Operation::Touch as i32),
relationship: Some(Relationship {
diff --git a/crates/secd/src/client/store/mod.rs b/crates/secd/src/client/store/mod.rs
index 7bf01d5..6c42dba 100644
--- a/crates/secd/src/client/store/mod.rs
+++ b/crates/secd/src/client/store/mod.rs
@@ -6,8 +6,8 @@ use std::sync::Arc;
use uuid::Uuid;
use crate::{
- util, Address, AddressType, AddressValidation, Credential, CredentialId, CredentialType,
- Identity, IdentityId, Session,
+ Address, AddressType, AddressValidation, Credential, CredentialId, CredentialType, Identity,
+ IdentityId,
};
use self::sql_db::SqlClient;
@@ -60,21 +60,13 @@ pub(crate) struct IdentityLens<'a> {
pub id: Option<&'a Uuid>,
pub address_type: Option<&'a AddressType>,
pub validated_address: Option<bool>,
- pub session_token_hash: Option<Vec<u8>>,
}
impl<'a> Lens for IdentityLens<'a> {}
-pub(crate) struct SessionLens<'a> {
- pub token_hash: Option<&'a Vec<u8>>,
- pub identity_id: Option<&'a IdentityId>,
-}
-impl<'a> Lens for SessionLens<'a> {}
-
pub(crate) struct CredentialLens<'a> {
pub id: Option<CredentialId>,
pub identity_id: Option<IdentityId>,
pub t: Option<&'a CredentialType>,
- pub restrict_by_key: Option<bool>,
}
impl<'a> Lens for CredentialLens<'a> {}
@@ -94,7 +86,7 @@ impl<'a> Storable<'a> for Address {
store: Arc<dyn Store>,
lens: &'a Self::Lens,
) -> Result<Vec<Self::Item>, StoreError> {
- let typ = lens.t.map(|at| at.to_string().clone());
+ let typ = lens.t.map(|at| at.to_string());
let typ = typ.as_deref();
let val = lens.t.and_then(|at| at.get_value());
@@ -151,54 +143,18 @@ impl<'a> Storable<'a> for Identity {
Ok(match store.get_type() {
StoreType::Postgres { c } => {
- c.find_identity(
- lens.id,
- val,
- lens.validated_address,
- &lens.session_token_hash,
- )
- .await?
+ c.find_identity(lens.id, val, lens.validated_address)
+ .await?
}
StoreType::Sqlite { c } => {
- c.find_identity(
- lens.id,
- val,
- lens.validated_address,
- &lens.session_token_hash,
- )
- .await?
+ c.find_identity(lens.id, val, lens.validated_address)
+ .await?
}
})
}
}
#[async_trait]
-impl<'a> Storable<'a> for Session {
- type Item = Session;
- type Lens = SessionLens<'a>;
-
- async fn write(&self, store: Arc<dyn Store>) -> Result<(), StoreError> {
- let token_hash = util::hash(&self.token);
- match store.get_type() {
- StoreType::Postgres { c } => c.write_session(self, &token_hash).await?,
- StoreType::Sqlite { c } => c.write_session(self, &token_hash).await?,
- }
- Ok(())
- }
- async fn find(
- store: Arc<dyn Store>,
- lens: &'a Self::Lens,
- ) -> Result<Vec<Self::Item>, StoreError> {
- let token = lens.token_hash.map(|t| t.clone()).unwrap_or(vec![]);
-
- Ok(match store.get_type() {
- StoreType::Postgres { c } => c.find_session(token, lens.identity_id).await?,
- StoreType::Sqlite { c } => c.find_session(token, lens.identity_id).await?,
- })
- }
-}
-
-#[async_trait]
impl<'a> Storable<'a> for Credential {
type Item = Credential;
type Lens = CredentialLens<'a>;
@@ -217,31 +173,9 @@ impl<'a> Storable<'a> for Credential {
) -> Result<Vec<Self::Item>, StoreError> {
Ok(match store.get_type() {
StoreType::Postgres { c } => {
- c.find_credential(
- lens.id,
- lens.identity_id,
- lens.t,
- if let Some(true) = lens.restrict_by_key {
- true
- } else {
- false
- },
- )
- .await?
- }
- StoreType::Sqlite { c } => {
- c.find_credential(
- lens.id,
- lens.identity_id,
- lens.t,
- if let Some(true) = lens.restrict_by_key {
- true
- } else {
- false
- },
- )
- .await?
+ c.find_credential(lens.id, lens.identity_id, lens.t).await?
}
+ StoreType::Sqlite { c } => c.find_credential(lens.id, lens.identity_id, lens.t).await?,
})
}
}
diff --git a/crates/secd/src/client/store/sql_db.rs b/crates/secd/src/client/store/sql_db.rs
index 3e72fe8..7b3a68e 100644
--- a/crates/secd/src/client/store/sql_db.rs
+++ b/crates/secd/src/client/store/sql_db.rs
@@ -1,7 +1,7 @@
use super::{Store, StoreError, StoreType};
use crate::{
- Address, AddressType, AddressValidation, AddressValidationMethod, Credential, CredentialId,
- CredentialType, Identity, IdentityId, Session,
+ util::ErrorContext, Address, AddressType, AddressValidation, AddressValidationMethod,
+ Credential, CredentialId, CredentialType, Identity, IdentityId,
};
use email_address::EmailAddress;
use lazy_static::lazy_static;
@@ -26,8 +26,6 @@ const WRITE_CREDENTIAL: &str = "write_credential";
const FIND_CREDENTIAL: &str = "find_credential";
const WRITE_IDENTITY: &str = "write_identity";
const FIND_IDENTITY: &str = "find_identity";
-const WRITE_SESSION: &str = "write_session";
-const FIND_SESSION: &str = "find_session";
const ERR_MSG_MIGRATION_FAILED: &str = "Failed to apply secd migrations to a sql db. File a bug at https://www.github.com/branchcontrol/secdiam";
@@ -59,14 +57,6 @@ lazy_static! {
include_str!("../../../store/sqlite/sql/find_identity.sql"),
),
(
- WRITE_SESSION,
- include_str!("../../../store/sqlite/sql/write_session.sql"),
- ),
- (
- FIND_SESSION,
- include_str!("../../../store/sqlite/sql/find_session.sql"),
- ),
- (
WRITE_CREDENTIAL,
include_str!("../../../store/sqlite/sql/write_credential.sql"),
),
@@ -105,14 +95,6 @@ lazy_static! {
include_str!("../../../store/pg/sql/find_identity.sql"),
),
(
- WRITE_SESSION,
- include_str!("../../../store/pg/sql/write_session.sql"),
- ),
- (
- FIND_SESSION,
- include_str!("../../../store/pg/sql/find_session.sql"),
- ),
- (
WRITE_CREDENTIAL,
include_str!("../../../store/pg/sql/write_credential.sql"),
),
@@ -145,7 +127,7 @@ impl<T> SqlxResultExt<T> for Result<T, sqlx::Error> {
return Err(StoreError::IdempotentCheckAlreadyExists);
}
}
- self.map_err(|e| StoreError::SqlClientError(e))
+ self.map_err(StoreError::SqlClientError)
}
}
@@ -169,7 +151,7 @@ impl Store for PgClient {
}
impl PgClient {
- pub async fn new(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> {
+ pub async fn new_ref(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> {
sqlx::migrate!("store/pg/migrations", "secd")
.run(&pool)
.await
@@ -196,7 +178,7 @@ impl Store for SqliteClient {
}
impl SqliteClient {
- pub async fn new(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> {
+ pub async fn new_ref(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> {
sqlx::migrate!("store/sqlite/migrations", "secd")
.run(&pool)
.await
@@ -436,7 +418,6 @@ where
id: Option<&Uuid>,
address_value: Option<&str>,
address_is_validated: Option<bool>,
- session_token_hash: &Option<Vec<u8>>,
) -> Result<Vec<Identity>, StoreError> {
let sqls = get_sqls(&self.sqls_root, FIND_IDENTITY);
let rs = sqlx::query_as::<
@@ -452,7 +433,6 @@ where
.bind(id)
.bind(address_value)
.bind(address_is_validated)
- .bind(session_token_hash)
.fetch_all(&self.pool)
.await
.extend_err()?;
@@ -462,7 +442,8 @@ where
res.push(Identity {
id,
address_validations: vec![],
- credentials: vec![],
+ credentials: self.find_credential(None, Some(id), None).await?,
+ new_credentials: vec![],
rules: vec![],
metadata,
created_at,
@@ -473,57 +454,12 @@ where
Ok(res)
}
- pub async fn write_session(&self, s: &Session, token_hash: &[u8]) -> Result<(), StoreError> {
- let sqls = get_sqls(&self.sqls_root, WRITE_SESSION);
- sqlx::query(&sqls[0])
- .bind(s.identity_id)
- .bind(token_hash)
- .bind(s.created_at)
- .bind(s.expired_at)
- .bind(s.revoked_at)
- .execute(&self.pool)
- .await
- .extend_err()?;
-
- Ok(())
- }
-
- pub async fn find_session(
- &self,
- token: Vec<u8>,
- identity_id: Option<&Uuid>,
- ) -> Result<Vec<Session>, StoreError> {
- let sqls = get_sqls(&self.sqls_root, FIND_SESSION);
- let rs =
- sqlx::query_as::<_, (Uuid, OffsetDateTime, OffsetDateTime, Option<OffsetDateTime>)>(
- &sqls[0],
- )
- .bind(token)
- .bind(identity_id)
- .bind(OffsetDateTime::now_utc())
- .bind(OffsetDateTime::now_utc())
- .fetch_all(&self.pool)
- .await
- .extend_err()?;
-
- let mut res = vec![];
- for (identity_id, created_at, expired_at, revoked_at) in rs.into_iter() {
- res.push(Session {
- identity_id,
- token: vec![],
- created_at,
- expired_at,
- revoked_at,
- });
- }
- Ok(res)
- }
-
pub async fn write_credential(&self, c: &Credential) -> Result<(), StoreError> {
let sqls = get_sqls(&self.sqls_root, WRITE_CREDENTIAL);
let partial_key = match &c.t {
- crate::CredentialType::Passphrase { key, value: _ } => Some(key.clone()),
- _ => None,
+ CredentialType::Passphrase { key, .. } => Some(key.clone()),
+ CredentialType::ApiToken { public, .. } => Some(public.clone()),
+ CredentialType::Session { key, .. } => Some(key.clone()),
};
sqlx::query(&sqls[0])
@@ -545,17 +481,13 @@ where
id: Option<Uuid>,
identity_id: Option<Uuid>,
t: Option<&CredentialType>,
- restrict_by_key: bool,
) -> Result<Vec<Credential>, StoreError> {
let sqls = get_sqls(&self.sqls_root, FIND_CREDENTIAL);
- let key = restrict_by_key
- .then(|| {
- t.map(|i| match i {
- CredentialType::Passphrase { key, value: _ } => key.clone(),
- _ => todo!(),
- })
- })
- .flatten();
+ let key = t.map(|i| match i {
+ CredentialType::Passphrase { key, .. } => key.clone(),
+ CredentialType::ApiToken { public, .. } => public.clone(),
+ CredentialType::Session { key, .. } => key.clone(),
+ });
let rs = sqlx::query_as::<
_,
@@ -578,7 +510,9 @@ where
let mut res = vec![];
for (id, identity_id, data, created_at, revoked_at, deleted_at) in rs.into_iter() {
- let t: CredentialType = serde_json::from_str(&data)?;
+ let t: CredentialType =
+ serde_json::from_str(&data).ctx("error while deserializing credential_type")?;
+
res.push(Credential {
id,
identity_id,
diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs
index 54759a5..eb5d33d 100644
--- a/crates/secd/src/lib.rs
+++ b/crates/secd/src/lib.rs
@@ -12,37 +12,26 @@ use client::{
Store, StoreError,
},
};
+use config::Config;
use email_address::EmailAddress;
-use lettre::message::Mailbox;
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
-use serde_with::serde_as;
-use std::{
- env::{set_var, var},
- str::FromStr,
- sync::Arc,
+use serde_with::{
+ base64::{Base64, UrlSafe},
+ formats::Unpadded,
+ serde_as,
};
+use std::{env::var, fs::read_to_string, str::FromStr, sync::Arc};
use strum_macros::{Display, EnumString, EnumVariantNames};
use time::OffsetDateTime;
use util::crypter::{Crypter, CrypterError};
use uuid::Uuid;
-pub const ENV_AUTH_STORE_CONN_STRING: &str = "SECD_AUTH_STORE_CONN_STRING";
-pub const ENV_CRYPTER_SECRET_KEY: &str = "SECD_CRYPTER_SECRET_KEY";
-pub const ENV_EMAIL_ADDRESS_FROM: &str = "SECD_EMAIL_ADDRESS_FROM";
-pub const ENV_EMAIL_ADDRESS_REPLYTO: &str = "SECD_EMAIL_ADDRESS_REPLYTO";
-pub const ENV_EMAIL_MESSENGER: &str = "SECD_EMAIL_MESSENGER";
-pub const ENV_EMAIL_MESSENGER_CLIENT_ID: &str = "SECD_EMAIL_MESSENGER_CLIENT_ID";
-pub const ENV_EMAIL_MESSENGER_CLIENT_SECRET: &str = "SECD_EMAIL_MESSENGER_CLIENT_SECRET";
-pub const ENV_EMAIL_SENDGRID_API_KEY: &str = "SECD_EMAIL_SENDGRID_API_KEY";
-pub const ENV_EMAIL_SIGNIN_MESSAGE: &str = "SECD_EMAIL_SIGNIN_MESSAGE";
-pub const ENV_EMAIL_SIGNUP_MESSAGE: &str = "SECD_EMAIL_SIGNUP_MESSAGE";
-pub const ENV_SPICE_SECRET: &str = "SECD_SPICE_SECRET";
-pub const ENV_SPICE_SERVER: &str = "SECD_SPICE_SERVER";
-
const ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6;
const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5;
const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true;
+const API_TOKEN_SIZE_BYTES: usize = 64;
+const CREDENTIAL_PUBLIC_PART_BYTES: usize = 16;
const CRYPTER_SECRET_KEY_DEFAULT: &str = "sup3rs3cr3t";
const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */;
const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */;
@@ -55,7 +44,6 @@ pub type IdentityId = Uuid;
pub type MotifId = Uuid;
pub type PhoneNumber = String;
pub type RefId = Uuid;
-pub type SessionToken = String;
#[derive(Debug, derive_more::Display, thiserror::Error)]
pub enum SecdError {
@@ -67,11 +55,14 @@ pub enum SecdError {
AddressValidationExpiredOrConsumed,
CredentialAlreadyExists,
+ InvalidCredential,
+ CredentialIsNotApiToken,
CrypterError(#[from] CrypterError),
TooManyIdentities,
IdentityNotFound,
+ IdentityAlreadyExists,
EmailMessengerError(#[from] EmailMessengerError),
InvalidEmaillAddress(#[from] email_address::Error),
@@ -79,10 +70,13 @@ pub enum SecdError {
FailedToProvideSessionIdentity(String),
InvalidSession,
+ ParseAssetFileError(String),
+
StoreError(#[from] StoreError),
StoreInitFailure(String),
FailedToDecodeInput(#[from] hex::FromHexError),
+ DecodeError(String),
AuthorizationNotSupported(String),
SpiceClient(#[from] SpiceError),
@@ -97,11 +91,20 @@ pub struct Secd {
cfg: Cfg,
}
+#[derive(Debug, Deserialize)]
struct Cfg {
- email_address_from: Option<Mailbox>,
- email_address_replyto: Option<Mailbox>,
- email_signup_message: Option<String>,
+ auth_store_conn: String,
+ crypter_secret_key: Option<String>,
+ email_address_from: Option<String>,
+ email_address_replyto: Option<String>,
+ email_messenger: Option<String>,
+ email_sendgrid_api_key: Option<String>,
+ email_signin_message_asset_loc: Option<String>,
+ email_signup_message_asset_loc: Option<String>,
email_signin_message: Option<String>,
+ email_signup_message: Option<String>,
+ spice_secret: String,
+ spice_server: String,
}
#[async_trait]
@@ -191,9 +194,9 @@ pub struct AddressValidation {
#[serde(with = "time::serde::timestamp::option")]
pub validated_at: Option<OffsetDateTime>,
pub attempts: i32,
- #[serde_as(as = "serde_with::hex::Hex")]
+ #[serde_as(as = "Base64<UrlSafe, Unpadded>")]
hashed_token: Vec<u8>,
- #[serde_as(as = "serde_with::hex::Hex")]
+ #[serde_as(as = "Base64<UrlSafe, Unpadded>")]
hashed_code: Vec<u8>,
}
@@ -225,38 +228,19 @@ pub struct Credential {
}
#[serde_as]
-#[derive(Debug, Display, Serialize, Deserialize, EnumString)]
+#[derive(Clone, Debug, Display, Serialize, Deserialize, EnumString)]
pub enum CredentialType {
+ ApiToken { public: String, private: String },
Passphrase { key: String, value: String },
- Oidc { value: String },
- OneTimeCodes { codes: Vec<String> },
- // Totp {
- // #[serde_as(as = "DisplayFromStr")]
- // url: Url,
- // code: String,
- // },
- WebAuthn { value: String },
-}
-
-struct SecuredCredential {
- pub id: CredentialId,
- pub identity_id: IdentityId,
- pub t: CredentialType,
- pub created_at: OffsetDateTime,
- pub revoked_at: Option<OffsetDateTime>,
- pub deleted_at: Option<OffsetDateTime>,
-}
-
-enum SecuredCredentialType {
- Passphrase { key: String, value: String },
- Oidc { value: String },
- OneTimeCodes { codes: Vec<String> },
+ Session { key: String, secret: String },
+ // Oidc { key: String, value: String },
+ // OneTimeCodes { key: String, codes: Vec<String> },
// Totp {
// #[serde_as(as = "DisplayFromStr")]
// url: Url,
// code: String,
// },
- WebAuthn { value: String },
+ // WebAuthn { value: String },
}
#[serde_with::skip_serializing_none]
@@ -265,6 +249,8 @@ pub struct Identity {
pub id: IdentityId,
pub address_validations: Vec<AddressValidation>,
pub credentials: Vec<Credential>,
+ #[serde(skip_serializing_if = "Vec::is_empty")]
+ pub new_credentials: Vec<Credential>,
pub rules: Vec<String>, // TODO: rules for (e.g. mfa reqs)
pub metadata: Option<String>,
#[serde(with = "time::serde::timestamp")]
@@ -273,20 +259,22 @@ pub struct Identity {
pub deleted_at: Option<OffsetDateTime>,
}
-#[serde_as]
-#[serde_with::skip_serializing_none]
-#[derive(Debug, Serialize)]
-pub struct Session {
- pub identity_id: IdentityId,
- #[serde_as(as = "serde_with::hex::Hex")]
- #[serde(skip_serializing_if = "Vec::is_empty")]
- pub token: Vec<u8>,
- #[serde(with = "time::serde::timestamp")]
- pub created_at: OffsetDateTime,
- #[serde(with = "time::serde::timestamp")]
- pub expired_at: OffsetDateTime,
- #[serde(with = "time::serde::timestamp::option")]
- pub revoked_at: Option<OffsetDateTime>,
+impl Cfg {
+ fn resolve(&mut self) -> Result<(), SecdError> {
+ if let Some(path) = &self.email_signin_message_asset_loc {
+ self.email_signin_message = Some(read_to_string(path).map_err(|err| {
+ SecdError::ParseAssetFileError(format!("Email Sign In Asset [{}]: {}", path, err))
+ })?);
+ }
+
+ if let Some(path) = &self.email_signup_message_asset_loc {
+ self.email_signup_message = Some(read_to_string(path).map_err(|err| {
+ SecdError::ParseAssetFileError(format!("Email Sign In Asset [{}]: {}", path, err))
+ })?);
+ }
+
+ Ok(())
+ }
}
impl Secd {
@@ -294,16 +282,28 @@ impl Secd {
///
/// Initialize SecD with the specified configuration, established the necessary
/// constraints, persistance stores, and options.
- pub async fn init(z_schema: Option<&str>) -> Result<Self, SecdError> {
- let auth_store = AuthStore::from(var(ENV_AUTH_STORE_CONN_STRING).ok());
+ pub async fn init(cfg_path: Option<&str>, z_schema: Option<&str>) -> Result<Self, SecdError> {
+ let mut cfg: Cfg = Config::builder()
+ .add_source(config::File::with_name(cfg_path.unwrap_or(
+ &var("SECD_CONFIG_PATH").expect("coud not read SECD_CONFIG_PATH from environment"),
+ )))
+ .build()
+ .unwrap()
+ .try_deserialize()
+ .expect("failed to retrieve secd config.toml");
+
+ cfg.resolve()?;
+
+ let auth_store = AuthStore::from(Some(cfg.auth_store_conn.clone()));
let email_messenger = AuthEmailMessenger::from_str(
- &var(ENV_EMAIL_MESSENGER).unwrap_or(AuthEmailMessenger::Local.to_string()),
+ &cfg.email_messenger
+ .clone()
+ .unwrap_or(AuthEmailMessenger::Local.to_string()),
)
.expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5");
- let crypter_secret_key = var(ENV_CRYPTER_SECRET_KEY).unwrap_or_else(|_| {
+ let crypter_secret_key = cfg.crypter_secret_key.clone().unwrap_or_else(|| {
warn!(
- "NO CRYPTER KEY PROVIDED, USING DEFAULT KEY. DO NOT USE THIS KEY IN PRODUCTION. PROVIDE A UNIQUE SECRET KEY BY SETTING THE ENVIORNMENT VARIABLE {}. THE DEFAULT KEY IS: {}",
- ENV_CRYPTER_SECRET_KEY,
+ "NO CRYPTER KEY PROVIDED, USING DEFAULT KEY. DO NOT USE THIS KEY IN PRODUCTION. PROVIDE A UNIQUE SECRET KEY BY SETTING THE CONFIGURATION KEY. THE DEFAULT KEY IS: {}",
CRYPTER_SECRET_KEY_DEFAULT,
);
CRYPTER_SECRET_KEY_DEFAULT.to_string()
@@ -312,17 +312,6 @@ impl Secd {
info!("starting client with auth_store: {:?}", auth_store);
info!("starting client with email_messenger: {:?}", auth_store);
- let cfg = Cfg {
- email_address_from: var(ENV_EMAIL_ADDRESS_FROM)
- .ok()
- .and_then(|s| s.parse().ok()),
- email_address_replyto: var(ENV_EMAIL_ADDRESS_REPLYTO)
- .ok()
- .and_then(|s| s.parse().ok()),
- email_signup_message: var(ENV_EMAIL_SIGNUP_MESSAGE).ok(),
- email_signin_message: var(ENV_EMAIL_SIGNIN_MESSAGE).ok(),
- };
-
let store = match auth_store {
AuthStore::Sqlite { conn } => {
if z_schema.is_some() {
@@ -331,7 +320,7 @@ impl Secd {
));
}
- SqliteClient::new(
+ SqliteClient::new_ref(
sqlx::sqlite::SqlitePoolOptions::new()
.connect(&conn)
.await
@@ -342,7 +331,7 @@ impl Secd {
.await
}
AuthStore::Postgres { conn } => {
- PgClient::new(
+ PgClient::new_ref(
sqlx::postgres::PgPoolOptions::new()
.connect(&conn)
.await
@@ -352,7 +341,7 @@ impl Secd {
)
.await
}
- rest @ _ => {
+ rest => {
error!(
"requested an AuthStore which has not yet been implemented: {:?}",
rest
@@ -362,19 +351,22 @@ impl Secd {
};
let email_sender = match email_messenger {
- AuthEmailMessenger::Local => LocalMailer::new(),
- AuthEmailMessenger::Sendgrid => Sendgrid::new(
- var(ENV_EMAIL_SENDGRID_API_KEY).expect("No SENDGRID_API_KEY provided"),
+ AuthEmailMessenger::Local => LocalMailer::new_ref(),
+ AuthEmailMessenger::Sendgrid => Sendgrid::new_ref(
+ cfg.email_sendgrid_api_key
+ .clone()
+ .expect("No SENDGRID_API_KEY provided"),
),
_ => unimplemented!(),
};
let spice = match z_schema {
Some(schema) => {
- let c: Arc<Spice> = Arc::new(Spice::new().await);
+ let c: Arc<Spice> =
+ Arc::new(Spice::new(cfg.spice_secret.clone(), cfg.spice_server.clone()).await);
c.write_schema(schema)
.await
- .expect("failed to write authorization schema".into());
+ .unwrap_or_else(|_| panic!("{}", "failed to write authorization schema"));
Some(c)
}
None => None,
diff --git a/crates/secd/src/util/crypter.rs b/crates/secd/src/util/crypter.rs
index 1717377..e5ec796 100644
--- a/crates/secd/src/util/crypter.rs
+++ b/crates/secd/src/util/crypter.rs
@@ -3,8 +3,8 @@ use aes_gcm::{
Aes256Gcm, Nonce,
};
use argon2::{
- password_hash::{self, SaltString},
- Argon2, PasswordHasher,
+ password_hash::{Salt, SaltString},
+ Argon2, PasswordHash, PasswordHasher,
};
use derive_more::Display;
use rand::Rng;
@@ -13,10 +13,10 @@ use thiserror::Error;
#[derive(Debug, Display, Error)]
pub enum CrypterError {
- EncryptError(String),
- DecryptError(String),
- DecodeError(String),
- HashError(String),
+ Encrypt(String),
+ Decrypt(String),
+ Decode(String),
+ Hash(String),
}
pub struct Crypter {
@@ -37,8 +37,8 @@ impl Crypter {
let rbs = rand::thread_rng().gen::<[u8; 12]>();
let iv = Nonce::from_slice(&rbs);
let crypt = cipher
- .encrypt(&iv, data)
- .map_err(|e| CrypterError::EncryptError(e.to_string()))?;
+ .encrypt(iv, data)
+ .map_err(|e| CrypterError::Encrypt(e.to_string()))?;
let mut msg = iv.to_vec();
msg.extend_from_slice(&crypt);
@@ -53,19 +53,36 @@ impl Crypter {
let iv = Nonce::from_slice(&data[0..=11]);
let data = &data[12..];
- Ok(cipher
- .decrypt(&iv, data)
- .map_err(|e| CrypterError::DecryptError(e.to_string()))?)
+ cipher
+ .decrypt(iv, data)
+ .map_err(|e| CrypterError::Decrypt(e.to_string()))
}
- pub fn hash(&self, data: &[u8]) -> Result<String, CrypterError> {
- let salt = SaltString::generate(&mut OsRng);
+ /// Hash data.
+ /// If a candidate is provided, then use that candidate's salt, passed as a full phc string.
+ pub fn hash(&self, data: &[u8], phc_candidate: Option<&str>) -> Result<String, CrypterError> {
+ let salt = match phc_candidate {
+ None => SaltString::generate(&mut OsRng).as_str().to_owned(),
+ Some(phc) => PasswordHash::new(phc)
+ .map_err(|e| CrypterError::Hash(e.to_string()))?
+ .salt.expect("unreachable fbacabb8-082a-423a-abff-06a961dc5828 [no salt found means a forced error since all resources, even those of high entropy, are salted]").as_str().to_owned(),
+ };
+
let hasher = Argon2::default();
Ok(hasher
- .hash_password(data, &salt)
- .map_err(|e| CrypterError::HashError(e.to_string()))?
+ .hash_password(
+ data,
+ Salt::from_b64(&salt).map_err(|e| CrypterError::Hash(e.to_string()))?,
+ )
+ .map_err(|e| CrypterError::Hash(e.to_string()))?
.to_string())
}
+
+ pub fn weak_hash(&self, data: &[u8]) -> Result<String, CrypterError> {
+ let mut hasher = Sha256::new();
+ hasher.update(data);
+ Ok(hex::encode(hasher.finalize()))
+ }
}
#[cfg(test)]
diff --git a/crates/secd/src/util/from.rs b/crates/secd/src/util/from.rs
index ec5b62d..7401468 100644
--- a/crates/secd/src/util/from.rs
+++ b/crates/secd/src/util/from.rs
@@ -3,7 +3,7 @@ use crate::AuthStore;
impl From<Option<String>> for AuthStore {
fn from(s: Option<String>) -> Self {
let conn = s.clone().unwrap_or("sqlite::memory:".into());
- match conn.split(":").next() {
+ match conn.split(':').next() {
Some("postgresql") | Some("postgres") => AuthStore::Postgres { conn },
Some("sqlite") => AuthStore::Sqlite { conn },
_ => panic!(
diff --git a/crates/secd/src/util/mod.rs b/crates/secd/src/util/mod.rs
index 8676f26..fb984d1 100644
--- a/crates/secd/src/util/mod.rs
+++ b/crates/secd/src/util/mod.rs
@@ -3,12 +3,14 @@ pub(crate) mod from;
use self::crypter::{Crypter, CrypterError};
use crate::{
- AddressType, Credential, CredentialType, IdentityId, SecdError, Session, SESSION_DURATION,
- SESSION_SIZE_BYTES,
+ AddressType, Credential, CredentialType, IdentityId, SecdError, API_TOKEN_SIZE_BYTES,
+ CREDENTIAL_PUBLIC_PART_BYTES, SESSION_DURATION, SESSION_SIZE_BYTES,
};
+use base64::{engine::general_purpose, Engine as _};
use sha2::{Digest, Sha256};
use std::str::from_utf8;
use time::OffsetDateTime;
+use uuid::Uuid;
pub(crate) fn hash(i: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
@@ -16,76 +18,265 @@ pub(crate) fn hash(i: &[u8]) -> Vec<u8> {
hasher.finalize().to_vec()
}
+pub trait ErrorContext<T, E: std::error::Error> {
+ fn ctx(self, err: &str) -> Result<T, E>;
+}
+
+impl<T, E: std::error::Error> ErrorContext<T, E> for Result<T, E> {
+ fn ctx(self, err: &str) -> Result<T, E> {
+ self.map_err(|e| {
+ log::error!("{} [{}]", err, e.to_string());
+ e
+ })
+ }
+}
+
impl AddressType {
pub fn get_value(&self) -> Option<String> {
match &self {
- AddressType::Email { email_address } => {
- email_address.as_ref().map(|a| a.to_string().clone())
- }
+ AddressType::Email { email_address } => email_address.as_ref().map(|a| a.to_string()),
AddressType::Sms { phone_number } => phone_number.as_ref().cloned(),
}
}
}
-impl Session {
- pub(crate) fn new(identity_id: IdentityId) -> Result<Self, SecdError> {
- let token = (0..SESSION_SIZE_BYTES)
+impl CredentialType {
+ pub fn new_api_token() -> Result<Self, SecdError> {
+ let public = general_purpose::URL_SAFE_NO_PAD.encode(
+ (0..CREDENTIAL_PUBLIC_PART_BYTES)
+ .map(|_| rand::random::<u8>())
+ .collect::<Vec<u8>>(),
+ );
+
+ let private = general_purpose::URL_SAFE_NO_PAD.encode(
+ (0..API_TOKEN_SIZE_BYTES)
+ .map(|_| rand::random::<u8>())
+ .collect::<Vec<u8>>(),
+ );
+
+ Ok(CredentialType::ApiToken { public, private })
+ }
+
+ pub fn session_from_str(token: &str) -> Result<Self, SecdError> {
+ let decoded = general_purpose::URL_SAFE_NO_PAD
+ .decode(token)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+
+ let key =
+ general_purpose::URL_SAFE_NO_PAD.encode(&decoded[0..CREDENTIAL_PUBLIC_PART_BYTES]);
+ let secret =
+ general_purpose::URL_SAFE_NO_PAD.encode(&decoded[CREDENTIAL_PUBLIC_PART_BYTES..]);
+
+ Ok(CredentialType::Session { key, secret })
+ }
+
+ pub fn api_token_from_str(token: &str) -> Result<Self, SecdError> {
+ let decoded = general_purpose::URL_SAFE_NO_PAD
+ .decode(token)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+
+ let public =
+ general_purpose::URL_SAFE_NO_PAD.encode(&decoded[0..CREDENTIAL_PUBLIC_PART_BYTES]);
+ let private =
+ general_purpose::URL_SAFE_NO_PAD.encode(&decoded[CREDENTIAL_PUBLIC_PART_BYTES..]);
+
+ Ok(CredentialType::ApiToken { public, private })
+ }
+
+ pub fn session_token(&self) -> Result<String, SecdError> {
+ match self {
+ CredentialType::Session { key, secret } => {
+ let key_bytes = general_purpose::URL_SAFE_NO_PAD
+ .decode(key)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+ let secret_bytes = general_purpose::URL_SAFE_NO_PAD
+ .decode(secret)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+
+ let mut input = key_bytes;
+ input.extend(secret_bytes);
+
+ Ok(general_purpose::URL_SAFE_NO_PAD.encode(input))
+ }
+ _ => Err(SecdError::InvalidCredential),
+ }
+ .ctx("the credential type is not a session")
+ }
+
+ pub fn api_token(&self) -> Result<String, SecdError> {
+ match self {
+ CredentialType::ApiToken { public, private } => {
+ let public_bytes = general_purpose::URL_SAFE_NO_PAD
+ .decode(public)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+ let private_bytes = general_purpose::URL_SAFE_NO_PAD
+ .decode(private)
+ .map_err(|e| SecdError::DecodeError(e.to_string()))?;
+
+ let mut input = public_bytes;
+ input.extend(private_bytes);
+
+ Ok(general_purpose::URL_SAFE_NO_PAD.encode(input))
+ }
+ _ => Err(SecdError::InvalidCredential),
+ }
+ .ctx("the credential type is not an api token")
+ }
+}
+
+impl Credential {
+ pub fn new_session(identity_id: IdentityId) -> Result<Self, SecdError> {
+ let key = (0..CREDENTIAL_PUBLIC_PART_BYTES)
+ .map(|_| rand::random::<u8>())
+ .collect::<Vec<u8>>();
+
+ let secret = (0..SESSION_SIZE_BYTES)
.map(|_| rand::random::<u8>())
.collect::<Vec<u8>>();
+
let now = OffsetDateTime::now_utc();
- Ok(Session {
+ Ok(Credential {
+ id: Uuid::new_v4(),
identity_id,
- token,
+ t: CredentialType::Session {
+ key: general_purpose::URL_SAFE_NO_PAD.encode(key),
+ secret: general_purpose::URL_SAFE_NO_PAD.encode(secret),
+ },
created_at: now,
- expired_at: now
- .checked_add(time::Duration::new(SESSION_DURATION, 0))
- .ok_or(SecdError::Todo)?,
- revoked_at: None,
+ revoked_at: Some(
+ now.checked_add(time::Duration::new(SESSION_DURATION, 0))
+ .ok_or(SecdError::Todo)?,
+ ),
+ deleted_at: None,
})
}
-}
-impl Credential {
- pub(crate) fn encrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
- Ok(match self.t {
- CredentialType::Passphrase {
- key: _,
- ref mut value,
+ pub fn encrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ match self.t {
+ CredentialType::ApiToken {
+ ref mut private, ..
} => {
+ *private = hex::encode(crypter.encrypt(private.as_bytes())?);
+ }
+ CredentialType::Passphrase { ref mut value, .. } => {
*value = hex::encode(crypter.encrypt(value.as_bytes())?);
}
- _ => {}
- })
+ CredentialType::Session { ref mut secret, .. } => {
+ *secret = hex::encode(crypter.encrypt(secret.as_bytes())?);
+ }
+ };
+ Ok(())
}
- pub(crate) fn decrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
- Ok(match self.t {
- CredentialType::Passphrase {
- key: _,
- ref mut value,
+ pub fn decrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ match self.t {
+ CredentialType::ApiToken {
+ ref mut private, ..
} => {
- *value = from_utf8(
+ *private = from_utf8(
&crypter.decrypt(
- &hex::decode(value.clone())
- .map_err(|e| CrypterError::DecodeError(e.to_string()))?,
+ &hex::decode(private.clone())
+ .map_err(|e| CrypterError::Decode(e.to_string()))?,
)?,
)
- .map_err(|e| CrypterError::DecodeError(e.to_string()))?
+ .map_err(|e| CrypterError::Decode(e.to_string()))?
.to_string()
}
- _ => {}
- })
+ CredentialType::Passphrase { ref mut value, .. } => {
+ *value = from_utf8(&crypter.decrypt(
+ &hex::decode(value.clone()).map_err(|e| CrypterError::Decode(e.to_string()))?,
+ )?)
+ .map_err(|e| CrypterError::Decode(e.to_string()))?
+ .to_string()
+ }
+ CredentialType::Session { ref mut secret, .. } => {
+ *secret = from_utf8(
+ &crypter.decrypt(
+ &hex::decode(secret.clone())
+ .map_err(|e| CrypterError::Decode(e.to_string()))?,
+ )?,
+ )
+ .map_err(|e| CrypterError::Decode(e.to_string()))?
+ .to_string()
+ }
+ };
+ Ok(())
}
- pub(crate) fn hash(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
- Ok(match self.t {
- CredentialType::Passphrase {
- key: _,
- ref mut value,
+ pub fn hash(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ match self.t {
+ CredentialType::ApiToken {
+ ref mut private, ..
} => {
- *value = crypter.hash(value.as_bytes())?;
+ *private = crypter.weak_hash(private.as_bytes())?;
}
- _ => {}
- })
+ CredentialType::Passphrase { ref mut value, .. } => {
+ *value = crypter.hash(value.as_bytes(), None)?;
+ }
+ CredentialType::Session { ref mut secret, .. } => {
+ *secret = crypter.weak_hash(secret.as_bytes())?;
+ }
+ };
+ Ok(())
+ }
+
+ pub fn hash_compare(
+ &mut self,
+ plaintext: &CredentialType,
+ crypter: &Crypter,
+ ) -> Result<(), SecdError> {
+ match (&self.t, plaintext) {
+ (
+ CredentialType::ApiToken {
+ public: current_public,
+ private: current_private,
+ },
+ CredentialType::ApiToken {
+ public: plaintext_public,
+ private: plaintext_private,
+ },
+ ) => {
+ let plaintext_hash = crypter.weak_hash(plaintext_private.as_bytes())?;
+ if plaintext_public != current_public || &plaintext_hash != current_private {
+ return Err(SecdError::InvalidCredential);
+ }
+ }
+ (
+ CredentialType::Passphrase {
+ key: current_key,
+ value: current_value,
+ },
+ CredentialType::Passphrase {
+ key: plaintext_key,
+ value: plaintext_value,
+ },
+ ) => {
+ let plaintext_hash =
+ crypter.hash(plaintext_value.as_bytes(), Some(current_value))?;
+ if plaintext_key != current_key || &plaintext_hash != current_value {
+ return Err(SecdError::InvalidCredential);
+ }
+ }
+ (
+ CredentialType::Session {
+ key: current_key,
+ secret: current_secret,
+ },
+ CredentialType::Session {
+ key: plaintext_key,
+ secret: plaintext_secret,
+ },
+ ) => {
+ let plaintext_hash = crypter.weak_hash(plaintext_secret.as_bytes())?;
+ if plaintext_key != current_key || &plaintext_hash != current_secret {
+ return Err(SecdError::InvalidCredential);
+ }
+ }
+ _ => panic!(
+ "unreachable 78cfff7c-5493-42c5-add7-044241b3d713 [different credential types]"
+ ),
+ }
+
+ Ok(())
}
}
diff --git a/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
index 0cf3fa0..0fd423e 100644
--- a/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
+++ b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
@@ -36,11 +36,10 @@ create table if not exists secd.credential (
, created_at timestamptz not null
, revoked_at timestamptz
, deleted_at timestamptz
+ , unique(partial_key)
);
-create unique index if not exists credential_passphrase_type_key_ix
-on secd.credential (partial_key)
-where type = 'Passphrase';
+create unique index if not exists credential_partial_key_type_key_ix on secd.credential (partial_key);
create table if not exists secd.address (
address_id bigserial primary key
diff --git a/crates/secd/store/pg/sql/find_credential.sql b/crates/secd/store/pg/sql/find_credential.sql
index e30c0ea..1736500 100644
--- a/crates/secd/store/pg/sql/find_credential.sql
+++ b/crates/secd/store/pg/sql/find_credential.sql
@@ -9,4 +9,4 @@ join secd.identity i using (identity_id)
where (($1::uuid is null) or (c.credential_public_id = $1))
and (($2::uuid is null) or (i.identity_public_id = $2))
and (($3::text is null) or (c.type = $3))
-and (($3::text is null or $4::text is null) or (c.data->$3->>'key' = $4))
+and (($3::text is null or $4::text is null) or (c.partial_key = $4))
diff --git a/crates/secd/store/pg/sql/find_identity.sql b/crates/secd/store/pg/sql/find_identity.sql
index 37105cb..41c8518 100644
--- a/crates/secd/store/pg/sql/find_identity.sql
+++ b/crates/secd/store/pg/sql/find_identity.sql
@@ -7,9 +7,7 @@ select distinct
from secd.identity i
left join secd.address_validation av using (identity_id)
left join secd.address a using (address_id)
-left join secd.session s using (identity_id)
where (($1::uuid is null) or (i.identity_public_id = $1))
and (($2::text is null) or (a.value = $2))
and (($3::bool is null) or (($3::bool is true) and (av.validated_at is not null)))
-and (($4::bytea is null) or (s.token_hash = $4))
and i.deleted_at is null;
diff --git a/crates/secd/store/pg/sql/find_session.sql b/crates/secd/store/pg/sql/find_session.sql
deleted file mode 100644
index ca58480..0000000
--- a/crates/secd/store/pg/sql/find_session.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-select distinct
- i.identity_public_id
- , s.created_at
- , s.expired_at
- , s.revoked_at
-from secd.session s
-join secd.identity i using (identity_id)
-where (($1::bytea is null) or (s.token_hash = $1))
-and (($2::uuid is null) or (i.identity_public_id = $2))
-and (($3::timestamptz is null) or (s.expired_at > $3))
-and ((revoked_at is null) or ($4::timestamptz is null) or (s.revoked_at > $4));
diff --git a/crates/secd/store/pg/sql/write_credential.sql b/crates/secd/store/pg/sql/write_credential.sql
index 17e03a2..ecaf523 100644
--- a/crates/secd/store/pg/sql/write_credential.sql
+++ b/crates/secd/store/pg/sql/write_credential.sql
@@ -16,4 +16,6 @@ insert into secd.credential (
, $6
, $7
, $8
-);
+) on conflict (partial_key) do update
+ set revoked_at = excluded.revoked_at
+ , deleted_at = excluded.deleted_at;
diff --git a/crates/secd/store/pg/sql/write_identity.sql b/crates/secd/store/pg/sql/write_identity.sql
index 4b2745b..e86d2f5 100644
--- a/crates/secd/store/pg/sql/write_identity.sql
+++ b/crates/secd/store/pg/sql/write_identity.sql
@@ -9,4 +9,5 @@ insert into secd.identity (
) on conflict (identity_public_id) do update
set data = excluded.data
, updated_at = excluded.updated_at
- , deleted_at = excluded.deleted_at;
+ , deleted_at = excluded.deleted_at
+returning (xmax = 0);
diff --git a/crates/secd/store/pg/sql/write_session.sql b/crates/secd/store/pg/sql/write_session.sql
deleted file mode 100644
index aa9c0a1..0000000
--- a/crates/secd/store/pg/sql/write_session.sql
+++ /dev/null
@@ -1,11 +0,0 @@
-insert into secd.session (
- identity_id
- , token_hash
- , created_at
- , expired_at
- , revoked_at
-) values (
- (select identity_id from secd.identity where identity_public_id = $1)
- , $2, $3, $4, $5
-) on conflict (token_hash) do update
- set revoked_at = excluded.revoked_at;