scufflecloud_core/
cedar.rs

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    /// Login to an existing account with email and password.
42    #[display("login_with_email_password")]
43    LoginWithEmailPassword,
44    #[display("request_magic_link")]
45    RequestMagicLink,
46    #[display("login_with_magic_link")]
47    LoginWithMagicLink,
48    /// Login to an existing account with Google OAuth2.
49    #[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    // UserSessionRequest related
94    #[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    // UserSession related
104    #[display("validate_mfa_for_user_session")]
105    ValidateMfaForUserSession,
106    #[display("refresh_user_session")]
107    RefreshUserSession,
108    #[display("invalidate_user_session")]
109    InvalidateUserSession,
110
111    // Organization related
112    #[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    // OrganizationInvitation related
128    #[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/// A general resource that is used whenever there is no specific resource for a request. (e.g. user login)
153#[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}