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 core_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("delete_webauthn_credential")]
71 DeleteWebauthnCredential,
72 #[display("list_webauthn_credentials")]
73 ListWebauthnCredentials,
74
75 #[display("create_totp_credential")]
76 CreateTotpCredential,
77 #[display("complete_create_totp_credential")]
78 CompleteCreateTotpCredential,
79 #[display("delete_totp_credential")]
80 DeleteTotpCredential,
81 #[display("list_totp_credentials")]
82 ListTotpCredentials,
83
84 #[display("regenerate_recovery_codes")]
85 RegenerateRecoveryCodes,
86 #[display("delete_user")]
87 DeleteUser,
88
89 #[display("create_user_session_request")]
91 CreateUserSessionRequest,
92 #[display("get_user_session_request")]
93 GetUserSessionRequest,
94 #[display("approve_user_session_request")]
95 ApproveUserSessionRequest,
96 #[display("complete_user_session_request")]
97 CompleteUserSessionRequest,
98
99 #[display("validate_mfa_for_user_session")]
101 ValidateMfaForUserSession,
102 #[display("refresh_user_session")]
103 RefreshUserSession,
104 #[display("invalidate_user_session")]
105 InvalidateUserSession,
106
107 #[display("create_organization")]
109 CreateOrganization,
110 #[display("get_organization")]
111 GetOrganization,
112 #[display("update_organization")]
113 UpdateOrganization,
114 #[display("list_organization_members")]
115 ListOrganizationMembers,
116 #[display("list_organizations_by_user")]
117 ListOrganizationsByUser,
118 #[display("create_project")]
119 CreateProject,
120 #[display("list_projects")]
121 ListProjects,
122
123 #[display("create_organization_invitation")]
125 CreateOrganizationInvitation,
126 #[display("list_organization_invitations_by_user")]
127 ListOrganizationInvitationsByUser,
128 #[display("list_organization_invitations_by_organization")]
129 ListOrganizationInvitationsByOrganization,
130 #[display("get_organization_invitation")]
131 GetOrganizationInvitation,
132 #[display("accept_organization_invitation")]
133 AcceptOrganizationInvitation,
134 #[display("decline_organization_invitation")]
135 DeclineOrganizationInvitation,
136}
137
138impl CedarIdentifiable for Action {
139 const ENTITY_TYPE: EntityTypeName = entity_type_name!("Action");
140
141 fn entity_id(&self) -> EntityId {
142 EntityId::new(self.to_string())
143 }
144}
145
146impl CedarEntity for Action {}
147
148#[derive(serde::Serialize)]
150pub struct CoreApplication;
151
152impl CedarIdentifiable for CoreApplication {
153 const ENTITY_TYPE: EntityTypeName = entity_type_name!("Application");
154
155 fn entity_id(&self) -> EntityId {
156 EntityId::new("core")
157 }
158}
159
160impl CedarEntity for CoreApplication {}
161
162pub(crate) async fn is_authorized<G: core_traits::Global>(
163 global: &Arc<G>,
164 user_session: Option<&UserSession>,
165 principal: &impl CedarEntity,
166 action: &impl CedarEntity,
167 resource: &impl CedarEntity,
168) -> Result<(), tonic::Status> {
169 let mut context = serde_json::Map::new();
170 if let Some(session) = user_session {
171 context.insert(
172 "user_session_mfa_pending".to_string(),
173 serde_json::Value::Bool(session.mfa_pending),
174 );
175 }
176
177 let schema = static_policies_schema();
178
179 let a_euid: cedar_policy::EntityUid = action.entity_uid().into();
180
181 let context = cedar_policy::Context::from_json_value(serde_json::Value::Object(context), Some((schema, &a_euid)))
182 .into_tonic_internal_err("failed to create cedar context")?;
183
184 let r = cedar_policy::Request::new(
185 principal.entity_uid().into(),
186 a_euid,
187 resource.entity_uid().into(),
188 context,
189 Some(schema),
190 )
191 .into_tonic_internal_err("failed to validate cedar request")?;
192
193 let entities = vec![
194 principal.to_entity(global.as_ref(), Some(schema)).await?,
195 action.to_entity(global.as_ref(), Some(schema)).await?,
196 resource.to_entity(global.as_ref(), Some(schema)).await?,
197 ];
198
199 let entities = Entities::empty()
200 .add_entities(entities, Some(schema))
201 .into_tonic_internal_err("failed to create cedar entities")?;
202
203 match cedar_policy::Authorizer::new()
204 .is_authorized(&r, static_policies(), &entities)
205 .decision()
206 {
207 Decision::Allow => Ok(()),
208 Decision::Deny => {
209 tracing::warn!(request = ?r, "authorization denied");
210 let message = format!(
211 "{} is not authorized to perform {} on {}",
212 r.principal().expect("is always known"),
213 r.action().expect("is always known"),
214 r.resource().expect("is always known")
215 );
216
217 Err(tonic::Status::with_error_details(
218 tonic::Code::PermissionDenied,
219 "you are not authorized to perform this action",
220 ErrorDetails::with_debug_info(vec![], message),
221 ))
222 }
223 }
224}