Skip to content

Commit

Permalink
Simplify security.rs and authorization logic (#674)
Browse files Browse the repository at this point in the history
This PR refactors `security.rs` and our auth* related axum extractors.

## Motivation

* Cut down on lines-of-code
* Make permission/authorization parsing a bit more intuitive for axum
handlers
* Better leverage the type system to restrict axum handlers only to the
permission-level information they need.

## Solution

Some specifics
* Many of our extractors had redundant information, or largely unused
fields. This PR removes them
* The `Permissions` struct has been simplified a bit, namely by moving
things into an enum (as opposed to using the `type_` enum just as a tag.
* Removed the axum extractor for `Permissions`. We often chain
authorization logic on top of the `Permissions`. However, the axum
extractor for `Permissions` only authenticates requests, it doesn't
authorize them.
* Fixes a discrepancy between `list_event_types` and `get_event_type`.
`list_event_types` was not checking for Organization privileges like
`get_event_types` was.
* `AuthenticatedOrganization`, `AuthenticatedApp`, etc, were also
simplified a bit, and moved into a separate `permissions.rs` module.
  • Loading branch information
svix-gabriel authored Oct 20, 2022
2 parents 90d2bd5 + cc01b84 commit e0fe49f
Show file tree
Hide file tree
Showing 13 changed files with 206 additions and 280 deletions.
1 change: 1 addition & 0 deletions server/svix-server/src/core/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod idempotency;
pub mod message_app;
pub mod operational_webhooks;
pub mod otel_spans;
pub mod permissions;
pub mod run_with_retries;
pub mod security;
pub mod types;
Expand Down
107 changes: 107 additions & 0 deletions server/svix-server/src/core/permissions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
use axum::{
async_trait,
extract::{FromRequest, Path, RequestParts},
Extension,
};
use sea_orm::DatabaseConnection;

use crate::{
ctx,
db::models::application,
error::{Error, HttpError, Result},
};

use super::{
security::{permissions_from_bearer, AccessLevel},
types::{ApplicationIdOrUid, OrganizationId},
};

pub struct Organization {
pub org_id: OrganizationId,
}

#[async_trait]
impl<B> FromRequest<B> for Organization
where
B: Send,
{
type Rejection = Error;

async fn from_request(req: &mut RequestParts<B>) -> Result<Self> {
let permissions = permissions_from_bearer(req).await?;

let org_id = match permissions.access_level {
AccessLevel::Organization(org_id) => org_id,
_ => return Err(HttpError::permission_denied(None, None).into()),
};

Ok(Self { org_id })
}
}

pub struct Application {
pub app: application::Model,
}

#[async_trait]
impl<B> FromRequest<B> for Application
where
B: Send,
{
type Rejection = Error;

async fn from_request(req: &mut RequestParts<B>) -> Result<Self> {
let permissions = permissions_from_bearer(req).await?;

let Path(ApplicationPathParams { app_id }) =
ctx!(Path::<ApplicationPathParams>::from_request(req).await)?;
let Extension(ref db) = ctx!(Extension::<DatabaseConnection>::from_request(req).await)?;
let app = ctx!(
application::Entity::secure_find_by_id_or_uid(permissions.org_id(), app_id.to_owned(),)
.one(db)
.await
)?
.ok_or_else(|| HttpError::not_found(None, None))?;

if let Some(permitted_app_id) = permissions.app_id() {
if permitted_app_id != app.id {
return Err(HttpError::not_found(None, None).into());
}
}

Ok(Self { app })
}
}

// Organization level privileges, with the requested application
pub struct OrganizationWithApplication {
pub app: application::Model,
}

#[async_trait]
impl<B> FromRequest<B> for OrganizationWithApplication
where
B: Send,
{
type Rejection = Error;

async fn from_request(req: &mut RequestParts<B>) -> Result<Self> {
let Organization { org_id } = ctx!(Organization::from_request(req).await)?;

let Path(ApplicationPathParams { app_id }) =
ctx!(Path::<ApplicationPathParams>::from_request(req).await)?;
let Extension(ref db) = ctx!(Extension::<DatabaseConnection>::from_request(req).await)?;
let app = ctx!(
application::Entity::secure_find_by_id_or_uid(org_id, app_id.to_owned(),)
.one(db)
.await
)?
.ok_or_else(|| HttpError::not_found(None, None))?;
Ok(OrganizationWithApplication { app })
}
}

#[derive(serde::Deserialize)]
struct ApplicationPathParams {
app_id: ApplicationIdOrUid,
}
161 changes: 26 additions & 135 deletions server/svix-server/src/core/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,21 @@
use std::fmt::Debug;

use axum::{
async_trait,
extract::{Extension, FromRequest, Path, RequestParts, TypedHeader},
extract::{Extension, FromRequest, RequestParts, TypedHeader},
headers::{authorization::Bearer, Authorization},
};

use jwt_simple::prelude::*;
use sea_orm::DatabaseConnection;

use validator::Validate;

use crate::{
cfg::Configuration,
ctx,
db::models::application,
error::{Error, HttpError, Result},
error::{HttpError, Result},
};

use super::types::{ApplicationId, ApplicationIdOrUid, OrganizationId};
use super::types::{ApplicationId, OrganizationId};

/// The default org_id we use (useful for generating JWTs when testing).
pub fn default_org_id() -> OrganizationId {
Expand All @@ -32,16 +30,29 @@ pub fn management_org_id() -> OrganizationId {
OrganizationId("org_00000000000SvixManagement00".to_owned())
}

pub enum AccessLevel {
Organization(OrganizationId),
Application(OrganizationId, ApplicationId),
}

pub struct Permissions {
pub type_: KeyType,
pub org_id: OrganizationId,
pub app_id: Option<ApplicationId>,
pub access_level: AccessLevel,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum KeyType {
Organization,
Application,
impl Permissions {
pub fn org_id(&self) -> OrganizationId {
match &self.access_level {
AccessLevel::Organization(org_id) => org_id.clone(),
AccessLevel::Application(org_id, _) => org_id.clone(),
}
}

pub fn app_id(&self) -> Option<ApplicationId> {
match &self.access_level {
AccessLevel::Organization(_) => None,
AccessLevel::Application(_, app_id) => Some(app_id.clone()),
}
}
}

#[derive(Clone, Serialize, Deserialize)]
Expand All @@ -50,18 +61,6 @@ pub struct CustomClaim {
pub organization: Option<String>,
}

#[async_trait]
impl<B> FromRequest<B> for Permissions
where
B: Send,
{
type Rejection = Error;

async fn from_request(req: &mut RequestParts<B>) -> Result<Self> {
permissions_from_bearer(req).await
}
}

pub async fn permissions_from_bearer<B: Send>(req: &mut RequestParts<B>) -> Result<Permissions> {
let Extension(ref cfg) = ctx!(Extension::<Configuration>::from_request(req).await)?;

Expand Down Expand Up @@ -102,9 +101,7 @@ pub fn permissions_from_jwt(claims: JWTClaims<CustomClaim>) -> Result<Permission
.map_err(|_| bad_token("sub", "application"))?;

Ok(Permissions {
org_id,
app_id: Some(app_id),
type_: KeyType::Application,
access_level: AccessLevel::Application(org_id, app_id),
})
} else {
Err(
Expand All @@ -123,9 +120,7 @@ pub fn permissions_from_jwt(claims: JWTClaims<CustomClaim>) -> Result<Permission
)
})?;
Ok(Permissions {
org_id,
app_id: None,
type_: KeyType::Organization,
access_level: AccessLevel::Organization(org_id),
})
} else {
Err(
Expand All @@ -135,110 +130,6 @@ pub fn permissions_from_jwt(claims: JWTClaims<CustomClaim>) -> Result<Permission
}
}

pub struct AuthenticatedOrganization {
pub permissions: Permissions,
}

#[async_trait]
impl<B> FromRequest<B> for AuthenticatedOrganization
where
B: Send,
{
type Rejection = Error;

async fn from_request(req: &mut RequestParts<B>) -> Result<Self> {
let permissions = Permissions::from_request(req).await?;
match permissions.type_ {
KeyType::Organization => {}
KeyType::Application => {
return Err(HttpError::permission_denied(None, None).into());
}
}

Ok(AuthenticatedOrganization { permissions })
}
}

#[derive(Deserialize)]
struct ApplicationPathParams {
app_id: ApplicationIdOrUid,
}

pub struct AuthenticatedOrganizationWithApplication {
pub permissions: Permissions,
pub app: application::Model,
}

#[async_trait]
impl<B> FromRequest<B> for AuthenticatedOrganizationWithApplication
where
B: Send,
{
type Rejection = Error;

async fn from_request(req: &mut RequestParts<B>) -> Result<Self> {
let permissions = Permissions::from_request(req).await?;

match permissions.type_ {
KeyType::Organization => {}
KeyType::Application => {
return Err(HttpError::permission_denied(None, None).into());
}
}

let Path(ApplicationPathParams { app_id }) =
ctx!(Path::<ApplicationPathParams>::from_request(req).await)?;
let Extension(ref db) = ctx!(Extension::<DatabaseConnection>::from_request(req).await)?;
let app = ctx!(
application::Entity::secure_find_by_id_or_uid(
permissions.org_id.clone(),
app_id.to_owned(),
)
.one(db)
.await
)?
.ok_or_else(|| HttpError::not_found(None, None))?;
Ok(AuthenticatedOrganizationWithApplication { permissions, app })
}
}

pub struct AuthenticatedApplication {
pub permissions: Permissions,
pub app: application::Model,
}

#[async_trait]
impl<B> FromRequest<B> for AuthenticatedApplication
where
B: Send,
{
type Rejection = Error;

async fn from_request(req: &mut RequestParts<B>) -> Result<Self> {
let permissions = Permissions::from_request(req).await?;
let Path(ApplicationPathParams { app_id }) =
ctx!(Path::<ApplicationPathParams>::from_request(req).await)?;
let Extension(ref db) = ctx!(Extension::<DatabaseConnection>::from_request(req).await)?;
let app = ctx!(
application::Entity::secure_find_by_id_or_uid(
permissions.org_id.clone(),
app_id.to_owned(),
)
.one(db)
.await
)?
.ok_or_else(|| HttpError::not_found(None, None))?;

if let Some(permitted_app_id) = &permissions.app_id {
if permitted_app_id != &app.id {
return Err(HttpError::not_found(None, None).into());
}
}

Ok(AuthenticatedApplication { permissions, app })
}
}

const JWT_ISSUER: &str = env!("CARGO_PKG_NAME");

pub fn generate_org_token(keys: &Keys, org_id: OrganizationId) -> Result<String> {
Expand Down
Loading

0 comments on commit e0fe49f

Please sign in to comment.