scufflecloud_core/models/
sessions.rs

1use base64::Engine;
2use diesel::Selectable;
3use diesel::prelude::{AsChangeset, Associations, Identifiable, Insertable, Queryable};
4
5use super::impl_enum;
6use crate::cedar::CedarEntity;
7use crate::chrono_ext::ChronoDateTimeExt;
8use crate::id::{Id, PrefixedId};
9use crate::models::users::{User, UserId};
10
11impl_enum!(DeviceAlgorithm, crate::schema::sql_types::DeviceAlgorithm, {
12    RsaOaepSha256 => b"RSA_OAEP_SHA256",
13});
14
15impl From<DeviceAlgorithm> for pb::scufflecloud::core::v1::DeviceAlgorithm {
16    fn from(value: DeviceAlgorithm) -> Self {
17        match value {
18            DeviceAlgorithm::RsaOaepSha256 => pb::scufflecloud::core::v1::DeviceAlgorithm::RsaOaepSha256,
19        }
20    }
21}
22
23impl From<pb::scufflecloud::core::v1::DeviceAlgorithm> for DeviceAlgorithm {
24    fn from(value: pb::scufflecloud::core::v1::DeviceAlgorithm) -> Self {
25        match value {
26            pb::scufflecloud::core::v1::DeviceAlgorithm::RsaOaepSha256 => DeviceAlgorithm::RsaOaepSha256,
27        }
28    }
29}
30
31pub(crate) type UserSessionRequestId = Id<UserSessionRequest>;
32
33#[derive(Debug, Queryable, Selectable, Insertable, Identifiable, AsChangeset, serde::Serialize)]
34#[diesel(table_name = crate::schema::user_session_requests)]
35#[diesel(check_for_backend(diesel::pg::Pg))]
36pub struct UserSessionRequest {
37    pub id: UserSessionRequestId,
38    pub device_name: String,
39    pub device_ip: ipnetwork::IpNetwork,
40    pub code: String,
41    pub approved_by: Option<UserId>,
42    pub expires_at: chrono::DateTime<chrono::Utc>,
43}
44
45impl PrefixedId for UserSessionRequest {
46    const PREFIX: &'static str = "sr";
47}
48
49impl<G> CedarEntity<G> for UserSessionRequest {
50    const ENTITY_TYPE: &'static str = "UserSessionRequest";
51
52    fn entity_id(&self) -> cedar_policy::EntityId {
53        cedar_policy::EntityId::new(self.id.to_string_unprefixed())
54    }
55}
56
57impl From<UserSessionRequest> for pb::scufflecloud::core::v1::UserSessionRequest {
58    fn from(value: UserSessionRequest) -> Self {
59        pb::scufflecloud::core::v1::UserSessionRequest {
60            id: value.id.to_string(),
61            name: value.device_name,
62            ip: value.device_ip.to_string(),
63            approved_by: value.approved_by.map(|id| id.to_string()),
64            expires_at: Some(value.expires_at.to_prost_timestamp_utc()),
65        }
66    }
67}
68
69pub(crate) type MagicLinkUserSessionRequestId = Id<MagicLinkUserSessionRequest>;
70
71#[derive(Debug, Queryable, Selectable, Insertable, Identifiable, AsChangeset, serde::Serialize)]
72#[diesel(table_name = crate::schema::magic_link_user_session_requests)]
73#[diesel(check_for_backend(diesel::pg::Pg))]
74pub struct MagicLinkUserSessionRequest {
75    pub id: MagicLinkUserSessionRequestId,
76    pub user_id: UserId,
77    pub code: Vec<u8>,
78    pub expires_at: chrono::DateTime<chrono::Utc>,
79}
80
81impl PrefixedId for MagicLinkUserSessionRequest {
82    const PREFIX: &'static str = "ml";
83}
84
85impl<G> CedarEntity<G> for MagicLinkUserSessionRequest {
86    const ENTITY_TYPE: &'static str = "MagicLinkUserSessionRequest";
87
88    fn entity_id(&self) -> cedar_policy::EntityId {
89        cedar_policy::EntityId::new(self.id.to_string_unprefixed())
90    }
91}
92
93pub(crate) type UserSessionTokenId = Id<UserSessionToken>;
94
95/// Does not represent a real database table as it is always part of a [`UserSession`].
96#[derive(Debug, serde::Serialize)]
97pub struct UserSessionToken {
98    pub id: UserSessionTokenId,
99    pub token: Vec<u8>,
100    pub expires_at: chrono::DateTime<chrono::Utc>,
101}
102
103impl PrefixedId for UserSessionToken {
104    const PREFIX: &'static str = "st";
105}
106
107impl<G> CedarEntity<G> for UserSessionToken {
108    const ENTITY_TYPE: &'static str = "UserSessionToken";
109
110    fn entity_id(&self) -> cedar_policy::EntityId {
111        cedar_policy::EntityId::new(self.id.to_string_unprefixed())
112    }
113}
114
115#[derive(Queryable, Selectable, Insertable, Identifiable, AsChangeset, Associations, Debug, Clone, serde::Serialize)]
116#[diesel(table_name = crate::schema::user_sessions)]
117#[diesel(primary_key(user_id, device_fingerprint))]
118#[diesel(belongs_to(User))]
119#[diesel(check_for_backend(diesel::pg::Pg))]
120pub struct UserSession {
121    pub user_id: UserId,
122    pub device_fingerprint: Vec<u8>,
123    pub device_algorithm: DeviceAlgorithm,
124    pub device_pk_data: Vec<u8>,
125    pub last_used_at: chrono::DateTime<chrono::Utc>,
126    pub last_ip: ipnetwork::IpNetwork,
127    pub token_id: Option<UserSessionTokenId>,
128    pub token: Option<Vec<u8>>,
129    pub token_expires_at: Option<chrono::DateTime<chrono::Utc>>,
130    pub expires_at: chrono::DateTime<chrono::Utc>,
131    pub mfa_pending: bool,
132}
133
134impl<G> CedarEntity<G> for UserSession {
135    const ENTITY_TYPE: &'static str = "UserSession";
136
137    fn entity_id(&self) -> cedar_policy::EntityId {
138        let user_id = (*self.user_id).to_string();
139        let fingerprint = base64::prelude::BASE64_STANDARD.encode(&self.device_fingerprint);
140        cedar_policy::EntityId::new(format!("{user_id}:{fingerprint}"))
141    }
142}
143
144impl From<UserSession> for pb::scufflecloud::core::v1::UserSession {
145    fn from(value: UserSession) -> Self {
146        pb::scufflecloud::core::v1::UserSession {
147            user_id: value.user_id.to_string(),
148            device_fingerprint: value.device_fingerprint,
149            last_used_at: Some(value.last_used_at.to_prost_timestamp_utc()),
150            last_ip: value.last_ip.to_string(),
151            token_id: value.token_id.map(|id| id.to_string()),
152            token_expires_at: value.token_expires_at.map(|t| t.to_prost_timestamp_utc()),
153            expires_at: Some(value.expires_at.to_prost_timestamp_utc()),
154            mfa_pending: value.mfa_pending,
155        }
156    }
157}
158
159pub(crate) type EmailRegistrationRequestId = Id<EmailRegistrationRequest>;
160
161#[derive(Queryable, Selectable, Insertable, Identifiable, AsChangeset, Associations, Debug, serde::Serialize)]
162#[diesel(table_name = crate::schema::email_registration_requests)]
163#[diesel(belongs_to(User))]
164#[diesel(check_for_backend(diesel::pg::Pg))]
165pub struct EmailRegistrationRequest {
166    pub id: EmailRegistrationRequestId,
167    pub user_id: Option<UserId>,
168    pub email: String,
169    pub code: Vec<u8>,
170    pub expires_at: chrono::DateTime<chrono::Utc>,
171}
172
173impl PrefixedId for EmailRegistrationRequest {
174    const PREFIX: &'static str = "er";
175}
176
177impl<G> CedarEntity<G> for EmailRegistrationRequest {
178    const ENTITY_TYPE: &'static str = "EmailRegistrationRequest";
179
180    fn entity_id(&self) -> cedar_policy::EntityId {
181        cedar_policy::EntityId::new(self.id.to_string_unprefixed())
182    }
183}