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}