Skip to content

Commit 8cde9fe

Browse files
committed
Add personal session data models to admin API
1 parent 2eea1a3 commit 8cde9fe

File tree

1 file changed

+184
-1
lines changed

1 file changed

+184
-1
lines changed

crates/handlers/src/admin/model.rs

Lines changed: 184 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@
77
use std::net::IpAddr;
88

99
use chrono::{DateTime, Utc};
10-
use mas_data_model::Device;
10+
use mas_data_model::{
11+
Device,
12+
personal::{
13+
PersonalAccessToken as DataModelPersonalAccessToken,
14+
session::{PersonalSession as DataModelPersonalSession, PersonalSessionOwner},
15+
},
16+
};
1117
use schemars::JsonSchema;
1218
use serde::Serialize;
19+
use thiserror::Error;
1320
use ulid::Ulid;
1421
use url::Url;
1522

@@ -771,3 +778,179 @@ impl UpstreamOAuthProvider {
771778
]
772779
}
773780
}
781+
782+
/// An error that shouldn't happen in practice, but suggests database
783+
/// inconsistency.
784+
#[derive(Debug, Error)]
785+
#[error(
786+
"personal session {session_id} in inconsistent state: not revoked but no valid access token"
787+
)]
788+
pub struct InconsistentPersonalSession {
789+
pub session_id: Ulid,
790+
}
791+
792+
// Note: we don't expose a separate concept of personal access tokens to the
793+
// admin API; we merge the relevant attributes into the personal session.
794+
/// A personal session (session using personal access tokens)
795+
#[derive(Serialize, JsonSchema)]
796+
pub struct PersonalSession {
797+
#[serde(skip)]
798+
id: Ulid,
799+
800+
/// When the session was created
801+
created_at: DateTime<Utc>,
802+
803+
/// When the session was revoked, if applicable
804+
revoked_at: Option<DateTime<Utc>>,
805+
806+
/// The ID of the user who owns this session (if user-owned)
807+
#[schemars(with = "super::schema::Ulid")]
808+
owner_user_id: Option<Ulid>,
809+
810+
/// The ID of the `OAuth2` client that owns this session (if client-owned)
811+
#[schemars(with = "super::schema::Ulid")]
812+
owner_client_id: Option<Ulid>,
813+
814+
/// The ID of the user that the session acts on behalf of
815+
#[schemars(with = "super::schema::Ulid")]
816+
actor_user_id: Ulid,
817+
818+
/// Human-readable name for the session
819+
human_name: String,
820+
821+
/// `OAuth2` scopes for this session
822+
scope: String,
823+
824+
/// When the session was last active
825+
last_active_at: Option<DateTime<Utc>>,
826+
827+
/// IP address of last activity
828+
last_active_ip: Option<IpAddr>,
829+
830+
/// When the current token for this session expires.
831+
/// The session will need to be regenerated, producing a new access token,
832+
/// after this time.
833+
/// None if the current token won't expire or if the session is revoked.
834+
expires_at: Option<DateTime<Utc>>,
835+
836+
/// The actual access token (only returned on creation)
837+
#[serde(skip_serializing_if = "Option::is_none")]
838+
access_token: Option<String>,
839+
}
840+
841+
impl
842+
TryFrom<(
843+
DataModelPersonalSession,
844+
Option<DataModelPersonalAccessToken>,
845+
)> for PersonalSession
846+
{
847+
type Error = InconsistentPersonalSession;
848+
849+
fn try_from(
850+
(session, token): (
851+
DataModelPersonalSession,
852+
Option<DataModelPersonalAccessToken>,
853+
),
854+
) -> Result<Self, InconsistentPersonalSession> {
855+
let expires_at = if let Some(token) = token {
856+
token.expires_at
857+
} else {
858+
if !session.is_revoked() {
859+
// No active token, but the session is not revoked.
860+
return Err(InconsistentPersonalSession {
861+
session_id: session.id,
862+
});
863+
}
864+
None
865+
};
866+
867+
let (owner_user_id, owner_client_id) = match session.owner {
868+
PersonalSessionOwner::User(id) => (Some(id), None),
869+
PersonalSessionOwner::OAuth2Client(id) => (None, Some(id)),
870+
};
871+
872+
Ok(Self {
873+
id: session.id,
874+
created_at: session.created_at,
875+
revoked_at: session.revoked_at(),
876+
owner_user_id,
877+
owner_client_id,
878+
actor_user_id: session.actor_user_id,
879+
human_name: session.human_name,
880+
scope: session.scope.to_string(),
881+
last_active_at: session.last_active_at,
882+
last_active_ip: session.last_active_ip,
883+
expires_at,
884+
// If relevant, the caller will populate using `with_token` afterwards.
885+
access_token: None,
886+
})
887+
}
888+
}
889+
890+
impl Resource for PersonalSession {
891+
const KIND: &'static str = "personal-session";
892+
const PATH: &'static str = "/api/admin/v1/personal-sessions";
893+
894+
fn id(&self) -> Ulid {
895+
self.id
896+
}
897+
}
898+
899+
impl PersonalSession {
900+
/// Sample personal sessions for documentation/testing
901+
pub fn samples() -> [Self; 3] {
902+
[
903+
Self {
904+
id: Ulid::from_string("01FSHN9AG0AJ6AC5HQ9X6H4RP4").unwrap(),
905+
created_at: DateTime::from_timestamp(1_642_338_000, 0).unwrap(), /* 2022-01-16T14:
906+
* 40:00Z */
907+
revoked_at: None,
908+
owner_user_id: Some(Ulid::from_string("01FSHN9AG0MZAA6S4AF7CTV32E").unwrap()),
909+
owner_client_id: None,
910+
actor_user_id: Ulid::from_string("01FSHN9AG0MZAA6S4AF7CTV32E").unwrap(),
911+
human_name: "Alice's Development Token".to_owned(),
912+
scope: "openid urn:matrix:org.matrix.msc2967.client:api:*".to_owned(),
913+
last_active_at: Some(DateTime::from_timestamp(1_642_347_000, 0).unwrap()), /* 2022-01-16T17:10:00Z */
914+
last_active_ip: Some("192.168.1.100".parse().unwrap()),
915+
expires_at: None,
916+
access_token: None,
917+
},
918+
Self {
919+
id: Ulid::from_string("01FSHN9AG0BJ6AC5HQ9X6H4RP5").unwrap(),
920+
created_at: DateTime::from_timestamp(1_642_338_060, 0).unwrap(), /* 2022-01-16T14:
921+
* 41:00Z */
922+
revoked_at: Some(DateTime::from_timestamp(1_642_350_000, 0).unwrap()), /* 2022-01-16T18:00:00Z */
923+
owner_user_id: Some(Ulid::from_string("01FSHN9AG0NZAA6S4AF7CTV32F").unwrap()),
924+
owner_client_id: None,
925+
actor_user_id: Ulid::from_string("01FSHN9AG0NZAA6S4AF7CTV32F").unwrap(),
926+
human_name: "Bob's Mobile App".to_owned(),
927+
scope: "openid".to_owned(),
928+
last_active_at: Some(DateTime::from_timestamp(1_642_349_000, 0).unwrap()), /* 2022-01-16T17:43:20Z */
929+
last_active_ip: Some("10.0.0.50".parse().unwrap()),
930+
expires_at: None,
931+
access_token: None,
932+
},
933+
Self {
934+
id: Ulid::from_string("01FSHN9AG0CJ6AC5HQ9X6H4RP6").unwrap(),
935+
created_at: DateTime::from_timestamp(1_642_338_120, 0).unwrap(), /* 2022-01-16T14:
936+
* 42:00Z */
937+
revoked_at: None,
938+
owner_user_id: None,
939+
owner_client_id: Some(Ulid::from_string("01FSHN9AG0DJ6AC5HQ9X6H4RP7").unwrap()),
940+
actor_user_id: Ulid::from_string("01FSHN9AG0MZAA6S4AF7CTV32E").unwrap(),
941+
human_name: "CI/CD Pipeline Token".to_owned(),
942+
scope: "openid urn:mas:admin".to_owned(),
943+
last_active_at: Some(DateTime::from_timestamp(1_642_348_000, 0).unwrap()), /* 2022-01-16T17:26:40Z */
944+
last_active_ip: Some("203.0.113.10".parse().unwrap()),
945+
expires_at: Some(DateTime::from_timestamp(1_642_999_000, 0).unwrap()),
946+
access_token: None,
947+
},
948+
]
949+
}
950+
951+
/// Add the actual token value (for use in creation responses)
952+
pub fn with_token(mut self, access_token: String) -> Self {
953+
self.access_token = Some(access_token);
954+
self
955+
}
956+
}

0 commit comments

Comments
 (0)