1use std::str::FromStr;
2use std::sync::{Arc, OnceLock};
3
4use cedar_policy::{Decision, Entities, EntityId, PolicySet, Schema};
5use core_cedar::{CedarEntity, CedarIdentifiable, EntityTypeName, entity_type_name};
6use core_db_types::models::UserSession;
7use ext_traits::ResultExt;
8use tonic_types::{ErrorDetails, StatusExt};
9
10fn static_policies() -> &'static PolicySet {
11 const STATIC_POLICIES_STR: &str = include_str!("../static_policies.cedar");
12 static STATIC_POLICIES: OnceLock<PolicySet> = OnceLock::new();
13
14 STATIC_POLICIES.get_or_init(|| PolicySet::from_str(STATIC_POLICIES_STR).expect("failed to parse static policies"))
15}
16
17fn static_policies_schema() -> &'static Schema {
18 const STATIC_POLICIES_SCHEMA_STR: &str = include_str!("../static_policies.cedarschema");
19 static STATIC_POLICIES_SCHEMA: OnceLock<Schema> = OnceLock::new();
20
21 STATIC_POLICIES_SCHEMA
22 .get_or_init(|| Schema::from_str(STATIC_POLICIES_SCHEMA_STR).expect("failed to parse static policies schema"))
23}
24
25#[derive(Debug, serde::Serialize)]
26pub struct Unauthenticated;
27
28impl CedarIdentifiable for Unauthenticated {
29 const ENTITY_TYPE: EntityTypeName = entity_type_name!("Unauthenticated");
30
31 fn entity_id(&self) -> EntityId {
32 EntityId::new("unauthenticated")
33 }
34}
35
36impl CedarEntity for Unauthenticated {}
37
38#[derive(Debug, Clone, Copy, derive_more::Display, serde::Serialize)]
39#[serde(untagged)]
40pub enum Action {
41 #[display("login_with_email_password")]
43 LoginWithEmailPassword,
44 #[display("request_magic_link")]
45 RequestMagicLink,
46 #[display("login_with_magic_link")]
47 LoginWithMagicLink,
48 #[display("login_with_google")]
50 LoginWithGoogle,
51 #[display("login_with_webauthn")]
52 LoginWithWebauthn,
53 #[display("get_user")]
54 GetUser,
55 #[display("update_user")]
56 UpdateUser,
57 #[display("list_user_emails")]
58 ListUserEmails,
59 #[display("create_user_email")]
60 CreateUserEmail,
61 #[display("delete_user_email")]
62 DeleteUserEmail,
63
64 #[display("create_webauthn_credential")]
65 CreateWebauthnCredential,
66 #[display("complete_create_webauthn_credential")]
67 CompleteCreateWebauthnCredential,
68 #[display("create_webauthn_challenge")]
69 CreateWebauthnChallenge,
70 #[display("update_webauthn_credential")]
71 UpdateWebauthnCredential,
72 #[display("delete_webauthn_credential")]
73 DeleteWebauthnCredential,
74 #[display("list_webauthn_credentials")]
75 ListWebauthnCredentials,
76
77 #[display("create_totp_credential")]
78 CreateTotpCredential,
79 #[display("complete_create_totp_credential")]
80 CompleteCreateTotpCredential,
81 #[display("update_totp_credential")]
82 UpdateTotpCredential,
83 #[display("delete_totp_credential")]
84 DeleteTotpCredential,
85 #[display("list_totp_credentials")]
86 ListTotpCredentials,
87
88 #[display("regenerate_recovery_codes")]
89 RegenerateRecoveryCodes,
90 #[display("delete_user")]
91 DeleteUser,
92
93 #[display("create_user_session_request")]
95 CreateUserSessionRequest,
96 #[display("get_user_session_request")]
97 GetUserSessionRequest,
98 #[display("approve_user_session_request")]
99 ApproveUserSessionRequest,
100 #[display("complete_user_session_request")]
101 CompleteUserSessionRequest,
102
103 #[display("validate_mfa_for_user_session")]
105 ValidateMfaForUserSession,
106 #[display("refresh_user_session")]
107 RefreshUserSession,
108 #[display("invalidate_user_session")]
109 InvalidateUserSession,
110
111 #[display("create_organization")]
113 CreateOrganization,
114 #[display("get_organization")]
115 GetOrganization,
116 #[display("update_organization")]
117 UpdateOrganization,
118 #[display("list_organization_members")]
119 ListOrganizationMembers,
120 #[display("list_organizations_by_user")]
121 ListOrganizationsByUser,
122 #[display("create_project")]
123 CreateProject,
124 #[display("list_projects")]
125 ListProjects,
126
127 #[display("create_organization_invitation")]
129 CreateOrganizationInvitation,
130 #[display("list_organization_invitations_by_user")]
131 ListOrganizationInvitationsByUser,
132 #[display("list_organization_invitations_by_organization")]
133 ListOrganizationInvitationsByOrganization,
134 #[display("get_organization_invitation")]
135 GetOrganizationInvitation,
136 #[display("accept_organization_invitation")]
137 AcceptOrganizationInvitation,
138 #[display("decline_organization_invitation")]
139 DeclineOrganizationInvitation,
140}
141
142impl CedarIdentifiable for Action {
143 const ENTITY_TYPE: EntityTypeName = entity_type_name!("Action");
144
145 fn entity_id(&self) -> EntityId {
146 EntityId::new(self.to_string())
147 }
148}
149
150impl CedarEntity for Action {}
151
152#[derive(serde::Serialize)]
154pub struct CoreApplication;
155
156impl CedarIdentifiable for CoreApplication {
157 const ENTITY_TYPE: EntityTypeName = entity_type_name!("Application");
158
159 fn entity_id(&self) -> EntityId {
160 EntityId::new("core")
161 }
162}
163
164impl CedarEntity for CoreApplication {}
165
166pub(crate) async fn is_authorized<G: core_traits::Global>(
167 global: &Arc<G>,
168 user_session: Option<&UserSession>,
169 principal: &impl CedarEntity,
170 action: &impl CedarEntity,
171 resource: &impl CedarEntity,
172) -> Result<(), tonic::Status> {
173 let mut context = serde_json::Map::new();
174 if let Some(session) = user_session {
175 context.insert(
176 "user_session_mfa_pending".to_string(),
177 serde_json::Value::Bool(session.mfa_pending),
178 );
179 }
180
181 let schema = static_policies_schema();
182
183 let a_euid: cedar_policy::EntityUid = action.entity_uid().into();
184
185 let context = cedar_policy::Context::from_json_value(serde_json::Value::Object(context), Some((schema, &a_euid)))
186 .into_tonic_internal_err("failed to create cedar context")?;
187
188 let r = cedar_policy::Request::new(
189 principal.entity_uid().into(),
190 a_euid,
191 resource.entity_uid().into(),
192 context,
193 Some(schema),
194 )
195 .into_tonic_internal_err("failed to validate cedar request")?;
196
197 let entities = vec![
198 principal.to_entity(global.as_ref(), Some(schema)).await?,
199 action.to_entity(global.as_ref(), Some(schema)).await?,
200 resource.to_entity(global.as_ref(), Some(schema)).await?,
201 ];
202
203 let entities = Entities::empty()
204 .add_entities(entities, Some(schema))
205 .into_tonic_internal_err("failed to create cedar entities")?;
206
207 match cedar_policy::Authorizer::new()
208 .is_authorized(&r, static_policies(), &entities)
209 .decision()
210 {
211 Decision::Allow => Ok(()),
212 Decision::Deny => {
213 tracing::warn!(request = ?r, "authorization denied");
214 let message = format!(
215 "{} is not authorized to perform {} on {}",
216 r.principal().expect("is always known"),
217 r.action().expect("is always known"),
218 r.resource().expect("is always known")
219 );
220
221 Err(tonic::Status::with_error_details(
222 tonic::Code::PermissionDenied,
223 "you are not authorized to perform this action",
224 ErrorDetails::with_debug_info(vec![], message),
225 ))
226 }
227 }
228}