aboutsummaryrefslogtreecommitdiff
path: root/crates
diff options
context:
space:
mode:
authorbenj <benj@rse8.com>2023-04-24 13:24:45 -0700
committerbenj <benj@rse8.com>2023-04-24 13:24:45 -0700
commiteb92f823c31a5e702af7005231f0d6915aad3342 (patch)
treebb624786a47accb2dfcfe95d20c00c9624c28a9c /crates
parent176aae037400b43cb3971cd968afe59c73b3097a (diff)
downloadsecdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar.gz
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar.bz2
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar.lz
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar.xz
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.tar.zst
secdiam-eb92f823c31a5e702af7005231f0d6915aad3342.zip
email templates, sendgrid, creds, and some experimental things
Started playing with namespace configs and integrating with zanzibar impls. Still lot's of experimenting and dead code going on.
Diffstat (limited to 'crates')
-rw-r--r--crates/iam/src/api.rs212
-rw-r--r--crates/iam/src/main.rs247
-rw-r--r--crates/secd/Cargo.toml8
-rw-r--r--crates/secd/README.md57
-rw-r--r--crates/secd/assets/default_login_email.html0
-rw-r--r--crates/secd/assets/default_signup_email.html0
-rw-r--r--crates/secd/assets/default_sms_login.txt0
-rw-r--r--crates/secd/assets/default_sms_signup.txt0
-rw-r--r--crates/secd/src/auth/n.rs166
-rw-r--r--crates/secd/src/auth/z/graph.rs209
-rw-r--r--crates/secd/src/auth/z/mod.rs (renamed from crates/secd/src/auth/z.rs)43
-rw-r--r--crates/secd/src/client/email/mod.rs186
-rw-r--r--crates/secd/src/client/store/mod.rs78
-rw-r--r--crates/secd/src/client/store/sql_db.rs132
-rw-r--r--crates/secd/src/lib.rs183
-rw-r--r--crates/secd/src/util/crypter.rs87
-rw-r--r--crates/secd/src/util/from.rs5
-rw-r--r--crates/secd/src/util/mod.rs107
-rw-r--r--crates/secd/store/pg/migrations/20221222002434_bootstrap.sql28
-rw-r--r--crates/secd/store/pg/sql/find_credential.sql12
-rw-r--r--crates/secd/store/pg/sql/write_credential.sql19
-rw-r--r--crates/secd/store/pg/sql/write_identity.sql2
-rw-r--r--crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql8
-rw-r--r--crates/secd/store/sqlite/sql/find_credential.sql12
-rw-r--r--crates/secd/store/sqlite/sql/write_credential.sql19
25 files changed, 1273 insertions, 547 deletions
diff --git a/crates/iam/src/api.rs b/crates/iam/src/api.rs
index ace3199..af175a7 100644
--- a/crates/iam/src/api.rs
+++ b/crates/iam/src/api.rs
@@ -1,7 +1,6 @@
use crate::ISSUE_TRACKER_LOC;
use clap::{Parser, Subcommand, ValueEnum};
use colored::*;
-use secd::IdentityId;
use serde::{Deserialize, Serialize};
use thiserror;
use url::Url;
@@ -81,52 +80,23 @@ pub enum Command {
object: DevObject,
},
#[command(
- about = "Get details for a specific IAM object",
+ about = "List and filter IAM objects",
long_about = "Get\n\nGet details for a specific IAM object"
)]
Get {
#[command(subcommand)]
object: GetObject,
},
+ /// Start the iam repl to more easily interact with iam and its primitives
+ Repl,
#[command(
- about = "Initialize an IAM store (alias for `iam admin init`)",
- long_about = "Init\n\nInitalize a new IAM admin store and save the store's configuration profile. This command is an alias for, and thus equiavlent to, `iam admin init`."
- )]
- Init {
- /// If true, interactively initialize an IAM store. Otherwise output a template config.
- #[arg(long, short, action)]
- interactive: bool,
- },
- #[command(
- about = "Link multiple IAM objects together",
- long_about = "Link\n\nCleave different IAM entities to create an IAM system."
- )]
- Link {
- #[command(subcommand)]
- object: LinkObject,
- /// Unlink the provided entities rather than link them.
- #[arg(long, short, action)]
- unlink: bool,
- },
- #[command(
- about = "List and filter IAM objects",
- long_about = "List\n\nPage through collections of IAM objects with optional filtering"
+ about = "Update specified IAM object",
+ long_about = "Update\n\nUpdate details for the specified IAM object"
)]
- Ls {
+ Update {
#[command(subcommand)]
- object: ListObject,
- /// Regex filter for entity names
- #[arg(long, short)]
- name: Option<String>,
- /// Only fetch entities created after this time
- #[arg(long, short)]
- after: Option<i64>,
- /// Only fetch entities created before this time
- #[arg(long, short)]
- before: Option<i64>,
+ object: UpdateObject,
},
- /// Start the iam repl to more easily interact with iam and its primitives
- Repl,
}
#[derive(Subcommand)]
@@ -232,86 +202,13 @@ pub enum VoiceProvider {
#[derive(Subcommand)]
pub enum CreateObject {
- #[command(
- about = "A set of long-lived tokens which authorize an identity",
- long_about = "Api Keys\n\nApi keys are long lived identifiers which authenticate and authorize a identity. Keys have a public and private part,\nwhich may be shared and must be kept private, respectively. Unlike sessions, api keys may be long-lived (infinite) or\nset to expire within certain timeframes."
- )]
- ApiKey {
- /// Identity against which this api key will be linked
- identity: Uuid,
- /// Time this api key expires (epoch time)
- expired_at: Option<i64>,
- },
- #[command(
- about = "A collection of identities",
- long_about = "Group\n\nA group may be created to operate simultaneously against a collection of identities. An identity may be part of mutliple groups, but it may not be part of the same group more than once."
- )]
- Group {
- /// The unique name for this group
- name: String,
- /// An optional set of identities to link against this group
- identities: Vec<Uuid>,
- },
- #[command(
- about = "A collection of services and service actions",
- long_about = "Permission\n\nA permission may be created to operate simultaneously against a collection of services and service actions. A service or service action may be part of mutliple permissions, but it may not be part of the same permission more than once. A permission may be used when many services and service actions are linked and unlinked against a role."
- )]
- Permission {
- /// An optional set of services to link against this permission
- #[arg(long, short)]
- services: Vec<Uuid>,
- /// An optional set of service actions to link against this permission
- #[arg(long, short)]
- actions: Vec<Uuid>,
- },
- #[command(
- about = "A collection of permissions",
- long_about = "Role\n\nA role may be created to operate simultaneously against a collection of permissions. A permission may be part of mutliple roles, but it may not be part of the same role more than once. A role may be used when many entities (such as groups or identities) are linked and unlinked against many permissions."
- )]
- Role {
- /// The unique name for this role
- name: String,
- /// An optional set of permissions to link against this role
- permissions: Vec<Uuid>,
- },
- #[command(
- about = "An entity for which an action may be authorized",
- long_about = "Service\n\nA service is an atomic entity which requires authorization. While a service's authorization may be subdivided by service actions, a service represents a logical element of authorization separation."
- )]
- Service {
- /// The unique name for this service
- name: String,
- /// URI for this service which may be used to resolve authorization
- #[arg(long, short)]
- uri: Option<String>,
- },
- #[command(
- about = "A specific authorization action by a service",
- long_about = "Service Action\n\nA service action is a domain specific action which defines what an identity authorization within that service. A service action may be a simple boolean value or a more complex express which is evaluated at runtime. For example, a boolean action may be something like `can_read_salary_table`, and a more complex action may be `readable_table_rows(datetime)` which executes at runtime and returns a value (or list of values) the service may use to determine authorization. Service actions are used as an inversion of control pattern to ensure that services do not need to worry about specific authorization actions for identities. A service action is unnecessary if the service has no specific authorization logic."
- )]
- ServiceAction {
- /// The unique name for this service action
- name: String,
- /// Program executed for this service action
- #[arg(long, short)]
- program: Option<String>,
- },
- #[command(
- about = "A timebound token which authorizes an identity",
- long_about = "Session\n\nA session is an opaque timebound token which allows an identity to authorize against IAM services. The session may be created by providing a validation request id and secret challenge code"
- )]
- Session {
- /// The validation id associated with a non-expired valid validation
- #[arg(long, short)]
- validation_id: Uuid,
- /// The secret code associated with this validation.
+ Credential {
+ #[command(subcommand)]
+ method: CredentialMethod,
+ /// The identity against which to associate this credential. A new identity will be created if no identity is provided.
#[arg(long, short)]
- secret_code: String,
+ identity_id: Option<Uuid>,
},
- #[command(
- about = "An action which initiates an address validation",
- long_about = "Validation\n\nA validation requires that the identity authenticate in some way, either by providing IAM managed credentials, an external gated mechanism (e.g. email, phone, or hardware key), or through a secondary authentication provider (oauth, saml, ldap, kerberos)."
- )]
Validation {
/// Method by which the validation will occur
#[command(subcommand)]
@@ -350,6 +247,17 @@ pub enum DevObject {
}
#[derive(Subcommand)]
+pub enum CredentialMethod {
+ /// A
+ Passphrase {
+ /// B
+ username: String,
+ /// C
+ passphrase: String,
+ },
+}
+
+#[derive(Subcommand)]
pub enum ValidationMethod {
/// An email address to which the validation will be sent
Email {
@@ -378,8 +286,12 @@ pub enum GetObject {
id: Option<Uuid>,
},
Identity {
+ /// The unique id corresponding to this identity.
+ #[arg(long, short)]
+ identity_id: Option<Uuid>,
/// Any session corresponding to this identity.
- session_token: String,
+ #[arg(long, short)]
+ session_token: Option<String>,
},
Permission {
/// Unique permission name
@@ -420,72 +332,16 @@ pub enum GetObject {
}
#[derive(Subcommand)]
-pub enum LinkObject {
- Group {
- group_name: String,
- #[arg(short, long, alias = "id")]
- group_id: Option<Uuid>,
-
- identity_ids: Vec<Uuid>,
- },
+pub enum UpdateObject {
Identity {
- identity_id: Uuid,
-
- group_names: Vec<String>,
- #[arg(long)]
- group_ids: Vec<Uuid>,
- },
- Permission {
- permission_name: String,
- #[arg(short, long, alias = "id")]
- permission_id: Option<Uuid>,
-
- role_names: Vec<String>,
- #[arg(long)]
- role_ids: Vec<Uuid>,
- },
- Role {
- role_name: String,
- #[arg(short, long, alias = "id")]
- role_id: Option<Uuid>,
-
- permission_names: Vec<String>,
- #[arg(long)]
- permission_ids: Vec<Uuid>,
- },
- Service {
- service_name: String,
- #[arg(short, long, alias = "id")]
- service_id: Option<Uuid>,
-
- permission_names: Vec<String>,
- #[arg(long)]
- permission_ids: Vec<Uuid>,
- },
- ServiceAction {
- service_action_name: String,
- #[arg(short, long, alias = "id")]
- service_action_id: Option<Uuid>,
-
- service_name: Vec<String>,
- #[arg(long)]
- service_ids: Vec<Uuid>,
+ /// Unique identifier for this identity.
+ id: Uuid,
+ /// Metadata for this identity. Note, structured metadata must be configured to be enforced.
+ #[arg(long, short)]
+ metadata: Option<String>,
},
}
-#[derive(Subcommand)]
-pub enum ListObject {
- ApiKey,
- Group,
- Identity,
- Permission,
- Role,
- Session,
- Service,
- ServiceAction,
- Validation,
-}
-
#[derive(Serialize, Deserialize)]
pub struct Config {
pub profile: Vec<ConfigProfile>,
diff --git a/crates/iam/src/main.rs b/crates/iam/src/main.rs
index ae44b46..c679af6 100644
--- a/crates/iam/src/main.rs
+++ b/crates/iam/src/main.rs
@@ -3,14 +3,14 @@ mod command;
mod util;
use anyhow::bail;
-use api::{
- AdminAction, Args, CliError, Command, CreateObject, DevObject, GetObject, LinkObject,
- ListObject,
-};
+use api::{AdminAction, Args, CliError, Command, CreateObject, DevObject, GetObject, UpdateObject};
use clap::Parser;
use command::dev_oauth2_listen;
use env_logger::Env;
-use secd::{auth::z, Secd, ENV_AUTH_STORE_CONN_STRING, ENV_SPICE_SECRET, ENV_SPICE_SERVER};
+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;
@@ -38,8 +38,7 @@ async fn main() {
async fn exec() -> Result<Option<String>> {
let args = Args::parse();
Ok(match args.command {
- Command::Init { interactive }
- | Command::Admin {
+ Command::Admin {
action: AdminAction::Init { interactive },
} => {
command::admin_init(interactive)
@@ -52,89 +51,25 @@ async fn exec() -> Result<Option<String>> {
// 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(Some(
- r#"
-definition user {}
-
-definition organization {
- relation r_member: user
- relation r_admin: user
-
- permission member = r_admin + r_member
- permission admin = r_admin
-}
-
-definition plugin {
- relation r_creator: user | organization#admin
- relation r_editor: user
- relation r_viewer: user
-
- permission creator = r_creator + r_creator->admin
- permission editor = r_editor + r_creator + r_creator->admin
- permission viewer = r_viewer + r_editor + r_creator + r_creator->admin
-}"#,
- ))
- .await
- .map_err(|e| CliError::SecdInitializationFailure(e.to_string()))?;
-
- secd.write(&vec![z::Relationship {
- subject: z::Subject::User((
- "user".into(),
- Uuid::parse_str("cd1e74de-6107-4191-a7b2-a142c549a9af").unwrap(),
- )),
- object: (
- "organization".into(),
- Uuid::parse_str("862f38b5-7f88-4b55-800f-af8da059e3a7").unwrap(),
- ),
- relation: "r_member".into(),
- }])
- .await
- .unwrap();
-
- let y = match secd
- .check(&z::Relationship {
- subject: z::Subject::User((
- "user".into(),
- Uuid::parse_str("cd1e74de-6107-4191-a7b2-a142c549a9af").unwrap(),
- )),
- object: (
- "organization".into(),
- Uuid::parse_str("862f38b5-7f88-4b55-800f-af8da059e3a7").unwrap(),
- ),
- relation: "member".into(),
- })
+ let secd = Secd::init(None)
.await
- {
- Ok(v) => v,
- Err(e) => panic!("fooooooooooooooooooooooooooooooooooooooooooooooo"),
- };
-
- println!("DID I HAZ IT? {:#?}", y);
+ .map_err(|e| CliError::SecdInitializationFailure(e.to_string()))?;
match rest {
Command::Admin { action } => admin(&secd, action).await?,
Command::Create { object } => create(&secd, object).await?,
Command::Dev { object } => dev(object).await?,
Command::Get { object } => get(&secd, object).await?,
- Command::Init { .. } => bail!(CliError::InternalError(error_detail(
- "4a696b66-6231-4a2f-811c-4448a41473d2",
- "Code path should be unreachable",
- ))),
- Command::Link { object, unlink } => todo!(),
- Command::Ls {
- object,
- name,
- before,
- after,
- } => todo!(),
Command::Repl => {
unimplemented!()
}
+ Command::Update { object } => update(&secd, object).await?,
}
}
})
@@ -162,37 +97,23 @@ async fn admin(secd: &Secd, cmd: AdminAction) -> Result<Option<String>> {
}
async fn create(secd: &Secd, cmd: CreateObject) -> Result<Option<String>> {
Ok(match cmd {
- CreateObject::ApiKey {
- identity,
- expired_at,
+ CreateObject::Credential {
+ method,
+ identity_id,
} => {
- println!("create object");
- None
- }
- CreateObject::Group { name, identities } => {
- println!("create group");
- None
- }
- CreateObject::Permission { services, actions } => {
- println!("create permission");
- None
- }
- CreateObject::Role { name, permissions } => {
- println!("create role");
- None
- }
- CreateObject::Service { name, uri } => {
- println!("create service");
- None
- }
- CreateObject::ServiceAction { name, program } => {
- println!("create service action");
- None
+ let t = match method {
+ api::CredentialMethod::Passphrase {
+ username,
+ passphrase,
+ } => CredentialType::Passphrase {
+ key: username,
+ value: passphrase,
+ },
+ };
+
+ let credential = secd.create_credential(t, identity_id).await?;
+ Some(serde_json::ser::to_string(&credential)?.to_string())
}
- CreateObject::Session {
- validation_id,
- secret_code,
- } => todo!(),
CreateObject::Validation {
method,
identity_id,
@@ -236,9 +157,13 @@ async fn get(secd: &Secd, cmd: GetObject) -> Result<Option<String>> {
println!("get object group");
None
}
- GetObject::Identity { session_token } => {
- Some(serde_json::ser::to_string(&secd.get_identity(&session_token).await?)?.to_string())
- }
+ 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");
@@ -265,105 +190,17 @@ async fn get(secd: &Secd, cmd: GetObject) -> Result<Option<String>> {
}
})
}
-async fn link(secd: &Secd, cmd: LinkObject, should_unlink: bool) -> Result<Option<String>> {
- Ok(match cmd {
- LinkObject::Group {
- group_name,
- group_id,
- identity_ids,
- } => {
- println!("link object group");
- None
- }
- LinkObject::Identity {
- identity_id,
- group_names,
- group_ids,
- } => {
- println!("link object identity");
- None
- }
- LinkObject::Permission {
- permission_name,
- permission_id,
- role_names,
- role_ids,
- } => {
- println!("link object permission");
- None
- }
- LinkObject::Role {
- role_name,
- role_id,
- permission_names,
- permission_ids,
- } => {
- println!("link object role");
- None
- }
- LinkObject::Service {
- service_name,
- service_id,
- permission_names,
- permission_ids,
- } => {
- println!("link object service");
- None
- }
- LinkObject::ServiceAction {
- service_action_name,
- service_action_id,
- service_name,
- service_ids,
- } => {
- println!("link object service action");
- None
- }
- })
-}
-async fn list(
- secd: &Secd,
- cmd: ListObject,
- filter_name: Option<String>,
- filter_before: Option<i64>,
- filter_after: Option<i64>,
-) -> Result<Option<String>> {
+
+async fn update(secd: &Secd, cmd: UpdateObject) -> Result<Option<String>> {
Ok(match cmd {
- ListObject::ApiKey => {
- println!("list object api key");
- None
- }
- ListObject::Group => {
- println!("list object group");
- None
- }
- ListObject::Identity => {
- println!("list object identity");
- None
- }
- ListObject::Permission => {
- println!("list object permission");
- None
- }
- ListObject::Role => {
- println!("list object role");
- None
- }
- ListObject::Service => {
- println!("list object service");
- None
- }
- ListObject::ServiceAction => {
- println!("list object service action");
- None
- }
- ListObject::Session => {
- println!("list object session");
- None
- }
- ListObject::Validation => {
- println!("list object valiation");
- None
+ UpdateObject::Identity { id, metadata } => {
+ let identity = if metadata.is_some() {
+ secd.update_identity_metadata(id, metadata.unwrap()).await?
+ } else {
+ secd.get_identity(Some(id), None).await?
+ };
+
+ Some(serde_json::to_string(&identity)?.to_string())
}
})
}
diff --git a/crates/secd/Cargo.toml b/crates/secd/Cargo.toml
index 1eb30b1..94ae43c 100644
--- a/crates/secd/Cargo.toml
+++ b/crates/secd/Cargo.toml
@@ -3,10 +3,12 @@ name = "secd"
version = "0.1.0"
edition = "2021"
+
[dependencies]
+aes-gcm = "0.10.1"
+argon2 = "0.4.1"
async-trait = "0.1.59"
anyhow = "1.0"
-base64 = "0.13.1"
clap = { version = "4.0.29", features = ["derive"] }
derive_more = "0.99"
email_address = "0.2"
@@ -15,17 +17,19 @@ lazy_static = "1.4"
lettre = "0.10.1"
log = "0.4"
openssl = "0.10.42"
+pest = "2.5.2"
prost = "0.9"
prost-types = "0.9.0"
rand = "0.8"
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"] }
sha2 = "0.10.6"
strum = "0.24.1"
strum_macros = "0.24"
-sqlx = { path = "../../../sqlx", features = [ "runtime-async-std-native-tls", "postgres", "uuid", "sqlite", "time" ] }
+sqlx = { git = "https://github.com/benjbellon/sqlx.git", branch = "bc/safe-schemas", features = [ "runtime-async-std-native-tls", "postgres", "uuid", "sqlite", "time" ] }
time = { version = "0.3", features = [ "serde" ] }
thiserror = "1.0"
tokio = { version = "1.23.0", feautres = ["rt", "macros"] }
diff --git a/crates/secd/README.md b/crates/secd/README.md
index 5786d0c..17c333d 100644
--- a/crates/secd/README.md
+++ b/crates/secd/README.md
@@ -52,3 +52,60 @@ GET /oidc/provider?state=123444 -- state validated by client
POST /api/auth/oidc { data ... }
motif = start_motif(Oidc, access_token, data)
session = complete_motif(motif.id)
+
+ref = secd.write(User(user, 1), (doc, 3), "editor");
+secd.attach_computed_property(ref, "property_name");
+
+secd.check(User(user, 1), (doc, 3), "editor")
+secd.compute\_check(User(user, 1), (doc, 3), "editor", ["property", args...], ["property", args...])
+e.g.
+secd.compute\_check("User(user, 1), (doc, 3), "editor", ["readable_row", 2134], ["property2", args...])
+
+.....NO: A computed property should just be a domain things, and if any data is needed, it can be attached to the auth store for that identity!!!!!!!!!!!!!!!!
+
+## Namespace stuff...
+
+use file/path/1
+use file/path/2
+
+namespace user { }
+namespace role {
+ relation member {
+ user | group#member
+ }
+
+ computed_property (t: timestamp, s: timestamp) {
+ perform a computation here...
+ }
+
+ computed_property (s: string) {
+ s.starts_with("b")
+ }
+}
+
+namespace group {
+ relation member {
+ user | this.admin
+ }
+ relation admin {
+ user
+ }
+}
+
+namespace doc {
+ relation owner { user }
+ relation editor { user | this.owner }
+ relation viewer { user | this.editor | this.parent#viewer | all(user) }
+ relation auditor { editor - this.owner }
+ relation parent { this.doc }
+}
+
+so, basically it's just:
+
+namespace N {
+ relation R {
+ N#R | N#R & N#R - N#R
+ }
+}
+
+These are `.iam` files. Any `.iam` file can be specified as the main file, and then each use statement will be followed.
diff --git a/crates/secd/assets/default_login_email.html b/crates/secd/assets/default_login_email.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/crates/secd/assets/default_login_email.html
diff --git a/crates/secd/assets/default_signup_email.html b/crates/secd/assets/default_signup_email.html
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/crates/secd/assets/default_signup_email.html
diff --git a/crates/secd/assets/default_sms_login.txt b/crates/secd/assets/default_sms_login.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/crates/secd/assets/default_sms_login.txt
diff --git a/crates/secd/assets/default_sms_signup.txt b/crates/secd/assets/default_sms_signup.txt
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/crates/secd/assets/default_sms_signup.txt
diff --git a/crates/secd/src/auth/n.rs b/crates/secd/src/auth/n.rs
index 1d3b2d5..1f32fd6 100644
--- a/crates/secd/src/auth/n.rs
+++ b/crates/secd/src/auth/n.rs
@@ -1,20 +1,23 @@
-use std::str::FromStr;
-
use crate::{
client::{
- email::{EmailValidationMessage, Sendable},
+ email::{
+ parse_email_template, EmailValidationMessage, Sendable, DEFAULT_SIGNIN_EMAIL,
+ DEFAULT_SIGNUP_EMAIL,
+ },
store::{
- AddressLens, AddressValidationLens, IdentityLens, SessionLens, Storable, StoreError,
+ AddressLens, AddressValidationLens, CredentialLens, IdentityLens, SessionLens,
+ Storable, StoreError,
},
},
util, Address, AddressType, AddressValidation, AddressValidationId, AddressValidationMethod,
- Credential, CredentialType, Identity, PhoneNumber, Secd, SecdError, Session, SessionToken,
+ Credential, CredentialType, Identity, IdentityId, Secd, SecdError, Session, SessionToken,
ADDRESSS_VALIDATION_CODE_SIZE, ADDRESS_VALIDATION_ALLOWS_ATTEMPTS,
ADDRESS_VALIDATION_IDENTITY_SURJECTION, EMAIL_VALIDATION_DURATION,
};
use email_address::EmailAddress;
use log::warn;
use rand::Rng;
+use std::str::FromStr;
use time::{Duration, OffsetDateTime};
use uuid::Uuid;
@@ -22,10 +25,15 @@ impl Secd {
pub async fn validate_email(
&self,
email_address: &str,
- identity_id: Option<Uuid>,
+ identity_id: Option<IdentityId>,
) -> Result<AddressValidation, SecdError> {
let email_address = EmailAddress::from_str(email_address)?;
- // record address (idempotent operation)
+ let mut email_template = self
+ .cfg
+ .email_signup_message
+ .clone()
+ .unwrap_or(DEFAULT_SIGNUP_EMAIL.into());
+
let mut address = Address {
id: Uuid::new_v4(),
t: AddressType::Email {
@@ -50,6 +58,12 @@ impl Secd {
.into_iter()
.next()
.ok_or(SecdError::AddressValidationFailed)?;
+
+ email_template = self
+ .cfg
+ .email_signin_message
+ .clone()
+ .unwrap_or(DEFAULT_SIGNIN_EMAIL.into());
}
let secret = hex::encode(rand::thread_rng().gen::<[u8; 32]>());
@@ -76,13 +90,23 @@ impl Secd {
validation.write(self.store.clone()).await?;
- let msg =EmailValidationMessage {
+ let msg = EmailValidationMessage {
+ from_address: self
+ .cfg
+ .email_address_from
+ .clone()
+ .unwrap_or("SecD <noreply@secd.com>".parse().unwrap()),
+ replyto_address: self
+ .cfg
+ .email_address_replyto
+ .clone()
+ .unwrap_or("SecD <noreply@secd.com>".parse().unwrap()),
recipient: email_address.clone(),
- subject: "Confirm Your Email".into(),
- body: format!("This is an email validation message. Click this link [{:?}?s={}] or use the code [{}]", validation.id, secret, code),
+ subject: "Login Request".into(),
+ body: parse_email_template(&email_template, validation.id, Some(secret), Some(code))?,
};
- match msg.send().await {
+ match msg.send(self.email_messenger.clone()).await {
Ok(_) => { /* TODO: Write down the message*/ }
Err(e) => {
validation.revoked_at = Some(OffsetDateTime::now_utc());
@@ -95,10 +119,11 @@ impl Secd {
}
pub async fn validate_sms(
&self,
- phone_number: &PhoneNumber,
+ // phone_number: &PhoneNumber,
) -> Result<AddressValidation, SecdError> {
todo!()
}
+
pub async fn complete_address_validation(
&self,
validation_id: &AddressValidationId,
@@ -215,21 +240,83 @@ impl Secd {
Ok(session)
}
+
pub async fn create_credential(
&self,
t: CredentialType,
- key: String,
- value: Option<String>,
- ) -> Result<Credential, SecdError> {
- todo!()
+ identity_id: Option<IdentityId>,
+ ) -> Result<Identity, SecdError> {
+ let 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)
+ .ok_or(SecdError::IdentityNotFound)?,
+
+ None => {
+ let id = Identity {
+ id: Uuid::new_v4(),
+ address_validations: vec![],
+ credentials: vec![],
+ rules: vec![],
+ metadata: None,
+ created_at: OffsetDateTime::now_utc(),
+ deleted_at: None,
+ };
+ id.write(self.store.clone()).await?;
+ id
+ }
+ };
+
+ let mut credential = match &Credential::find(
+ self.store.clone(),
+ &CredentialLens {
+ id: None,
+ identity_id: Some(identity.id),
+ t: Some(&t),
+ restrict_by_key: Some(false),
+ },
+ )
+ .await?[..]
+ {
+ [] => Credential {
+ id: Uuid::new_v4(),
+ identity_id: identity.id,
+ t,
+ created_at: OffsetDateTime::now_utc(),
+ revoked_at: None,
+ deleted_at: None,
+ },
+ _ => return Err(SecdError::CredentialAlreadyExists),
+ };
+
+ credential.hash(&self.crypter)?;
+ credential
+ .write(self.store.clone())
+ .await
+ .map_err(|err| match err {
+ StoreError::IdempotentCheckAlreadyExists => SecdError::CredentialAlreadyExists,
+ err => SecdError::StoreError(err),
+ })?;
+
+ Ok(identity)
}
pub async fn validate_credential(
&self,
- t: CredentialType,
- key: String,
- value: Option<String>,
+ // t: CredentialType,
+ // key: String,
+ // value: Option<String>,
) -> Result<Session, SecdError> {
+ // Credential::find(store, lens) use key here as unique index
todo!()
}
@@ -254,15 +341,23 @@ impl Secd {
}
}
- pub async fn get_identity(&self, i: &SessionToken) -> Result<Identity, SecdError> {
- let token_hash = util::hash(&hex::decode(i)?);
+ pub async fn get_identity(
+ &self,
+ i: Option<IdentityId>,
+ t: Option<SessionToken>,
+ ) -> Result<Identity, SecdError> {
+ let token_hash = match t {
+ Some(tok) => Some(util::hash(&hex::decode(&tok)?)),
+ None => None,
+ };
+
let mut i = Identity::find(
self.store.clone(),
&IdentityLens {
- id: None,
+ id: i.as_ref(),
address_type: None,
validated_address: None,
- session_token_hash: Some(token_hash),
+ session_token_hash: token_hash,
},
)
.await?;
@@ -279,6 +374,31 @@ impl Secd {
}
}
+ pub async fn update_identity_metadata(
+ &self,
+ i: IdentityId,
+ md: String,
+ ) -> Result<Identity, SecdError> {
+ let mut identity = Identity::find(
+ self.store.clone(),
+ &IdentityLens {
+ id: Some(&i),
+ address_type: None,
+ validated_address: None,
+ session_token_hash: None,
+ },
+ )
+ .await?
+ .into_iter()
+ .nth(0)
+ .ok_or(SecdError::IdentityNotFound)?;
+
+ identity.metadata = Some(md);
+ identity.write(self.store.clone()).await?;
+
+ 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?;
diff --git a/crates/secd/src/auth/z/graph.rs b/crates/secd/src/auth/z/graph.rs
new file mode 100644
index 0000000..9ca045b
--- /dev/null
+++ b/crates/secd/src/auth/z/graph.rs
@@ -0,0 +1,209 @@
+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);
+ }
+}
diff --git a/crates/secd/src/auth/z.rs b/crates/secd/src/auth/z/mod.rs
index 31f449c..b364583 100644
--- a/crates/secd/src/auth/z.rs
+++ b/crates/secd/src/auth/z/mod.rs
@@ -1,6 +1,8 @@
-use uuid::Uuid;
+mod graph;
-use crate::{Secd, SecdError};
+use crate::{Authorization, Secd, SecdError};
+use async_trait::async_trait;
+use uuid::Uuid;
pub type Namespace = String;
pub type Object = (Namespace, Uuid);
@@ -18,8 +20,9 @@ pub enum Subject {
UserSet { user: Object, relation: Relation },
}
-impl Secd {
- pub async fn check(&self, r: &Relationship) -> Result<bool, SecdError> {
+#[async_trait]
+impl Authorization for Secd {
+ async fn check(&self, r: &Relationship) -> Result<bool, SecdError> {
let spice = self
.spice
.clone()
@@ -27,16 +30,16 @@ impl Secd {
Ok(spice.check_permission(r).await?)
}
- pub async fn expand(&self) -> Result<(), SecdError> {
+ async fn expand(&self) -> Result<(), SecdError> {
todo!()
}
- pub async fn read(&self) -> Result<(), SecdError> {
+ async fn read(&self) -> Result<(), SecdError> {
todo!()
}
- pub async fn watch(&self) -> Result<(), SecdError> {
+ async fn watch(&self) -> Result<(), SecdError> {
unimplemented!()
}
- pub async fn write(&self, ts: &[Relationship]) -> Result<(), SecdError> {
+ async fn write(&self, ts: &[Relationship]) -> Result<(), SecdError> {
let spice = self
.spice
.clone()
@@ -59,3 +62,27 @@ impl Secd {
Ok(())
}
}
+
+enum RelationToken {
+ Start,
+ Or,
+ And,
+ Exclude,
+}
+struct RelationContainer {
+ name: Relation,
+ bins: Vec<(RelationToken, Relation)>,
+}
+
+struct NamespaceContainer {
+ relations: Vec<RelationContainer>,
+}
+
+impl Secd {
+ async fn write_namespace(&self, ns: &NamespaceContainer) -> Result<(), SecdError> {
+ todo!()
+ }
+ async fn read_namespace(&self) -> Result<NamespaceContainer, SecdError> {
+ todo!()
+ }
+}
diff --git a/crates/secd/src/client/email/mod.rs b/crates/secd/src/client/email/mod.rs
index 915d18c..7c7b233 100644
--- a/crates/secd/src/client/email/mod.rs
+++ b/crates/secd/src/client/email/mod.rs
@@ -1,68 +1,172 @@
+use async_trait::async_trait;
use email_address::EmailAddress;
-use lettre::Transport;
-use log::error;
-use std::collections::HashMap;
+use lettre::{
+ message::{Mailbox, MultiPart},
+ Transport,
+};
+use log::{error, warn};
+use reqwest::StatusCode;
+use sendgrid::v3::{Content, Email, Personalization};
+use std::sync::Arc;
+
+use crate::AddressValidationId;
+
+pub const DEFAULT_SIGNUP_EMAIL: &str = "This email was recently used to signup. Please use the following code to complete your validation: {{secd::validation_code}}. If you did not request this signup link, you can safely ignore this email.";
+pub const DEFAULT_SIGNIN_EMAIL: &str = "An account with this email was recently used to signin. Please use the following code to complete your sign in process: {{secd::validation_code}}. If you did not request this signin link, you can safely ingore this email.";
#[derive(Debug, thiserror::Error, derive_more::Display)]
pub enum EmailMessengerError {
FailedToSendEmail,
+ LibLettreError(#[from] lettre::error::Error),
+ SendgridError(#[from] sendgrid::SendgridError),
}
pub struct EmailValidationMessage {
+ pub from_address: Mailbox,
+ pub replyto_address: Mailbox,
pub recipient: EmailAddress,
pub subject: String,
pub body: String,
}
-#[async_trait::async_trait]
-pub(crate) trait EmailMessenger {
- async fn send_email(
- &self,
- email_address: &EmailAddress,
- template: &str,
- template_vars: HashMap<&str, &str>,
- ) -> Result<(), EmailMessengerError>;
+#[async_trait]
+pub(crate) trait EmailMessenger: Send + Sync {
+ fn get_type(&self) -> MessengerType;
+ fn get_api_key(&self) -> String;
}
-pub(crate) struct LocalMailer {}
+pub enum MessengerType {
+ LocalMailer,
+ Sendgrid,
+}
-#[async_trait::async_trait]
+pub(crate) struct LocalMailer {}
+impl LocalMailer {
+ pub fn new() -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
+ warn!("You are using the local mailer, which will not work in production!");
+ Arc::new(LocalMailer {})
+ }
+}
impl EmailMessenger for LocalMailer {
- async fn send_email(
- &self,
- email_address: &EmailAddress,
- template: &str,
- template_vars: HashMap<&str, &str>,
- ) -> Result<(), EmailMessengerError> {
- todo!()
+ fn get_type(&self) -> MessengerType {
+ MessengerType::LocalMailer
+ }
+ fn get_api_key(&self) -> String {
+ panic!("unreachable since no API key is expected for LocalMailer");
+ }
+}
+pub(crate) struct Sendgrid {
+ pub api_key: String,
+}
+impl Sendgrid {
+ pub fn new(api_key: String) -> Arc<dyn EmailMessenger + Send + Sync + 'static> {
+ Arc::new(Sendgrid { api_key })
+ }
+}
+impl EmailMessenger for Sendgrid {
+ fn get_type(&self) -> MessengerType {
+ MessengerType::Sendgrid
+ }
+ fn get_api_key(&self) -> String {
+ self.api_key.clone()
}
}
-#[async_trait::async_trait]
+#[async_trait]
pub(crate) trait Sendable {
- async fn send(&self) -> Result<(), EmailMessengerError>;
+ async fn send(&self, messenge: Arc<dyn EmailMessenger>) -> Result<(), EmailMessengerError>;
}
-#[async_trait::async_trait]
+#[async_trait]
impl Sendable for EmailValidationMessage {
- // TODO: We need to break this up as before, especially so we can feature
- // gate unwanted things like Lettre...
- async fn send(&self) -> Result<(), EmailMessengerError> {
- // TODO: Get these things from the template...
- let email = lettre::Message::builder()
- .from("BranchControl <iam@branchcontrol.com>".parse().unwrap())
- .reply_to("BranchControl <iam@branchcontrol.com>".parse().unwrap())
- .to(self.recipient.to_string().parse().unwrap())
- .subject(self.subject.clone())
- .body(self.body.clone())
- .unwrap();
-
- let mailer = lettre::SmtpTransport::unencrypted_localhost();
-
- mailer.send(&email).map_err(|e| {
- error!("failed to send email {:?}", e);
- EmailMessengerError::FailedToSendEmail
- })?;
+ async fn send(&self, messenger: Arc<dyn EmailMessenger>) -> Result<(), EmailMessengerError> {
+ match messenger.get_type() {
+ MessengerType::LocalMailer => {
+ let email = lettre::Message::builder()
+ .from(self.from_address.to_string().parse().unwrap())
+ .reply_to(self.replyto_address.to_string().parse().unwrap())
+ .to(self.recipient.to_string().parse().unwrap())
+ .subject(self.subject.clone())
+ .multipart(MultiPart::alternative_plain_html(
+ "".to_string(),
+ String::from(self.body.clone()),
+ ))?;
+
+ let mailer = lettre::SmtpTransport::unencrypted_localhost();
+
+ mailer.send(&email).map_err(|e| {
+ error!("failed to send email {:?}", e);
+ EmailMessengerError::FailedToSendEmail
+ })?;
+ }
+ MessengerType::Sendgrid => {
+ let msg = sendgrid::v3::Message::new(Email::new(self.from_address.to_string()))
+ .set_subject(&self.subject)
+ .add_content(
+ Content::new()
+ .set_content_type("text/html")
+ .set_value(&self.body),
+ )
+ .add_personalization(Personalization::new(Email::new(
+ self.recipient.to_string(),
+ )));
+
+ let sender = sendgrid::v3::Sender::new(messenger.get_api_key());
+ let resp = sender.send(&msg).await?;
+ match resp.status() {
+ StatusCode::ACCEPTED => {}
+ _ => {
+ error!(
+ "sendgrid failed to send message with status: {}",
+ resp.status()
+ )
+ }
+ }
+ }
+ };
+
Ok(())
}
}
+
+pub(crate) fn parse_email_template(
+ template: &str,
+ validation_id: AddressValidationId,
+ validation_secret: Option<String>,
+ validation_code: Option<String>,
+) -> Result<String, EmailMessengerError> {
+ let mut t = template.clone().to_string();
+ // 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
+ // for the $$secd:request_id$$ and $$secd:request_code$$, either of which may be
+ // 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));
+
+ Ok(t)
+}
+
+#[cfg(test)]
+mod test {
+ use uuid::Uuid;
+
+ use super::*;
+
+ #[test]
+ fn test_parse_and_substitue() {
+ let raw = "This is an email validation message. Navigate to https://www.secd.com/auth/{secd::validation_id}?s={secd::validation_secret} or use the code [{secd::validation_code}]";
+
+ let parsed = parse_email_template(
+ raw,
+ Uuid::parse_str("90f42ba9-ed4a-4f56-b371-df05634a1626").unwrap(),
+ Some("s3cr3t".into()),
+ Some("102030".into()),
+ )
+ .unwrap();
+
+ assert_eq!(parsed, "This is an email validation message. Navigate to https://www.secd.com/auth/90f42ba9-ed4a-4f56-b371-df05634a1626?s=s3cr3t or use the code [102030]")
+ }
+}
diff --git a/crates/secd/src/client/store/mod.rs b/crates/secd/src/client/store/mod.rs
index 8a076c4..7bf01d5 100644
--- a/crates/secd/src/client/store/mod.rs
+++ b/crates/secd/src/client/store/mod.rs
@@ -1,22 +1,28 @@
pub(crate) mod sql_db;
+use async_trait::async_trait;
use sqlx::{Postgres, Sqlite};
use std::sync::Arc;
use uuid::Uuid;
-use crate::{util, Address, AddressType, AddressValidation, Identity, IdentityId, Session};
+use crate::{
+ util, Address, AddressType, AddressValidation, Credential, CredentialId, CredentialType,
+ Identity, IdentityId, Session,
+};
use self::sql_db::SqlClient;
#[derive(Debug, thiserror::Error, derive_more::Display)]
pub enum StoreError {
SqlClientError(#[from] sqlx::Error),
+ SerdeError(#[from] serde_json::Error),
+ ParseError(#[from] strum::ParseError),
StoreValueCannotBeParsedInvariant,
IdempotentCheckAlreadyExists,
}
-#[async_trait::async_trait(?Send)]
-pub trait Store {
+#[async_trait]
+pub trait Store: Send + Sync {
fn get_type(&self) -> StoreType;
}
@@ -25,7 +31,7 @@ pub enum StoreType {
Sqlite { c: Arc<SqlClient<Sqlite>> },
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
pub(crate) trait Storable<'a> {
type Item;
type Lens;
@@ -64,7 +70,15 @@ pub(crate) struct SessionLens<'a> {
}
impl<'a> Lens for SessionLens<'a> {}
-#[async_trait::async_trait(?Send)]
+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> {}
+
+#[async_trait]
impl<'a> Storable<'a> for Address {
type Item = Address;
type Lens = AddressLens<'a>;
@@ -93,7 +107,7 @@ impl<'a> Storable<'a> for Address {
}
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
impl<'a> Storable<'a> for AddressValidation {
type Item = AddressValidation;
type Lens = AddressValidationLens<'a>;
@@ -116,7 +130,7 @@ impl<'a> Storable<'a> for AddressValidation {
}
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
impl<'a> Storable<'a> for Identity {
type Item = Identity;
type Lens = IdentityLens<'a>;
@@ -158,7 +172,7 @@ impl<'a> Storable<'a> for Identity {
}
}
-#[async_trait::async_trait(?Send)]
+#[async_trait]
impl<'a> Storable<'a> for Session {
type Item = Session;
type Lens = SessionLens<'a>;
@@ -183,3 +197,51 @@ impl<'a> Storable<'a> for Session {
})
}
}
+
+#[async_trait]
+impl<'a> Storable<'a> for Credential {
+ type Item = Credential;
+ type Lens = CredentialLens<'a>;
+
+ async fn write(&self, store: Arc<dyn Store>) -> Result<(), StoreError> {
+ match store.get_type() {
+ StoreType::Postgres { c } => c.write_credential(self).await?,
+ StoreType::Sqlite { c } => c.write_credential(self).await?,
+ }
+ Ok(())
+ }
+
+ async fn find(
+ store: Arc<dyn Store>,
+ lens: &'a Self::Lens,
+ ) -> 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?
+ }
+ })
+ }
+}
diff --git a/crates/secd/src/client/store/sql_db.rs b/crates/secd/src/client/store/sql_db.rs
index ecb13be..3e72fe8 100644
--- a/crates/secd/src/client/store/sql_db.rs
+++ b/crates/secd/src/client/store/sql_db.rs
@@ -1,27 +1,19 @@
-use std::{str::FromStr, sync::Arc};
-
-use email_address::EmailAddress;
-use serde_json::value::RawValue;
-use sqlx::{
- database::HasArguments, types::Json, ColumnIndex, Database, Decode, Encode, Executor,
- IntoArguments, Pool, Transaction, Type,
-};
-use time::OffsetDateTime;
-use uuid::Uuid;
-
+use super::{Store, StoreError, StoreType};
use crate::{
- Address, AddressType, AddressValidation, AddressValidationMethod, Identity, Session,
- SessionToken,
+ Address, AddressType, AddressValidation, AddressValidationMethod, Credential, CredentialId,
+ CredentialType, Identity, IdentityId, Session,
};
-
+use email_address::EmailAddress;
use lazy_static::lazy_static;
+use sqlx::{
+ database::HasArguments, ColumnIndex, Database, Decode, Encode, Executor, IntoArguments, Pool,
+ Transaction, Type,
+};
use sqlx::{Postgres, Sqlite};
use std::collections::HashMap;
-
-use super::{
- AddressLens, AddressValidationLens, IdentityLens, SessionLens, Storable, Store, StoreError,
- StoreType,
-};
+use std::{str::FromStr, sync::Arc};
+use time::OffsetDateTime;
+use uuid::Uuid;
const SQLITE: &str = "sqlite";
const PGSQL: &str = "pgsql";
@@ -30,6 +22,8 @@ const WRITE_ADDRESS: &str = "write_address";
const FIND_ADDRESS: &str = "find_address";
const WRITE_ADDRESS_VALIDATION: &str = "write_address_validation";
const FIND_ADDRESS_VALIDATION: &str = "find_address_validation";
+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";
@@ -72,6 +66,14 @@ lazy_static! {
FIND_SESSION,
include_str!("../../../store/sqlite/sql/find_session.sql"),
),
+ (
+ WRITE_CREDENTIAL,
+ include_str!("../../../store/sqlite/sql/write_credential.sql"),
+ ),
+ (
+ FIND_CREDENTIAL,
+ include_str!("../../../store/sqlite/sql/find_credential.sql"),
+ ),
]
.iter()
.cloned()
@@ -110,6 +112,14 @@ lazy_static! {
FIND_SESSION,
include_str!("../../../store/pg/sql/find_session.sql"),
),
+ (
+ WRITE_CREDENTIAL,
+ include_str!("../../../store/pg/sql/write_credential.sql"),
+ ),
+ (
+ FIND_CREDENTIAL,
+ include_str!("../../../store/pg/sql/find_credential.sql"),
+ ),
]
.iter()
.cloned()
@@ -131,7 +141,7 @@ pub trait SqlxResultExt<T> {
impl<T> SqlxResultExt<T> for Result<T, sqlx::Error> {
fn extend_err(self) -> Result<T, StoreError> {
if let Err(sqlx::Error::Database(dbe)) = &self {
- if dbe.code() == Some("23505".into()) {
+ if dbe.code() == Some("23505".into()) || dbe.code() == Some("2067".into()) {
return Err(StoreError::IdempotentCheckAlreadyExists);
}
}
@@ -160,7 +170,7 @@ impl Store for PgClient {
impl PgClient {
pub async fn new(pool: sqlx::Pool<Postgres>) -> Arc<dyn Store + Send + Sync + 'static> {
- sqlx::migrate!("store/pg/migrations")
+ sqlx::migrate!("store/pg/migrations", "secd")
.run(&pool)
.await
.expect(ERR_MSG_MIGRATION_FAILED);
@@ -187,7 +197,7 @@ impl Store for SqliteClient {
impl SqliteClient {
pub async fn new(pool: sqlx::Pool<Sqlite>) -> Arc<dyn Store + Send + Sync + 'static> {
- sqlx::migrate!("store/sqlite/migrations")
+ sqlx::migrate!("store/sqlite/migrations", "secd")
.run(&pool)
.await
.expect(ERR_MSG_MIGRATION_FAILED);
@@ -410,8 +420,7 @@ where
let sqls = get_sqls(&self.sqls_root, WRITE_IDENTITY);
sqlx::query(&sqls[0])
.bind(i.id)
- // TODO: validate this is actually Json somewhere way up the chain (when being deserialized)
- .bind(i.metadata.clone().unwrap_or("{}".into()))
+ .bind(i.metadata.clone())
.bind(i.created_at)
.bind(OffsetDateTime::now_utc())
.bind(i.deleted_at)
@@ -449,7 +458,7 @@ where
.extend_err()?;
let mut res = vec![];
- for (id, metadata, created_at, updated_at, deleted_at) in rs.into_iter() {
+ for (id, metadata, created_at, _, deleted_at) in rs.into_iter() {
res.push(Identity {
id,
address_validations: vec![],
@@ -509,6 +518,79 @@ where
}
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,
+ };
+
+ sqlx::query(&sqls[0])
+ .bind(c.id)
+ .bind(c.identity_id)
+ .bind(partial_key)
+ .bind(c.t.to_string())
+ .bind(serde_json::to_string(&c.t)?)
+ .bind(c.created_at)
+ .bind(c.revoked_at)
+ .bind(c.deleted_at)
+ .execute(&self.pool)
+ .await
+ .extend_err()?;
+ Ok(())
+ }
+ pub async fn find_credential(
+ &self,
+ 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 rs = sqlx::query_as::<
+ _,
+ (
+ CredentialId,
+ IdentityId,
+ String,
+ OffsetDateTime,
+ Option<OffsetDateTime>,
+ Option<OffsetDateTime>,
+ ),
+ >(&sqls[0])
+ .bind(id.as_ref())
+ .bind(identity_id.as_ref())
+ .bind(t.map(|i| i.to_string()))
+ .bind(key)
+ .fetch_all(&self.pool)
+ .await
+ .extend_err()?;
+
+ 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)?;
+ res.push(Credential {
+ id,
+ identity_id,
+ t,
+ created_at,
+ revoked_at,
+ deleted_at,
+ })
+ }
+
+ Ok(res)
+ }
}
fn get_sqls(root: &str, file: &str) -> Vec<String> {
diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs
index 15615b2..54759a5 100644
--- a/crates/secd/src/lib.rs
+++ b/crates/secd/src/lib.rs
@@ -2,8 +2,10 @@ pub mod auth;
mod client;
mod util;
+use async_trait::async_trait;
+use auth::z::Relationship;
use client::{
- email::{EmailMessenger, EmailMessengerError, LocalMailer},
+ email::{EmailMessenger, EmailMessengerError, LocalMailer, Sendgrid},
spice::{Spice, SpiceError},
store::{
sql_db::{PgClient, SqliteClient},
@@ -11,28 +13,40 @@ use client::{
},
};
use email_address::EmailAddress;
-use log::{error, info};
+use lettre::message::Mailbox;
+use log::{error, info, warn};
use serde::{Deserialize, Serialize};
-use serde_with::{serde_as, DisplayFromStr};
-use std::{env::var, str::FromStr, sync::Arc};
+use serde_with::serde_as;
+use std::{
+ env::{set_var, var},
+ str::FromStr,
+ sync::Arc,
+};
use strum_macros::{Display, EnumString, EnumVariantNames};
use time::OffsetDateTime;
-use url::Url;
+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 SESSION_SIZE_BYTES: usize = 32;
-const SESSION_DURATION: i64 = 60 /* seconds*/ * 60 /* minutes */ * 24 /* hours */ * 360 /* days */;
-const EMAIL_VALIDATION_DURATION: i64 = 60 /* seconds*/ * 15 /* minutes */;
const ADDRESSS_VALIDATION_CODE_SIZE: u8 = 6;
const ADDRESS_VALIDATION_ALLOWS_ATTEMPTS: u8 = 5;
const ADDRESS_VALIDATION_IDENTITY_SURJECTION: bool = true;
+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 */;
+const SESSION_SIZE_BYTES: usize = 32;
pub type AddressId = Uuid;
pub type AddressValidationId = Uuid;
@@ -45,10 +59,17 @@ pub type SessionToken = String;
#[derive(Debug, derive_more::Display, thiserror::Error)]
pub enum SecdError {
+ // AuthenticationError(AuthnError);
+ // AuthorizationError(AuthzError);
+ // // InitializationError(...)
AddressValidationFailed,
AddressValidationSessionExchangeFailed,
AddressValidationExpiredOrConsumed,
+ CredentialAlreadyExists,
+
+ CrypterError(#[from] CrypterError),
+
TooManyIdentities,
IdentityNotFound,
@@ -69,9 +90,62 @@ pub enum SecdError {
}
pub struct Secd {
- store: Arc<dyn Store + Send + Sync + 'static>,
+ crypter: Crypter,
email_messenger: Arc<dyn EmailMessenger + Send + Sync + 'static>,
spice: Option<Arc<Spice>>,
+ store: Arc<dyn Store + Send + Sync + 'static>,
+ cfg: Cfg,
+}
+
+struct Cfg {
+ email_address_from: Option<Mailbox>,
+ email_address_replyto: Option<Mailbox>,
+ email_signup_message: Option<String>,
+ email_signin_message: Option<String>,
+}
+
+#[async_trait]
+pub trait Authentication {
+ async fn validate_address(
+ &self,
+ address_type: AddressType,
+ identity_id: Option<IdentityId>,
+ ) -> Result<AddressValidation, SecdError>;
+
+ async fn complete_address_validation(
+ &self,
+ validation_id: &AddressValidationId,
+ plaintext_token: Option<String>,
+ plaintext_code: Option<String>,
+ ) -> Result<AddressValidation, SecdError>;
+
+ async fn create_credential(
+ &self,
+ t: &CredentialType,
+ identity_id: Option<IdentityId>,
+ ) -> Result<IdentityId, SecdError>;
+ // async fn update_credential(&self, t: &CredentialType) -> Result<(), SecdError>;
+ async fn reset_credential(
+ &self,
+ t: &CredentialType,
+ address: &AddressType,
+ ) -> Result<Credential, SecdError>;
+ async fn validate_credential(&self, t: &CredentialType) -> Result<Credential, SecdError>;
+
+ // async fn expire_session_chain(&self, t: &SessionToken) -> Result<(), SecdError>;
+ // async fn expire_sessions(&self, i: &IdentityId) -> Result<(), SecdError>;
+
+ // async fn get_identity(&self, t: &SessionToken) -> Result<Identity, SecdError>;
+ // async fn get_session(&self, t: &SessionToken) -> Result<Session, SecdError>;
+}
+
+#[async_trait]
+pub trait Authorization {
+ async fn check(&self, r: &Relationship) -> Result<bool, SecdError>;
+ async fn expand(&self) -> Result<(), SecdError>;
+ async fn read(&self) -> Result<(), SecdError>;
+ async fn watch(&self) -> Result<(), SecdError>;
+ async fn write(&self, relationships: &[Relationship]) -> Result<(), SecdError>;
}
#[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)]
@@ -136,34 +210,53 @@ pub enum AddressType {
Sms { phone_number: Option<PhoneNumber> },
}
+#[serde_as]
#[derive(Debug, Serialize)]
pub struct Credential {
pub id: CredentialId,
pub identity_id: IdentityId,
pub t: CredentialType,
+ #[serde(with = "time::serde::timestamp")]
+ pub created_at: OffsetDateTime,
+ #[serde(with = "time::serde::timestamp::option")]
+ pub revoked_at: Option<OffsetDateTime>,
+ #[serde(with = "time::serde::timestamp::option")]
+ pub deleted_at: Option<OffsetDateTime>,
}
#[serde_as]
-#[derive(Debug, Serialize)]
+#[derive(Debug, Display, Serialize, Deserialize, EnumString)]
pub enum CredentialType {
- Passphrase {
- key: String,
- value: String,
- },
- Oicd {
- value: String,
- },
- OneTimeCodes {
- codes: Vec<String>,
- },
- Totp {
- #[serde_as(as = "DisplayFromStr")]
- url: Url,
- code: String,
- },
- WebAuthn {
- value: 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> },
+ // Totp {
+ // #[serde_as(as = "DisplayFromStr")]
+ // url: Url,
+ // code: String,
+ // },
+ WebAuthn { value: String },
}
#[serde_with::skip_serializing_none]
@@ -207,12 +300,29 @@ impl Secd {
&var(ENV_EMAIL_MESSENGER).unwrap_or(AuthEmailMessenger::Local.to_string()),
)
.expect("unreachable f4ad0f48-0812-427f-b477-0f9c67bb69c5");
- let email_messenger_client_id = var(ENV_EMAIL_MESSENGER_CLIENT_ID).ok();
- let email_messenger_client_secret = var(ENV_EMAIL_MESSENGER_CLIENT_SECRET).ok();
+ let crypter_secret_key = var(ENV_CRYPTER_SECRET_KEY).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,
+ CRYPTER_SECRET_KEY_DEFAULT,
+ );
+ CRYPTER_SECRET_KEY_DEFAULT.to_string()
+ });
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() {
@@ -252,7 +362,10 @@ impl Secd {
};
let email_sender = match email_messenger {
- AuthEmailMessenger::Local => LocalMailer {},
+ AuthEmailMessenger::Local => LocalMailer::new(),
+ AuthEmailMessenger::Sendgrid => Sendgrid::new(
+ var(ENV_EMAIL_SENDGRID_API_KEY).expect("No SENDGRID_API_KEY provided"),
+ ),
_ => unimplemented!(),
};
@@ -267,10 +380,14 @@ impl Secd {
None => None,
};
+ let crypter = Crypter::new(crypter_secret_key.as_bytes());
+
Ok(Secd {
- store,
- email_messenger: Arc::new(email_sender),
+ crypter,
+ email_messenger: email_sender,
spice,
+ store,
+ cfg,
})
}
}
diff --git a/crates/secd/src/util/crypter.rs b/crates/secd/src/util/crypter.rs
new file mode 100644
index 0000000..1717377
--- /dev/null
+++ b/crates/secd/src/util/crypter.rs
@@ -0,0 +1,87 @@
+use aes_gcm::{
+ aead::{Aead, KeyInit, OsRng},
+ Aes256Gcm, Nonce,
+};
+use argon2::{
+ password_hash::{self, SaltString},
+ Argon2, PasswordHasher,
+};
+use derive_more::Display;
+use rand::Rng;
+use sha2::{Digest, Sha256};
+use thiserror::Error;
+
+#[derive(Debug, Display, Error)]
+pub enum CrypterError {
+ EncryptError(String),
+ DecryptError(String),
+ DecodeError(String),
+ HashError(String),
+}
+
+pub struct Crypter {
+ pub key: Vec<u8>,
+}
+
+impl Crypter {
+ pub fn new(key: &[u8]) -> Self {
+ Self { key: key.to_vec() }
+ }
+
+ pub fn encrypt(&self, data: &[u8]) -> Result<Vec<u8>, CrypterError> {
+ let mut hasher = Sha256::new();
+ hasher.update(&self.key);
+
+ let cipher = Aes256Gcm::new(&hasher.finalize());
+
+ 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()))?;
+
+ let mut msg = iv.to_vec();
+ msg.extend_from_slice(&crypt);
+ Ok(msg)
+ }
+
+ pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>, CrypterError> {
+ let mut hasher = Sha256::new();
+ hasher.update(&self.key);
+
+ let cipher = Aes256Gcm::new(&hasher.finalize());
+
+ 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()))?)
+ }
+
+ pub fn hash(&self, data: &[u8]) -> Result<String, CrypterError> {
+ let salt = SaltString::generate(&mut OsRng);
+ let hasher = Argon2::default();
+ Ok(hasher
+ .hash_password(data, &salt)
+ .map_err(|e| CrypterError::HashError(e.to_string()))?
+ .to_string())
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn encrypt_data_test() {
+ let crypter = Crypter {
+ key: "testkey".to_string().into_bytes(),
+ };
+
+ let plaintext = "This is a secret.";
+ let enc = crypter.encrypt(&plaintext.as_bytes()).unwrap();
+
+ let res = crypter.decrypt(&enc).unwrap();
+ assert_eq!(plaintext, std::str::from_utf8(&res).unwrap());
+ }
+}
diff --git a/crates/secd/src/util/from.rs b/crates/secd/src/util/from.rs
index bab8a25..ec5b62d 100644
--- a/crates/secd/src/util/from.rs
+++ b/crates/secd/src/util/from.rs
@@ -1,5 +1,3 @@
-use std::str::FromStr;
-
use crate::AuthStore;
impl From<Option<String>> for AuthStore {
@@ -23,7 +21,6 @@ mod test {
#[test]
fn auth_store_from_string() {
let in_conn = None;
- let a = AuthStore::from(in_conn.clone());
match AuthStore::from(in_conn.clone()) {
AuthStore::Sqlite { conn } => assert_eq!(conn, "sqlite::memory:"),
r @ _ => assert!(
@@ -46,14 +43,12 @@ mod test {
}
let sqlite_conn = Some("sqlite:///path/to/db.sql".into());
- let a = AuthStore::from(sqlite_conn.clone());
match AuthStore::from(sqlite_conn.clone()) {
AuthStore::Sqlite { conn } => assert_eq!(conn, sqlite_conn.unwrap()),
r @ _ => assert!(false, "should have parsed as sqlite. Found: {:?}", r),
}
let sqlite_mem_conn = Some("sqlite:memory:".into());
- let a = AuthStore::from(sqlite_mem_conn.clone());
match AuthStore::from(sqlite_mem_conn.clone()) {
AuthStore::Sqlite { conn } => assert_eq!(conn, sqlite_mem_conn.unwrap()),
r @ _ => assert!(
diff --git a/crates/secd/src/util/mod.rs b/crates/secd/src/util/mod.rs
index c26986d..8676f26 100644
--- a/crates/secd/src/util/mod.rs
+++ b/crates/secd/src/util/mod.rs
@@ -1,21 +1,14 @@
+pub(crate) mod crypter;
pub(crate) mod from;
-use rand::{thread_rng, Rng};
+use self::crypter::{Crypter, CrypterError};
+use crate::{
+ AddressType, Credential, CredentialType, IdentityId, SecdError, Session, SESSION_DURATION,
+ SESSION_SIZE_BYTES,
+};
use sha2::{Digest, Sha256};
+use std::str::from_utf8;
use time::OffsetDateTime;
-use url::Url;
-
-use crate::{AddressType, IdentityId, SecdError, Session, SESSION_DURATION, SESSION_SIZE_BYTES};
-
-pub(crate) fn remove_trailing_slash(url: &mut Url) -> String {
- let mut u = url.to_string();
-
- if u.ends_with('/') {
- u.pop();
- }
-
- u
-}
pub(crate) fn hash(i: &[u8]) -> Vec<u8> {
let mut hasher = Sha256::new();
@@ -51,3 +44,89 @@ impl Session {
})
}
}
+
+impl Credential {
+ pub(crate) fn encrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ Ok(match self.t {
+ CredentialType::Passphrase {
+ key: _,
+ ref mut value,
+ } => {
+ *value = hex::encode(crypter.encrypt(value.as_bytes())?);
+ }
+ _ => {}
+ })
+ }
+ pub(crate) fn decrypt(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ Ok(match self.t {
+ CredentialType::Passphrase {
+ key: _,
+ ref mut value,
+ } => {
+ *value = from_utf8(
+ &crypter.decrypt(
+ &hex::decode(value.clone())
+ .map_err(|e| CrypterError::DecodeError(e.to_string()))?,
+ )?,
+ )
+ .map_err(|e| CrypterError::DecodeError(e.to_string()))?
+ .to_string()
+ }
+ _ => {}
+ })
+ }
+
+ pub(crate) fn hash(&mut self, crypter: &Crypter) -> Result<(), SecdError> {
+ Ok(match self.t {
+ CredentialType::Passphrase {
+ key: _,
+ ref mut value,
+ } => {
+ *value = crypter.hash(value.as_bytes())?;
+ }
+ _ => {}
+ })
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use uuid::Uuid;
+
+ use super::*;
+
+ #[test]
+ fn test_credential_encrypt() {
+ let c = Crypter::new("AMAZING_KEY".as_bytes());
+
+ let plaintext_secret = "super_password".to_string();
+
+ let mut credential = Credential {
+ id: Uuid::new_v4(),
+ identity_id: Uuid::new_v4(),
+ t: CredentialType::Passphrase {
+ key: "super_user".into(),
+ value: plaintext_secret.clone(),
+ },
+ created_at: OffsetDateTime::now_utc(),
+ revoked_at: None,
+ deleted_at: None,
+ };
+
+ credential.encrypt(&c).unwrap();
+ match &credential.t {
+ CredentialType::Passphrase { key: _, value } => {
+ assert_ne!(plaintext_secret.clone(), value.clone())
+ }
+ _ => {}
+ };
+
+ credential.decrypt(&c).unwrap();
+ match &credential.t {
+ CredentialType::Passphrase { key: _, value } => {
+ assert_eq!(plaintext_secret.clone(), value.clone())
+ }
+ _ => {}
+ };
+ }
+}
diff --git a/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
index 2b89957..0cf3fa0 100644
--- a/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
+++ b/crates/secd/store/pg/migrations/20221222002434_bootstrap.sql
@@ -19,7 +19,7 @@ create table if not exists secd.realm_data (
create table if not exists secd.identity (
identity_id bigserial primary key
, identity_public_id uuid not null
- , data jsonb -- some things are dervied, others are not
+ , data text -- we do not prescribe JSON or any other serialization format.
, created_at timestamptz not null
, updated_at timestamptz not null
, deleted_at timestamptz
@@ -30,14 +30,18 @@ create table if not exists secd.credential (
credential_id bigserial primary key
, credential_public_id uuid not null
, identity_id bigint not null references secd.identity(identity_id)
+ , partial_key text
, type text not null-- e.g. password, oidc, totop, lookup_secret, webauthn, ...
, data jsonb not null
- , version integer not null
, created_at timestamptz not null
, revoked_at timestamptz
, deleted_at timestamptz
);
+create unique index if not exists credential_passphrase_type_key_ix
+on secd.credential (partial_key)
+where type = 'Passphrase';
+
create table if not exists secd.address (
address_id bigserial primary key
, address_public_id uuid not null
@@ -83,3 +87,23 @@ create table if not exists secd.message (
, created_at timestamptz not null
, sent_at timestamptz
);
+
+create table if not exists secd.namespace_config (
+ namespace text not null
+ , serialized_config text not null
+ , created_at xid8 not null
+ , deleted_at xid8
+ -- TODO: indexes and stuff
+);
+
+create table if not exists secd.relation_tuple (
+ namespace text not null
+ , object_id text not null
+ , relation text not null
+ , userset_namespace text not null
+ , userset_object_id text not null
+ , userset_relation text not null
+ , created_at xid8 not null
+ , deleted_at xid8 not null
+ -- TODO: indexes and stuff
+);
diff --git a/crates/secd/store/pg/sql/find_credential.sql b/crates/secd/store/pg/sql/find_credential.sql
new file mode 100644
index 0000000..e30c0ea
--- /dev/null
+++ b/crates/secd/store/pg/sql/find_credential.sql
@@ -0,0 +1,12 @@
+select c.credential_public_id
+ , i.identity_public_id
+ , c.data::text
+ , c.created_at
+ , c.revoked_at
+ , c.deleted_at
+from secd.credential c
+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))
diff --git a/crates/secd/store/pg/sql/write_credential.sql b/crates/secd/store/pg/sql/write_credential.sql
new file mode 100644
index 0000000..17e03a2
--- /dev/null
+++ b/crates/secd/store/pg/sql/write_credential.sql
@@ -0,0 +1,19 @@
+insert into secd.credential (
+ credential_public_id
+ , identity_id
+ , partial_key
+ , type
+ , data
+ , created_at
+ , revoked_at
+ , deleted_at
+) values (
+ $1
+ , (select identity_id from secd.identity where identity_public_id = $2)
+ , $3
+ , $4
+ , $5::jsonb
+ , $6
+ , $7
+ , $8
+);
diff --git a/crates/secd/store/pg/sql/write_identity.sql b/crates/secd/store/pg/sql/write_identity.sql
index 67662a6..4b2745b 100644
--- a/crates/secd/store/pg/sql/write_identity.sql
+++ b/crates/secd/store/pg/sql/write_identity.sql
@@ -5,7 +5,7 @@ insert into secd.identity (
, updated_at
, deleted_at
) values (
- $1, $2::jsonb, $3, $4, $5
+ $1, $2, $3, $4, $5
) on conflict (identity_public_id) do update
set data = excluded.data
, updated_at = excluded.updated_at
diff --git a/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql b/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
index 299f282..b2ce45d 100644
--- a/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
+++ b/crates/secd/store/sqlite/migrations/20221125051738_bootstrap.sql
@@ -15,7 +15,7 @@ create table if not exists realm_data (
create table if not exists identity (
identity_id integer primary key
, identity_public_id uuid not null
- , data text -- some things are dervied, others are not
+ , data text -- we do not prescribe JSON or any other serialization format
, created_at integer not null
, updated_at integer not null
, deleted_at integer
@@ -26,14 +26,18 @@ create table if not exists credential (
credential_id integer primary key
, credential_public_id uuid not null
, identity_id integer not null references identity(identity_id)
+ , partial_key text
, type text not null-- e.g. password, oidc, totop, lookup_secret, webauthn, ...
, data text not null
- , version integer not null
, created_at integer not null
, revoked_at integer
, deleted_at integer
);
+create unique index if not exists credential_passphrase_type_key_ix
+on credential (partial_key)
+where type = 'Passphrase';
+
create table if not exists address (
address_id integer primary key
, address_public_id uuid not null
diff --git a/crates/secd/store/sqlite/sql/find_credential.sql b/crates/secd/store/sqlite/sql/find_credential.sql
new file mode 100644
index 0000000..9062914
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/find_credential.sql
@@ -0,0 +1,12 @@
+select c.credential_public_id
+ , i.identity_public_id
+ , c.data
+ , c.created_at
+ , c.revoked_at
+ , c.deleted_at
+from credential c
+join identity i using (identity_id)
+where (($1 is null) or (c.credential_public_id = $1))
+and (($2 is null) or (i.identity_public_id = $2))
+and (($3 is null) or (c.type = $3))
+and (($3 is null or $4 is null) or (c.data->$3->>'key' = $4))
diff --git a/crates/secd/store/sqlite/sql/write_credential.sql b/crates/secd/store/sqlite/sql/write_credential.sql
new file mode 100644
index 0000000..3319226
--- /dev/null
+++ b/crates/secd/store/sqlite/sql/write_credential.sql
@@ -0,0 +1,19 @@
+insert into credential (
+ credential_public_id
+ , identity_id
+ , partial_key
+ , type
+ , data
+ , created_at
+ , revoked_at
+ , deleted_at
+) values (
+ $1
+ , (select identity_id from identity where identity_public_id = $2)
+ , $3
+ , $4
+ , $5
+ , $6
+ , $7
+ , $8
+);