1use std::collections::HashSet;
2use std::str::FromStr;
3use std::sync::{Arc, OnceLock};
4
5use cedar_policy::{Decision, Entities, Entity, EntityId, EntityTypeName, EntityUid, PolicySet};
6use tonic_types::{ErrorDetails, StatusExt};
7
8use crate::CoreConfig;
9use crate::id::{Id, PrefixedId};
10use crate::models::UserSession;
11use crate::std_ext::ResultExt;
12
13fn static_policies() -> &'static PolicySet {
14 const STATIC_POLICIES_STR: &str = include_str!("../static_policies.cedar");
15 static STATIC_POLICIES: OnceLock<PolicySet> = OnceLock::new();
16
17 STATIC_POLICIES.get_or_init(|| PolicySet::from_str(STATIC_POLICIES_STR).expect("failed to parse static policies"))
18}
19
20fn uid_to_json(uid: EntityUid) -> serde_json::Value {
21 serde_json::json!({
22 "type": uid.type_name().to_string(),
23 "id": uid.id().unescaped(),
24 })
25}
26
27pub(crate) trait CedarEntity<G>: serde::Serialize {
28 const ENTITY_TYPE: &'static str;
32
33 fn entity_id(&self) -> EntityId;
34
35 fn entity_uid(&self) -> EntityUid {
36 let name = EntityTypeName::from_str(Self::ENTITY_TYPE).expect("invalid entity type name");
37 EntityUid::from_type_name_and_id(name, self.entity_id())
38 }
39
40 async fn attributes(&self, global: &Arc<G>) -> Result<serde_json::value::Map<String, serde_json::Value>, tonic::Status> {
41 let _global = global;
42 if let serde_json::Value::Object(object) =
43 serde_json::to_value(self).into_tonic_internal_err("failed to serialize cedar entity")?
44 {
45 let object = object.into_iter().filter(|(_, v)| !v.is_null()).collect();
47 Ok(object)
48 } else {
49 Ok(serde_json::value::Map::new())
50 }
51 }
52
53 async fn parents(&self, global: &Arc<G>) -> Result<HashSet<EntityUid>, tonic::Status> {
54 let _global = global;
55 Ok(HashSet::new())
56 }
57
58 async fn to_entity(&self, global: &Arc<G>) -> Result<Entity, tonic::Status> {
59 let mut value = serde_json::value::Map::new();
60 value.insert("uid".to_string(), uid_to_json(self.entity_uid()));
61 value.insert("attrs".to_string(), serde_json::Value::Object(self.attributes(global).await?));
62 value.insert(
63 "parents".to_string(),
64 serde_json::Value::Array(self.parents(global).await?.into_iter().map(uid_to_json).collect()),
65 );
66
67 let entity = Entity::from_json_value(serde_json::Value::Object(value), None)
68 .into_tonic_internal_err("failed to create cedar entity")?;
69 Ok(entity)
70 }
71}
72
73impl<G, T: CedarEntity<G>> CedarEntity<G> for &T {
74 const ENTITY_TYPE: &'static str = T::ENTITY_TYPE;
75
76 fn entity_id(&self) -> EntityId {
77 T::entity_id(self)
78 }
79
80 fn entity_uid(&self) -> EntityUid {
81 T::entity_uid(self)
82 }
83}
84
85impl<G, T: PrefixedId + CedarEntity<G>> CedarEntity<G> for Id<T> {
86 const ENTITY_TYPE: &'static str = T::ENTITY_TYPE;
87
88 fn entity_id(&self) -> EntityId {
89 EntityId::new(self.to_string_unprefixed())
90 }
91}
92
93#[derive(Debug, serde::Serialize)]
94pub struct Unauthenticated;
95
96impl<G> CedarEntity<G> for Unauthenticated {
97 const ENTITY_TYPE: &'static str = "Unauthenticated";
98
99 fn entity_id(&self) -> EntityId {
100 EntityId::new("unauthenticated")
101 }
102}
103
104#[derive(Debug, Clone, Copy, derive_more::Display, serde::Serialize)]
105pub enum Action {
106 #[display("register_with_email")]
109 RegisterWithEmail,
110 #[display("complete_register_with_email")]
111 CompleteRegisterWithEmail,
112 #[display("get_login_with_email_options")]
113 GetLoginWithEmailOptions,
114 #[display("login_with_email_password")]
116 LoginWithEmailPassword,
117 #[display("request_magic_link")]
118 RequestMagicLink,
119 #[display("login_with_magic_link")]
120 LoginWithMagicLink,
121 #[display("login_with_google")]
123 LoginWithGoogle,
124 #[display("login_with_webauthn")]
125 LoginWithWebauthn,
126 #[display("get_user")]
127 GetUser,
128 #[display("update_user")]
129 UpdateUser,
130 #[display("list_user_emails")]
131 ListUserEmails,
132 #[display("create_user_email")]
133 CreateUserEmail,
134 #[display("delete_user_email")]
135 DeleteUserEmail,
136
137 #[display("create_webauthn_credential")]
138 CreateWebauthnCredential,
139 #[display("complete_create_webauthn_credential")]
140 CompleteCreateWebauthnCredential,
141 #[display("create_webauthn_challenge")]
142 CreateWebauthnChallenge,
143 #[display("delete_webauthn_credential")]
144 DeleteWebauthnCredential,
145 #[display("list_webauthn_credentials")]
146 ListWebauthnCredentials,
147
148 #[display("create_totp_credential")]
149 CreateTotpCredential,
150 #[display("complete_create_totp_credential")]
151 CompleteCreateTotpCredential,
152 #[display("delete_totp_credential")]
153 DeleteTotpCredential,
154 #[display("list_totp_credentials")]
155 ListTotpCredentials,
156
157 #[display("regenerate_recovery_codes")]
158 RegenerateRecoveryCodes,
159 #[display("delete_user")]
160 DeleteUser,
161
162 #[display("create_user_session_request")]
164 CreateUserSessionRequest,
165 #[display("get_user_session_request")]
166 GetUserSessionRequest,
167 #[display("approve_user_session_request")]
168 ApproveUserSessionRequest,
169 #[display("complete_user_session_request")]
170 CompleteUserSessionRequest,
171
172 #[display("validate_mfa_for_user_session")]
174 ValidateMfaForUserSession,
175 #[display("refresh_user_session")]
176 RefreshUserSession,
177 #[display("invalidate_user_session")]
178 InvalidateUserSession,
179
180 #[display("create_organization")]
182 CreateOrganization,
183 #[display("get_organization")]
184 GetOrganization,
185 #[display("update_organization")]
186 UpdateOrganization,
187 #[display("list_organization_members")]
188 ListOrganizationMembers,
189 #[display("list_organizations_by_user")]
190 ListOrganizationsByUser,
191 #[display("create_project")]
192 CreateProject,
193 #[display("list_project")]
194 ListProjects,
195
196 #[display("create_organization_invitation")]
198 CreateOrganizationInvitation,
199 #[display("list_organization_invitations_by_user")]
200 ListOrganizationInvitationsByUser,
201 #[display("list_organization_invitations_by_organization")]
202 ListOrganizationInvitationsByOrganization,
203 #[display("get_organization_invitation")]
204 GetOrganizationInvitation,
205 #[display("accept_organization_invitation")]
206 AcceptOrganizationInvitation,
207 #[display("decline_organization_invitation")]
208 DeclineOrganizationInvitation,
209}
210
211impl<G> CedarEntity<G> for Action {
212 const ENTITY_TYPE: &'static str = "Action";
213
214 fn entity_id(&self) -> EntityId {
215 EntityId::new(self.to_string())
216 }
217}
218
219#[derive(serde::Serialize)]
221pub struct CoreApplication;
222
223impl<G> CedarEntity<G> for CoreApplication {
224 const ENTITY_TYPE: &'static str = "Application";
225
226 fn entity_id(&self) -> EntityId {
227 EntityId::new("core")
228 }
229}
230
231pub(crate) async fn is_authorized<G: CoreConfig>(
232 global: &Arc<G>,
233 user_session: Option<&UserSession>,
234 principal: impl CedarEntity<G>,
235 action: impl CedarEntity<G>,
236 resource: impl CedarEntity<G>,
237) -> Result<(), tonic::Status> {
238 let mut context = serde_json::Map::new();
239 if let Some(session) = user_session {
240 context.insert(
241 "user_session_mfa_pending".to_string(),
242 serde_json::Value::Bool(session.mfa_pending),
243 );
244 }
245
246 let context = cedar_policy::Context::from_json_value(serde_json::Value::Object(context), None)
247 .into_tonic_internal_err("failed to create cedar context")?;
248
249 let r = cedar_policy::Request::new(
250 principal.entity_uid(),
251 action.entity_uid(),
252 resource.entity_uid(),
253 context,
254 None,
255 )
256 .into_tonic_internal_err("failed to validate cedar request")?;
257
258 let entities = vec![
259 principal.to_entity(global).await?,
260 action.to_entity(global).await?,
261 resource.to_entity(global).await?,
262 ];
263
264 let entities = Entities::empty()
265 .add_entities(entities, None)
266 .into_tonic_internal_err("failed to create cedar entities")?;
267
268 match global.authorizer().is_authorized(&r, static_policies(), &entities).decision() {
269 Decision::Allow => Ok(()),
270 Decision::Deny => {
271 tracing::warn!(request = ?r, "authorization denied");
272 let message = format!(
273 "{} is not authorized to perform {} on {}",
274 r.principal().expect("is always known"),
275 r.action().expect("is always known"),
276 r.resource().expect("is always known")
277 );
278
279 Err(tonic::Status::with_error_details(
280 tonic::Code::PermissionDenied,
281 "you are not authorized to perform this action",
282 ErrorDetails::with_debug_info(vec![], message),
283 ))
284 }
285 }
286}