diff options
| author | benj <benj@rse8.com> | 2022-12-30 15:57:36 -0800 |
|---|---|---|
| committer | benj <benj@rse8.com> | 2022-12-30 15:57:36 -0800 |
| commit | 8ca3433b2a4a82723e00e64b1e5aff0b1bed95b3 (patch) | |
| tree | 1ff85fd9fbd94a5559f9dbac755973fd58b31f28 /crates/secd/src | |
| parent | f0ea9ecd17b03605d747044874a26e1bd52c0ee1 (diff) | |
| download | secdiam-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 '')
| -rw-r--r-- | crates/secd/src/auth/mod.rs | 2 | ||||
| -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.rs | 54 | ||||
| -rw-r--r-- | crates/secd/src/client/mod.rs | 1 | ||||
| -rw-r--r-- | crates/secd/src/client/spice/mod.rs | 154 | ||||
| -rw-r--r-- | crates/secd/src/command/mod.rs | 74 | ||||
| -rw-r--r-- | crates/secd/src/lib.rs | 97 |
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, + }) + } +} |
