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 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    /// 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("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    // UserSessionRequest related
90    #[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    // UserSession related
100    #[display("validate_mfa_for_user_session")]
101    ValidateMfaForUserSession,
102    #[display("refresh_user_session")]
103    RefreshUserSession,
104    #[display("invalidate_user_session")]
105    InvalidateUserSession,
106
107    // Organization related
108    #[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    // OrganizationInvitation related
124    #[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/// A general resource that is used whenever there is no specific resource for a request. (e.g. user login)
149#[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}