Skip to main content

platform_http/
auth.rs

1use crate::{ApiErrorResponse, HttpRequestContext};
2use axum::extract::FromRequestParts;
3use axum::http::request::Parts;
4use platform_core::{ActorContext, AppError, ErrorCode};
5
6pub const CONSOLE_ADMIN_SCOPE: &str = "console.admin";
7
8#[derive(Debug, Clone)]
9pub struct OptionalActor(pub ActorContext);
10
11#[derive(Debug, Clone)]
12pub struct AuthenticatedActor(pub ActorContext);
13
14#[derive(Debug, Clone)]
15pub struct UserActor {
16    pub user_id: String,
17    pub scopes: Vec<String>,
18}
19
20#[derive(Debug, Clone)]
21pub struct ServiceActor {
22    pub service_id: String,
23    pub scopes: Vec<String>,
24}
25
26#[derive(Debug, Clone)]
27pub enum AdminActor {
28    Service {
29        service_id: String,
30        scopes: Vec<String>,
31    },
32    User {
33        user_id: String,
34        scopes: Vec<String>,
35    },
36    System,
37}
38
39impl<S> FromRequestParts<S> for OptionalActor
40where
41    S: Send + Sync,
42{
43    type Rejection = ApiErrorResponse;
44
45    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
46        let HttpRequestContext(ctx) = HttpRequestContext::from_request_parts(parts, state).await?;
47        Ok(Self(ctx.actor))
48    }
49}
50
51impl<S> FromRequestParts<S> for AuthenticatedActor
52where
53    S: Send + Sync,
54{
55    type Rejection = ApiErrorResponse;
56
57    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
58        let HttpRequestContext(ctx) = HttpRequestContext::from_request_parts(parts, state).await?;
59        match ctx.actor {
60            ActorContext::Anonymous => Err(ApiErrorResponse::with_context(
61                AppError::new(ErrorCode::Unauthorized, "Authentication is required"),
62                &ctx,
63            )),
64            actor => Ok(Self(actor)),
65        }
66    }
67}
68
69impl<S> FromRequestParts<S> for UserActor
70where
71    S: Send + Sync,
72{
73    type Rejection = ApiErrorResponse;
74
75    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
76        let HttpRequestContext(ctx) = HttpRequestContext::from_request_parts(parts, state).await?;
77        match ctx.actor {
78            ActorContext::Anonymous => Err(ApiErrorResponse::with_context(
79                AppError::new(ErrorCode::Unauthorized, "Authentication is required"),
80                &ctx,
81            )),
82            ActorContext::User { user_id, scopes } => Ok(Self { user_id, scopes }),
83            ActorContext::Service { .. } | ActorContext::System => {
84                Err(ApiErrorResponse::with_context(
85                    AppError::new(ErrorCode::Forbidden, "User authentication is required"),
86                    &ctx,
87                ))
88            }
89        }
90    }
91}
92
93impl<S> FromRequestParts<S> for ServiceActor
94where
95    S: Send + Sync,
96{
97    type Rejection = ApiErrorResponse;
98
99    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
100        let HttpRequestContext(ctx) = HttpRequestContext::from_request_parts(parts, state).await?;
101        match ctx.actor {
102            ActorContext::Anonymous => Err(ApiErrorResponse::with_context(
103                AppError::new(ErrorCode::Unauthorized, "Authentication is required"),
104                &ctx,
105            )),
106            ActorContext::Service { service_id, scopes } => Ok(Self { service_id, scopes }),
107            ActorContext::User { .. } | ActorContext::System => {
108                Err(ApiErrorResponse::with_context(
109                    AppError::new(ErrorCode::Forbidden, "Service authentication is required"),
110                    &ctx,
111                ))
112            }
113        }
114    }
115}
116
117impl<S> FromRequestParts<S> for AdminActor
118where
119    S: Send + Sync,
120{
121    type Rejection = ApiErrorResponse;
122
123    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
124        let HttpRequestContext(ctx) = HttpRequestContext::from_request_parts(parts, state).await?;
125        match ctx.actor {
126            ActorContext::Anonymous => Err(ApiErrorResponse::with_context(
127                AppError::new(ErrorCode::Unauthorized, "Authentication is required"),
128                &ctx,
129            )),
130            ActorContext::Service { service_id, scopes } => {
131                Ok(Self::Service { service_id, scopes })
132            }
133            ActorContext::System => Ok(Self::System),
134            ActorContext::User { user_id, scopes }
135                if scopes.iter().any(|scope| scope == CONSOLE_ADMIN_SCOPE) =>
136            {
137                Ok(Self::User { user_id, scopes })
138            }
139            ActorContext::User { .. } => Err(ApiErrorResponse::with_context(
140                AppError::new(
141                    ErrorCode::Forbidden,
142                    format!("missing console admin scope: {CONSOLE_ADMIN_SCOPE}"),
143                ),
144                &ctx,
145            )),
146        }
147    }
148}