Skip to content

Commit

Permalink
Merge pull request #81 from refactor-group/continued_authz
Browse files Browse the repository at this point in the history
Continued authz
  • Loading branch information
calebbourg authored Jan 4, 2025
2 parents e120410 + 501d1d6 commit a0428fd
Show file tree
Hide file tree
Showing 14 changed files with 457 additions and 19 deletions.
3 changes: 2 additions & 1 deletion entity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ serde = { version = "1.0.210", features = ["derive"] }
sqlx = { version = "0.8.2", features = ["time", "runtime-tokio"] }
sqlx-sqlite = { version = "0.8.2" }
utoipa = { version = "4.2.0", features = ["axum_extras", "uuid"] }
uuid = { version = "1.11.0", features = ["v4"] }

uuid = { version = "1.11.0", features = ["v4", "serde"] }

[dependencies.sea-orm]
version = "1.1.0"
Expand Down
26 changes: 26 additions & 0 deletions entity_api/src/coaching_relationship.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ pub async fn create(
Ok(coaching_relationship_active_model.insert(db).await?)
}

pub async fn find_by_id(db: &DatabaseConnection, id: Id) -> Result<Option<Model>, Error> {
Ok(Entity::find_by_id(id).one(db).await?)
}

pub async fn find_by_user(db: &DatabaseConnection, user_id: Id) -> Result<Vec<Model>, Error> {
let coaching_relationships: Vec<coaching_relationships::Model> =
coaching_relationships::Entity::find()
Expand Down Expand Up @@ -233,6 +237,28 @@ mod tests {
use super::*;
use sea_orm::{DatabaseBackend, MockDatabase, Transaction};

#[tokio::test]
async fn find_by_id_returns_record_when_present() -> Result<(), Error> {
let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();

let coaching_relationship_id = Id::new_v4();
let _ = find_by_id(&db, coaching_relationship_id).await;

assert_eq!(
db.into_transaction_log(),
[Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "coaching_relationships"."id", "coaching_relationships"."organization_id", "coaching_relationships"."coach_id", "coaching_relationships"."coachee_id", "coaching_relationships"."created_at", "coaching_relationships"."updated_at" FROM "refactor_platform"."coaching_relationships" WHERE "coaching_relationships"."id" = $1 LIMIT $2"#,
[
coaching_relationship_id.into(),
sea_orm::Value::BigUnsigned(Some(1))
]
)]
);

Ok(())
}

#[tokio::test]
async fn find_by_user_returns_all_records_associated_with_user() -> Result<(), Error> {
let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();
Expand Down
73 changes: 72 additions & 1 deletion entity_api/src/coaching_session.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
use super::error::{EntityApiErrorCode, Error};
use crate::{naive_date_parse_str, uuid_parse_str};
use entity::coaching_sessions::{self, ActiveModel, Entity, Model};
use entity::{
coaching_relationships,
coaching_sessions::{self, ActiveModel, Entity, Model},
Id,
};
use log::debug;
use sea_orm::{entity::prelude::*, DatabaseConnection, Set, TryIntoModel};
use std::collections::HashMap;
Expand Down Expand Up @@ -31,6 +35,29 @@ pub async fn create(
.try_into_model()?)
}

pub async fn find_by_id(db: &DatabaseConnection, id: Id) -> Result<Option<Model>, Error> {
Ok(Entity::find_by_id(id).one(db).await?)
}

pub async fn find_by_id_with_coaching_relationship(
db: &DatabaseConnection,
id: Id,
) -> Result<(Model, coaching_relationships::Model), Error> {
if let Some(results) = Entity::find_by_id(id)
.find_also_related(coaching_relationships::Entity)
.one(db)
.await?
{
if let Some(coaching_relationship) = results.1 {
return Ok((results.0, coaching_relationship));
}
}
Err(Error {
inner: None,
error_code: EntityApiErrorCode::RecordNotFound,
})
}

pub async fn find_by(
db: &DatabaseConnection,
params: HashMap<String, String>,
Expand Down Expand Up @@ -100,6 +127,50 @@ mod tests {
Ok(())
}

#[tokio::test]
async fn find_by_id_returns_a_single_record() -> Result<(), Error> {
let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();

let coaching_session_id = Id::new_v4();
let _ = find_by_id(&db, coaching_session_id).await;

assert_eq!(
db.into_transaction_log(),
[Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "coaching_sessions"."id", "coaching_sessions"."coaching_relationship_id", "coaching_sessions"."date", "coaching_sessions"."timezone", "coaching_sessions"."created_at", "coaching_sessions"."updated_at" FROM "refactor_platform"."coaching_sessions" WHERE "coaching_sessions"."id" = $1 LIMIT $2"#,
[
coaching_session_id.into(),
sea_orm::Value::BigUnsigned(Some(1))
]
)]
);

Ok(())
}

#[tokio::test]
async fn find_by_id_with_coaching_relationship_returns_a_single_record() -> Result<(), Error> {
let db = MockDatabase::new(DatabaseBackend::Postgres).into_connection();

let coaching_session_id = Id::new_v4();
let _ = find_by_id_with_coaching_relationship(&db, coaching_session_id).await;

assert_eq!(
db.into_transaction_log(),
[Transaction::from_sql_and_values(
DatabaseBackend::Postgres,
r#"SELECT "coaching_sessions"."id" AS "A_id", "coaching_sessions"."coaching_relationship_id" AS "A_coaching_relationship_id", "coaching_sessions"."date" AS "A_date", "coaching_sessions"."timezone" AS "A_timezone", "coaching_sessions"."created_at" AS "A_created_at", "coaching_sessions"."updated_at" AS "A_updated_at", "coaching_relationships"."id" AS "B_id", "coaching_relationships"."organization_id" AS "B_organization_id", "coaching_relationships"."coach_id" AS "B_coach_id", "coaching_relationships"."coachee_id" AS "B_coachee_id", "coaching_relationships"."created_at" AS "B_created_at", "coaching_relationships"."updated_at" AS "B_updated_at" FROM "refactor_platform"."coaching_sessions" LEFT JOIN "refactor_platform"."coaching_relationships" ON "coaching_sessions"."coaching_relationship_id" = "coaching_relationships"."id" WHERE "coaching_sessions"."id" = $1 LIMIT $2"#,
[
coaching_session_id.into(),
sea_orm::Value::BigUnsigned(Some(1))
]
)]
);

Ok(())
}

#[tokio::test]
async fn find_by_coaching_relationships_returns_all_records_associated_with_coaching_relationship(
) -> Result<(), Error> {
Expand Down
4 changes: 2 additions & 2 deletions migration/src/refactor_platform_rs.sql
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
-- SQL dump generated using DBML (dbml-lang.org)
-- SQL dump generated using DBML (dbml.dbdiagram.io)
-- Database: PostgreSQL
-- Generated at: 2024-12-16T05:18:20.372Z
-- Generated at: 2025-01-04T15:25:10.725Z


CREATE TYPE "refactor_platform"."status" AS ENUM (
Expand Down
6 changes: 4 additions & 2 deletions web/src/controller/organization_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@ use log::debug;
)]
pub async fn index(
CompareApiVersion(_v): CompareApiVersion,
AuthenticatedUser(_user): AuthenticatedUser,
AuthenticatedUser(user): AuthenticatedUser,
// TODO: create a new Extractor to authorize the user to access
// the data requested
State(app_state): State<AppState>,
Query(params): Query<HashMap<String, String>>,
Query(mut params): Query<HashMap<String, String>>,
) -> Result<impl IntoResponse, Error> {
debug!("GET all Organizations");

params.insert("user_id".to_string(), user.id.to_string());

let organizations = OrganizationApi::find_by(app_state.db_conn_ref(), params).await?;

debug!("Found Organizations: {:?}", organizations);
Expand Down
1 change: 1 addition & 0 deletions web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use tower_http::cors::CorsLayer;
mod controller;
mod error;
pub(crate) mod extractors;
pub(crate) mod protect;
mod router;

pub async fn init_server(app_state: AppState) -> Result<()> {
Expand Down
51 changes: 51 additions & 0 deletions web/src/protect/actions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::{extractors::authenticated_user::AuthenticatedUser, AppState};
use axum::{
extract::{Query, Request, State},
http::StatusCode,
middleware::Next,
response::IntoResponse,
};
use entity::Id;
use entity_api::coaching_session;
use log::*;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub(crate) struct QueryParams {
coaching_session_id: Id,
}

/// Checks that coaching relationship record associated with the coaching session
/// referenced by `coaching_session_id exists and that the authenticated user is associated with it.
/// Intended to be given to axum::middleware::from_fn_with_state in the router
pub(crate) async fn index(
State(app_state): State<AppState>,
AuthenticatedUser(user): AuthenticatedUser,
Query(params): Query<QueryParams>,
request: Request,
next: Next,
) -> impl IntoResponse {
match coaching_session::find_by_id_with_coaching_relationship(
app_state.db_conn_ref(),
params.coaching_session_id,
)
.await
{
Ok((_coaching_session, coaching_relationship)) => {
if coaching_relationship.coach_id == user.id
|| coaching_relationship.coachee_id == user.id
{
// User has access to coaching relationship
next.run(request).await
} else {
// User does not have access to coaching relationship
(StatusCode::UNAUTHORIZED, "UNAUTHORIZED").into_response()
}
}
Err(e) => {
error!("Error authorizing overarching goals index{:?}", e);

(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL SERVER ERROR").into_response()
}
}
}
51 changes: 51 additions & 0 deletions web/src/protect/agreements.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::{extractors::authenticated_user::AuthenticatedUser, AppState};
use axum::{
extract::{Query, Request, State},
http::StatusCode,
middleware::Next,
response::IntoResponse,
};
use entity::Id;
use entity_api::coaching_session;
use log::*;
use serde::Deserialize;

#[derive(Debug, Deserialize)]
pub(crate) struct QueryParams {
coaching_session_id: Id,
}

/// Checks that coaching relationship record associated with the coaching session
/// referenced by `coaching_session_id exists and that the authenticated user is associated with it.
/// Intended to be given to axum::middleware::from_fn_with_state in the router
pub(crate) async fn index(
State(app_state): State<AppState>,
AuthenticatedUser(user): AuthenticatedUser,
Query(params): Query<QueryParams>,
request: Request,
next: Next,
) -> impl IntoResponse {
match coaching_session::find_by_id_with_coaching_relationship(
app_state.db_conn_ref(),
params.coaching_session_id,
)
.await
{
Ok((_coaching_session, coaching_relationship)) => {
if coaching_relationship.coach_id == user.id
|| coaching_relationship.coachee_id == user.id
{
// User has access to coaching relationship
next.run(request).await
} else {
// User does not have access to coaching relationship
(StatusCode::UNAUTHORIZED, "UNAUTHORIZED").into_response()
}
}
Err(e) => {
error!("Error authorizing overarching goals index{:?}", e);

(StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL SERVER ERROR").into_response()
}
}
}
34 changes: 34 additions & 0 deletions web/src/protect/coaching_relationships.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use crate::{extractors::authenticated_user::AuthenticatedUser, AppState};
use axum::{
extract::{Path, Request, State},
http::StatusCode,
middleware::Next,
response::IntoResponse,
};

use entity::Id;
use entity_api::organization;
use std::collections::HashSet;

/// Checks that the organization record referenced by `organization_id`
/// exists and that the authenticated user is associated with i.t
/// Intended to be given to axum::middleware::from_fn_with_state in the router
pub(crate) async fn index(
State(app_state): State<AppState>,
AuthenticatedUser(user): AuthenticatedUser,
Path(organization_id): Path<Id>,
request: Request,
next: Next,
) -> impl IntoResponse {
let user_organization_ids = organization::find_by_user(app_state.db_conn_ref(), user.id)
.await
.unwrap_or(vec![])
.into_iter()
.map(|org| org.id)
.collect::<HashSet<Id>>();
if user_organization_ids.contains(&organization_id) {
next.run(request).await
} else {
(StatusCode::UNAUTHORIZED, "UNAUTHORIZED").into_response()
}
}
47 changes: 47 additions & 0 deletions web/src/protect/coaching_sessions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
use crate::{extractors::authenticated_user::AuthenticatedUser, AppState};
use axum::{
extract::{Query, Request, State},
http::StatusCode,
middleware::Next,
response::IntoResponse,
};
use serde::Deserialize;

use entity::Id;
use entity_api::coaching_relationship;

#[derive(Debug, Deserialize)]
pub(crate) struct QueryParams {
coaching_relationship_id: Id,
}

/// Checks that coaching relationship record referenced by `coaching_relationship_id`
/// exists and that the authenticated user is associated with it.
/// Intended to be given to axum::middleware::from_fn_with_state in the router
pub(crate) async fn index(
State(app_state): State<AppState>,
AuthenticatedUser(user): AuthenticatedUser,
Query(params): Query<QueryParams>,
request: Request,
next: Next,
) -> impl IntoResponse {
let coaching_relationship =
coaching_relationship::find_by_id(app_state.db_conn_ref(), params.coaching_relationship_id)
.await
.unwrap_or_default();
match coaching_relationship {
Some(coaching_relationship) => {
if coaching_relationship.coach_id == user.id
|| coaching_relationship.coachee_id == user.id
{
// User has access to coaching relationship
next.run(request).await
} else {
// User does not have access to coaching relationship
(StatusCode::UNAUTHORIZED, "UNAUTHORIZED").into_response()
}
}
// coaching relationship with given ID not found
None => (StatusCode::NOT_FOUND, "NOT FOUND").into_response(),
}
}
6 changes: 6 additions & 0 deletions web/src/protect/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
pub(crate) mod actions;
pub(crate) mod agreements;
pub(crate) mod coaching_relationships;
pub(crate) mod coaching_sessions;
pub(crate) mod notes;
pub(crate) mod overarching_goals;
Loading

0 comments on commit a0428fd

Please sign in to comment.