Skip to content

Commit

Permalink
Merge pull request #4892 from systeminit/sf/sdf-api-perms
Browse files Browse the repository at this point in the history
feat: add `WorkspacePermission` axum middleware & v2 change set apply
  • Loading branch information
fnichol authored Oct 30, 2024
2 parents e12ce7b + 90c61ff commit 5e3e6fc
Show file tree
Hide file tree
Showing 21 changed files with 323 additions and 97 deletions.
2 changes: 0 additions & 2 deletions .ci/docker-compose.test-integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,3 @@ services:
- "SPICEDB_GRPC_PRESHARED_KEY=hobgoblin"
- "SPICEDB_DATASTORE_ENGINE=memory"
- "ZED_KEYRING_PASSWORD=orc"
ports:
- "50051:50051"
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions component/spicedb/schema.zed
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@
relation approver: user
relation owner: user
permission approve = approver+owner
permission manage = owner
}
1 change: 1 addition & 0 deletions lib/permissions/BUCK
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ rust_library(
name = "permissions",
deps = [
"//lib/si-data-spicedb:si-data-spicedb",
"//lib/si-events-rs:si-events",
"//third-party/rust:remain",
"//third-party/rust:serde",
"//third-party/rust:strum",
Expand Down
5 changes: 3 additions & 2 deletions lib/permissions/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ publish.workspace = true

[dependencies]
remain = { workspace = true }
strum = { workspace = true }
si-data-spicedb = { path = "../../lib/si-data-spicedb" }
serde = { workspace = true }
si-data-spicedb = { path = "../../lib/si-data-spicedb" }
si-events = { path = "../../lib/si-events-rs" }
strum = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }

Expand Down
46 changes: 28 additions & 18 deletions lib/permissions/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use si_data_spicedb::{
PermissionsObject, Relationship, Relationships, SpiceDbClient, SpiceDbError, ZedToken,
};
use si_events::{UserPk, WorkspacePk};
use std::result;
use thiserror::Error;

Expand All @@ -20,20 +21,21 @@ pub enum Error {

type Result<T> = result::Result<T, Error>;

#[derive(strum::Display, Debug)]
#[derive(Clone, Copy, strum::Display, Debug)]
#[strum(serialize_all = "snake_case")]
pub enum ObjectType {
User,
Workspace,
}

#[derive(Clone, strum::Display)]
#[derive(Clone, Copy, strum::Display)]
#[strum(serialize_all = "snake_case")]
pub enum Permission {
Approve,
Manage,
}

#[derive(Clone, strum::Display, Debug)]
#[derive(Clone, Copy, strum::Display, Debug)]
#[strum(serialize_all = "snake_case")]
pub enum Relation {
Approver,
Expand Down Expand Up @@ -76,6 +78,10 @@ impl RelationBuilder {
self
}

pub fn workspace_object(self, id: WorkspacePk) -> Self {
self.object(ObjectType::Workspace, id)
}

pub fn relation(mut self, relation: Relation) -> Self {
self.relation = Some(relation);
self
Expand All @@ -86,13 +92,17 @@ impl RelationBuilder {
self
}

pub fn user_subject(self, id: UserPk) -> Self {
self.subject(ObjectType::User, id)
}

pub fn zed_token(mut self, token: ZedToken) -> Self {
self.zed_token = Some(token.clone());
self
}

/// Creates a new relationship in SpiceDb
pub async fn create(&self, mut client: SpiceDbClient) -> Result<Option<ZedToken>> {
pub async fn create(&self, client: &mut SpiceDbClient) -> Result<Option<ZedToken>> {
match self.check() {
Ok(relationship) => client
.create_relationships(vec![relationship])
Expand All @@ -103,7 +113,7 @@ impl RelationBuilder {
}

/// Deletes an existing relationship in SpiceDb
pub async fn delete(&self, mut client: SpiceDbClient) -> Result<Option<ZedToken>> {
pub async fn delete(&self, client: &mut SpiceDbClient) -> Result<Option<ZedToken>> {
match self.check() {
Ok(relationship) => client
.delete_relationships(vec![relationship])
Expand All @@ -114,8 +124,8 @@ impl RelationBuilder {
}

/// Reads existing relations in SpiceDb for a given object and relation
pub async fn read(&self, mut client: SpiceDbClient) -> Result<Relationships> {
match (self.object.clone(), self.relation.clone()) {
pub async fn read(&self, client: &mut SpiceDbClient) -> Result<Relationships> {
match (self.object.clone(), self.relation) {
(Some(object), Some(relation)) => client
.read_relationship(Relationship::new(
object,
Expand All @@ -132,11 +142,7 @@ impl RelationBuilder {
}

fn check(&self) -> Result<Relationship> {
match (
self.object.clone(),
self.relation.clone(),
self.subject.clone(),
) {
match (self.object.clone(), self.relation, self.subject.clone()) {
(Some(object), Some(relation), Some(subject)) => Ok(Relationship::new(
object,
relation,
Expand Down Expand Up @@ -196,6 +202,10 @@ impl PermissionBuilder {
self
}

pub fn workspace_object(self, id: WorkspacePk) -> Self {
self.object(ObjectType::Workspace, id)
}

pub fn permission(mut self, permission: Permission) -> Self {
self.permission = Some(permission);
self
Expand All @@ -206,13 +216,17 @@ impl PermissionBuilder {
self
}

pub fn user_subject(self, id: UserPk) -> Self {
self.subject(ObjectType::User, id)
}

pub fn zed_token(mut self, token: ZedToken) -> Self {
self.zed_token = Some(token.clone());
self
}

/// Checks if the given subject has the given permission in the given object
pub async fn has_permission(&self, mut client: SpiceDbClient) -> Result<bool> {
pub async fn has_permission(&self, client: &mut SpiceDbClient) -> Result<bool> {
match self.check() {
Ok(perms) => Ok(client
.check_permissions(perms)
Expand All @@ -223,11 +237,7 @@ impl PermissionBuilder {
}

fn check(&self) -> Result<si_data_spicedb::Permission> {
match (
self.object.clone(),
self.permission.clone(),
self.subject.clone(),
) {
match (self.object.clone(), self.permission, self.subject.clone()) {
(Some(object), Some(permission), Some(subject)) => {
Ok(si_data_spicedb::Permission::new(
object,
Expand Down
6 changes: 0 additions & 6 deletions lib/permissions/src/schema/schema.zed

This file was deleted.

10 changes: 5 additions & 5 deletions lib/permissions/tests/integration_test/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ async fn write_schema(mut client: SpiceDbClient) {
async fn add_remove_approver_from_workspace() {
let config = spicedb_config();

let client = Client::new(&config)
let mut client = Client::new(&config)
.await
.expect("failed to connect to spicedb");

Expand All @@ -59,7 +59,7 @@ async fn add_remove_approver_from_workspace() {
.subject(ObjectType::User, user_id.clone());

let zed_token = relation
.create(client.clone())
.create(&mut client)
.await
.expect("could not create relationship")
.expect("could not unwrap zed token");
Expand All @@ -71,19 +71,19 @@ async fn add_remove_approver_from_workspace() {
.zed_token(zed_token);

assert!(can_approve
.has_permission(client.clone())
.has_permission(&mut client)
.await
.expect("could not check permission"));

let zed_token = relation
.delete(client.clone())
.delete(&mut client)
.await
.expect("could not delete permission")
.expect("could not unwrap zed token");

assert!(!can_approve
.zed_token(zed_token)
.has_permission(client.clone())
.has_permission(&mut client)
.await
.expect("could not check permission"));
}
8 changes: 6 additions & 2 deletions lib/sdf-server/src/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,8 +106,12 @@ impl AppState {
&self.shutdown_token
}

pub fn spicedb_client(&self) -> Option<&SpiceDbClient> {
self.spicedb_client.as_ref()
pub fn spicedb_client(&mut self) -> Option<&mut SpiceDbClient> {
self.spicedb_client.as_mut()
}

pub fn spicedb_client_clone(&self) -> Option<SpiceDbClient> {
self.spicedb_client.clone()
}
}

Expand Down
8 changes: 7 additions & 1 deletion lib/sdf-server/src/extract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,10 @@ impl FromRequestParts<AppState> for Authorization {
parts: &mut Parts,
state: &AppState,
) -> Result<Self, Self::Rejection> {
if let Some(claim) = parts.extensions.get::<UserClaim>() {
return Ok(Self(*claim));
}

let HandlerContext(builder) = HandlerContext::from_request_parts(parts, state).await?;
let mut ctx = builder.build_default().await.map_err(internal_error)?;
let jwt_public_signing_key = state.jwt_public_signing_key().clone();
Expand All @@ -210,6 +214,8 @@ impl FromRequestParts<AppState> for Authorization {
return Err(unauthorized_error());
}

parts.extensions.insert(claim);

Ok(Self(claim))
}
}
Expand Down Expand Up @@ -315,7 +321,7 @@ fn internal_error(message: impl fmt::Display) -> (StatusCode, Json<serde_json::V
)
}

fn unauthorized_error() -> (StatusCode, Json<serde_json::Value>) {
pub fn unauthorized_error() -> (StatusCode, Json<serde_json::Value>) {
let status_code = StatusCode::UNAUTHORIZED;
(
status_code,
Expand Down
1 change: 1 addition & 0 deletions lib/sdf-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ mod app_state;
mod config;
mod extract;
mod init;
pub mod middleware;
mod migrations;
mod nats_multiplexer;
mod routes;
Expand Down
3 changes: 3 additions & 0 deletions lib/sdf-server/src/middleware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod workspace_permission;

pub use self::workspace_permission::{WorkspacePermission, WorkspacePermissionLayer};
96 changes: 96 additions & 0 deletions lib/sdf-server/src/middleware/workspace_permission.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use std::task::{Context, Poll};

use axum::{
body::Body,
extract::FromRequestParts,
http::Request,
response::{IntoResponse, Response},
};
use futures::future::BoxFuture;
use permissions::{Permission, PermissionBuilder};
use tower::{Layer, Service};

use crate::{
extract::{self, Authorization},
AppState,
};

#[derive(Clone)]
pub struct WorkspacePermissionLayer {
state: AppState,
permission: Permission,
}

impl WorkspacePermissionLayer {
pub fn new(state: AppState, permission: Permission) -> Self {
Self { state, permission }
}
}

impl<S> Layer<S> for WorkspacePermissionLayer {
type Service = WorkspacePermission<S>;

fn layer(&self, inner: S) -> Self::Service {
WorkspacePermission {
inner,
state: self.state.clone(),
permission: self.permission,
}
}
}

#[derive(Clone)]
pub struct WorkspacePermission<S> {
inner: S,
state: AppState,
permission: Permission,
}

impl<S> Service<Request<Body>> for WorkspacePermission<S>
where
S: Service<Request<Body>, Response = Response> + Clone + Send + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = BoxFuture<'static, Result<Self::Response, Self::Error>>;

fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

fn call(&mut self, req: Request<Body>) -> Self::Future {
let mut me = self.clone();

Box::pin(async move {
let (mut parts, body) = req.into_parts();

let Authorization(claim) =
match Authorization::from_request_parts(&mut parts, &me.state).await {
Ok(claim) => claim,
Err(err) => return Ok(err.into_response()),
};

if let Some(client) = me.state.spicedb_client() {
let is_allowed = match PermissionBuilder::new()
.workspace_object(claim.workspace_pk.into())
.permission(me.permission)
.user_subject(claim.user_pk.into())
.has_permission(client)
.await
{
Ok(is_allowed) => is_allowed,
Err(_) => return Ok(extract::unauthorized_error().into_response()),
};
if !is_allowed {
return Ok(extract::unauthorized_error().into_response());
}
}

let req = Request::from_parts(parts, body);

let response = me.inner.call(req).await?;
Ok(response)
})
}
}
5 changes: 4 additions & 1 deletion lib/sdf-server/src/routes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@ pub fn routes(state: AppState) -> Router {
crate::service::qualification::routes(),
)
.nest("/api/secret", crate::service::secret::routes())
.nest("/api/session", crate::service::session::routes())
.nest(
"/api/session",
crate::service::session::routes(state.clone()),
)
.nest("/api/ws", crate::service::ws::routes())
.nest("/api/module", crate::service::module::routes())
.nest("/api/variant", crate::service::variant::routes())
Expand Down
Loading

0 comments on commit 5e3e6fc

Please sign in to comment.