aboutsummaryrefslogtreecommitdiff
path: root/crates/secd/src
diff options
context:
space:
mode:
authorbenj <benj@rse8.com>2022-12-30 15:57:36 -0800
committerbenj <benj@rse8.com>2022-12-30 15:57:36 -0800
commit8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3 (patch)
tree1ff85fd9fbd94a5559f9dbac755973fd58b31f28 /crates/secd/src
parentf0ea9ecd17b03605d747044874a26e1bd52c0ee1 (diff)
downloadsecdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar
secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar.gz
secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar.bz2
secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar.lz
secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar.xz
secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.tar.zst
secdiam-8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3.zip
impl authZ write and check (depends on spicedb for now)
Diffstat (limited to 'crates/secd/src')
-rw-r--r--crates/secd/src/auth/mod.rs2
-rw-r--r--crates/secd/src/auth/n.rs (renamed from crates/secd/src/command/authn.rs)0
-rw-r--r--crates/secd/src/auth/z.rs54
-rw-r--r--crates/secd/src/client/mod.rs1
-rw-r--r--crates/secd/src/client/spice/mod.rs154
-rw-r--r--crates/secd/src/command/mod.rs74
-rw-r--r--crates/secd/src/lib.rs97
7 files changed, 304 insertions, 78 deletions
diff --git a/crates/secd/src/auth/mod.rs b/crates/secd/src/auth/mod.rs
new file mode 100644
index 0000000..9275c79
--- /dev/null
+++ b/crates/secd/src/auth/mod.rs
@@ -0,0 +1,2 @@
+pub mod n;
+pub mod z;
diff --git a/crates/secd/src/command/authn.rs b/crates/secd/src/auth/n.rs
index 1d3b2d5..1d3b2d5 100644
--- a/crates/secd/src/command/authn.rs
+++ b/crates/secd/src/auth/n.rs
diff --git a/crates/secd/src/auth/z.rs b/crates/secd/src/auth/z.rs
new file mode 100644
index 0000000..81c3639
--- /dev/null
+++ b/crates/secd/src/auth/z.rs
@@ -0,0 +1,54 @@
+use uuid::Uuid;
+
+use crate::{client::spice::SpiceError, Secd};
+
+#[derive(Debug, thiserror::Error, derive_more::Display)]
+pub enum AuthZError {
+ SpiceClient(#[from] SpiceError),
+ Todo,
+}
+
+pub type Namespace = String;
+pub type Object = (Namespace, Uuid);
+pub type Relation = String;
+
+pub struct Relationship {
+ pub subject: Subject,
+ pub object: Object,
+ pub relation: String,
+}
+
+#[derive(Clone)]
+pub enum Subject {
+ User(Object),
+ UserSet { user: Object, relation: Relation },
+}
+
+impl Secd {
+ pub async fn check(&self, r: &Relationship) -> Result<bool, AuthZError> {
+ let spice = self
+ .spice
+ .clone()
+ .expect("TODO: only supports postgres right now");
+
+ Ok(spice.check_permission(r).await?)
+ }
+ pub async fn expand(&self) -> Result<(), AuthZError> {
+ todo!()
+ }
+ pub async fn read(&self) -> Result<(), AuthZError> {
+ todo!()
+ }
+ pub async fn watch(&self) -> Result<(), AuthZError> {
+ unimplemented!()
+ }
+ pub async fn write(&self, ts: &[Relationship]) -> Result<(), AuthZError> {
+ let spice = self
+ .spice
+ .clone()
+ .expect("TODO: only supports postgres right now");
+
+ spice.write_relationship(ts).await?;
+ Ok(())
+ }
+}
diff --git a/crates/secd/src/client/mod.rs b/crates/secd/src/client/mod.rs
index e5272fd..709ecad 100644
--- a/crates/secd/src/client/mod.rs
+++ b/crates/secd/src/client/mod.rs
@@ -1,2 +1,3 @@
pub(crate) mod email;
+pub(crate) mod spice;
pub(crate) mod store;
diff --git a/crates/secd/src/client/spice/mod.rs b/crates/secd/src/client/spice/mod.rs
new file mode 100644
index 0000000..d3ca30d
--- /dev/null
+++ b/crates/secd/src/client/spice/mod.rs
@@ -0,0 +1,154 @@
+// TODO: This whole thing depends on having spice server running...which I do not want
+// in a public secd library (or really at all). We will eventually get rid of this in
+// favor of a light weight solution that leverages the Zanzibar API but disregards the
+// scaling part.
+
+pub mod spice {
+ tonic::include_proto!("authzed.api.v1");
+}
+
+use spice::permissions_service_client::PermissionsServiceClient;
+use spice::schema_service_client::SchemaServiceClient;
+use spice::WriteSchemaRequest;
+use std::env::var;
+use tonic::metadata::MetadataValue;
+use tonic::transport::Channel;
+use tonic::{Request, Status};
+
+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};
+
+#[derive(Debug, thiserror::Error, derive_more::Display)]
+pub enum SpiceError {
+ TonicTransport(#[from] tonic::transport::Error),
+ TonicStatus(#[from] tonic::Status),
+}
+
+pub(crate) struct Spice {
+ channel: Channel,
+ secret: String,
+}
+
+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");
+
+ let channel = Channel::from_shared(server)
+ .expect("invalid SPICE_SERVER uri")
+ .connect()
+ .await
+ .expect("initialization error: Spice failed to connect to DB.");
+
+ Spice { channel, secret }
+ }
+
+ pub async fn check_permission(&self, r: &z::Relationship) -> Result<bool, SpiceError> {
+ let mut client =
+ PermissionsServiceClient::with_interceptor(self.channel.clone(), |req: Request<()>| {
+ self.intercept(req)
+ });
+
+ let request = tonic::Request::new(CheckPermissionRequest {
+ consistency: Some(Consistency {
+ requirement: Some(consistency::Requirement::MinimizeLatency(true)),
+ }),
+ resource: Some(ObjectReference::from(&r.object)),
+ permission: r.relation.clone(),
+ subject: Some(SubjectReference::from(&r.subject)),
+ context: None,
+ });
+
+ let response = client.check_permission(request).await?.into_inner();
+
+ Ok(match Permissionship::from_i32(response.permissionship) {
+ Some(Permissionship::HasPermission) => true,
+ _ => false,
+ })
+ }
+
+ pub async fn write_relationship(&self, rs: &[z::Relationship]) -> Result<(), SpiceError> {
+ let mut client =
+ PermissionsServiceClient::with_interceptor(self.channel.clone(), |req: Request<()>| {
+ self.intercept(req)
+ });
+
+ let request = tonic::Request::new(WriteRelationshipsRequest {
+ updates: rs
+ .into_iter()
+ .map(|t| RelationshipUpdate {
+ operation: (relationship_update::Operation::Touch as i32),
+ relationship: Some(Relationship {
+ resource: Some(ObjectReference::from(&t.object)),
+ relation: t.relation.clone(),
+ subject: Some(SubjectReference::from(&t.subject)),
+ optional_caveat: None,
+ }),
+ })
+ .collect(),
+ optional_preconditions: vec![],
+ });
+
+ client.write_relationships(request).await?;
+
+ Ok(())
+ }
+
+ pub async fn write_schema(&self, schema: &str) -> Result<(), SpiceError> {
+ let mut client =
+ SchemaServiceClient::with_interceptor(self.channel.clone(), |req: Request<()>| {
+ self.intercept(req)
+ });
+ let request = tonic::Request::new(WriteSchemaRequest {
+ schema: schema.into(),
+ });
+
+ client.write_schema(request).await?;
+
+ Ok(())
+ }
+
+ fn intercept(&self, mut req: Request<()>) -> Result<Request<()>, Status> {
+ req.metadata_mut().insert(
+ "authorization",
+ MetadataValue::from_str(&format!("Bearer {}", self.secret)).unwrap(),
+ );
+ Ok(req)
+ }
+}
+
+impl From<&z::Subject> for SubjectReference {
+ fn from(s: &z::Subject) -> Self {
+ let tup = match s {
+ Subject::User(u) => (u.0.clone(), u.1.clone().to_string(), "".to_string()),
+ Subject::UserSet { user, relation } => {
+ (user.0.clone(), user.1.clone().to_string(), relation.clone())
+ }
+ };
+
+ SubjectReference {
+ object: Some(ObjectReference {
+ object_type: tup.0,
+ object_id: tup.1,
+ }),
+ optional_relation: tup.2,
+ }
+ }
+}
+
+impl From<&z::Object> for ObjectReference {
+ fn from(o: &z::Object) -> Self {
+ ObjectReference {
+ object_type: o.0.clone(),
+ object_id: o.1.clone().to_string(),
+ }
+ }
+}
diff --git a/crates/secd/src/command/mod.rs b/crates/secd/src/command/mod.rs
deleted file mode 100644
index c14cf6c..0000000
--- a/crates/secd/src/command/mod.rs
+++ /dev/null
@@ -1,74 +0,0 @@
-pub mod authn;
-
-use super::{AuthEmailMessenger, AuthStore, Secd, SecdError};
-use crate::{
- client::{
- email,
- store::sql_db::{PgClient, SqliteClient},
- },
- ENV_AUTH_STORE_CONN_STRING, ENV_EMAIL_MESSENGER, ENV_EMAIL_MESSENGER_CLIENT_ID,
- ENV_EMAIL_MESSENGER_CLIENT_SECRET,
-};
-use log::{error, info};
-use std::{env::var, str::FromStr, sync::Arc};
-
-impl Secd {
- /// init
- ///
- /// Initialize SecD with the specified configuration, established the necessary
- /// constraints, persistance stores, and options.
- pub async fn init() -> Result<Self, SecdError> {
- let auth_store = AuthStore::from(var(ENV_AUTH_STORE_CONN_STRING).ok());
- let email_messenger = AuthEmailMessenger::from_str(
- &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();
-
- info!("starting client with auth_store: {:?}", auth_store);
- info!("starting client with email_messenger: {:?}", auth_store);
-
- let store = match auth_store {
- AuthStore::Sqlite { conn } => {
- SqliteClient::new(
- sqlx::sqlite::SqlitePoolOptions::new()
- .connect(&conn)
- .await
- .map_err(|e| {
- SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e))
- })?,
- )
- .await
- }
- AuthStore::Postgres { conn } => {
- PgClient::new(
- sqlx::postgres::PgPoolOptions::new()
- .connect(&conn)
- .await
- .map_err(|e| {
- SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e))
- })?,
- )
- .await
- }
- rest @ _ => {
- error!(
- "requested an AuthStore which has not yet been implemented: {:?}",
- rest
- );
- unimplemented!()
- }
- };
-
- let email_sender = match email_messenger {
- AuthEmailMessenger::Local => email::LocalMailer {},
- _ => unimplemented!(),
- };
-
- Ok(Secd {
- store,
- email_messenger: Arc::new(email_sender),
- })
- }
-}
diff --git a/crates/secd/src/lib.rs b/crates/secd/src/lib.rs
index c84f7cf..15a92a8 100644
--- a/crates/secd/src/lib.rs
+++ b/crates/secd/src/lib.rs
@@ -1,15 +1,20 @@
+pub mod auth;
mod client;
-mod command;
mod util;
use client::{
- email::{EmailMessenger, EmailMessengerError},
- store::{Store, StoreError},
+ email::{EmailMessenger, EmailMessengerError, LocalMailer},
+ spice::Spice,
+ store::{
+ sql_db::{PgClient, SqliteClient},
+ Store, StoreError,
+ },
};
use email_address::EmailAddress;
+use log::{error, info};
use serde::{Deserialize, Serialize};
use serde_with::{serde_as, DisplayFromStr};
-use std::sync::Arc;
+use std::{env::var, str::FromStr, sync::Arc};
use strum_macros::{Display, EnumString, EnumVariantNames};
use time::OffsetDateTime;
use url::Url;
@@ -19,6 +24,8 @@ pub const ENV_AUTH_STORE_CONN_STRING: &str = "SECD_AUTH_STORE_CONN_STRING";
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_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 */;
@@ -55,12 +62,15 @@ pub enum SecdError {
StoreInitFailure(String),
FailedToDecodeInput(#[from] hex::FromHexError),
+
+ AuthorizationNotSupported(String),
Todo,
}
pub struct Secd {
store: Arc<dyn Store + Send + Sync + 'static>,
email_messenger: Arc<dyn EmailMessenger + Send + Sync + 'static>,
+ spice: Option<Arc<Spice>>,
}
#[derive(Display, Debug, Serialize, Deserialize, EnumString, EnumVariantNames)]
@@ -184,3 +194,82 @@ pub struct Session {
#[serde(with = "time::serde::timestamp::option")]
pub revoked_at: Option<OffsetDateTime>,
}
+
+impl Secd {
+ /// init
+ ///
+ /// 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());
+ let email_messenger = AuthEmailMessenger::from_str(
+ &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();
+
+ info!("starting client with auth_store: {:?}", auth_store);
+ info!("starting client with email_messenger: {:?}", auth_store);
+
+ let store = match auth_store {
+ AuthStore::Sqlite { conn } => {
+ if z_schema.is_some() {
+ return Err(SecdError::AuthorizationNotSupported(
+ "sqlite is currently unsupported".into(),
+ ));
+ }
+
+ SqliteClient::new(
+ sqlx::sqlite::SqlitePoolOptions::new()
+ .connect(&conn)
+ .await
+ .map_err(|e| {
+ SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e))
+ })?,
+ )
+ .await
+ }
+ AuthStore::Postgres { conn } => {
+ PgClient::new(
+ sqlx::postgres::PgPoolOptions::new()
+ .connect(&conn)
+ .await
+ .map_err(|e| {
+ SecdError::StoreInitFailure(format!("failed to init sqlite: {}", e))
+ })?,
+ )
+ .await
+ }
+ rest @ _ => {
+ error!(
+ "requested an AuthStore which has not yet been implemented: {:?}",
+ rest
+ );
+ unimplemented!()
+ }
+ };
+
+ let email_sender = match email_messenger {
+ AuthEmailMessenger::Local => LocalMailer {},
+ _ => unimplemented!(),
+ };
+
+ let spice = match z_schema {
+ Some(schema) => {
+ let c: Arc<Spice> = Arc::new(Spice::new().await);
+ c.write_schema(schema)
+ .await
+ .expect("failed to write authorization schema".into());
+ Some(c)
+ }
+ None => None,
+ };
+
+ Ok(Secd {
+ store,
+ email_messenger: Arc::new(email_sender),
+ spice,
+ })
+ }
+}