Skip to main content

edda_auth/
protocol.rs

1//! Requests and responses of the auth service, and what tokens say.
2
3use chrono::{DateTime, Utc};
4use edda_types::Edition;
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8use crate::{Nonce, PublicKey, Role, Signature, SignedCert, bytes};
9
10/// What a token grants: who is calling, in which campaign, as what.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
13pub struct Claims {
14    /// The device's (or service's) key.
15    pub key: PublicKey,
16    pub campaign: Uuid,
17    pub role: Principal,
18    /// "Vex's laptop", or the service's name. Used as the actor in logs.
19    pub name: String,
20    /// The rules the campaign plays by, so services can apply them.
21    #[serde(default)]
22    pub edition: Edition,
23    /// What the campaign may keep and do, as whoever runs the services set.
24    #[serde(default, skip_serializing_if = "CampaignSettings::is_default")]
25    pub settings: CampaignSettings,
26    pub expires_at: DateTime<Utc>,
27}
28
29/// What a campaign may keep and do, set by whoever runs the services
30/// through the admin API (a hosting's plans, say), and carried in its
31/// tokens so every service applies them. By default, the services' own.
32#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
33#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
34pub struct CampaignSettings {
35    /// Nothing may be changed, only looked at (until a bill is paid, say).
36    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
37    pub read_only: bool,
38    /// Limits of its own, in place of the services'.
39    #[serde(default, skip_serializing_if = "Quotas::is_default")]
40    pub quotas: Quotas,
41}
42
43impl CampaignSettings {
44    /// Whether these are the defaults: the services' own limits, and
45    /// changes allowed.
46    pub fn is_default(&self) -> bool {
47        *self == Self::default()
48    }
49}
50
51/// How much a campaign may keep: each one set replaces the services' limit
52/// of that name (see the hosting docs' limits).
53#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
54#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
55#[serde(default)]
56pub struct Quotas {
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub characters: Option<u32>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub monsters: Option<u32>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub rules_texts: Option<u32>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub combats: Option<u32>,
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub plans: Option<u32>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub devices: Option<u32>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub events_per_combat: Option<u32>,
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub connections_per_combat: Option<u32>,
73}
74
75impl Quotas {
76    /// Whether none is set.
77    pub fn is_default(&self) -> bool {
78        *self == Self::default()
79    }
80}
81
82/// Who a token is for: a DM device, a player's, or a service.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
84#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
85#[serde(rename_all = "snake_case")]
86pub enum Principal {
87    Dm,
88    Player,
89    /// Another service acting on its own behalf (e.g. the combat service
90    /// writing hit points back to character sheets).
91    Service,
92}
93
94impl From<Role> for Principal {
95    fn from(role: Role) -> Self {
96        match role {
97            Role::Dm => Principal::Dm,
98            Role::Player => Principal::Player,
99        }
100    }
101}
102
103impl Claims {
104    /// Whether the token is a DM device's.
105    pub fn is_dm(&self) -> bool {
106        self.role == Principal::Dm
107    }
108}
109
110/// `POST /login/challenge`
111#[derive(Debug, Clone, Serialize, Deserialize)]
112#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
113pub struct ChallengeRequest {
114    pub campaign: Uuid,
115    pub key: PublicKey,
116}
117
118/// `POST /login/challenge`: what to sign to log in, valid for a minute.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
121pub struct Challenge {
122    pub id: Uuid,
123    pub nonce: Nonce,
124    pub campaign: Uuid,
125    /// The auth service's own key, which the device should check against
126    /// the one it expects (so it isn't logging in somewhere else).
127    pub server: PublicKey,
128    pub expires_at: DateTime<Utc>,
129}
130
131/// What a device signs to answer a challenge. Covers everything that
132/// identifies this login, so the signature is useless anywhere else.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
135pub struct LoginProof {
136    pub challenge: Uuid,
137    pub nonce: Nonce,
138    pub campaign: Uuid,
139    pub server: PublicKey,
140}
141
142impl From<&Challenge> for LoginProof {
143    fn from(c: &Challenge) -> Self {
144        Self {
145            challenge: c.id,
146            nonce: c.nonce,
147            campaign: c.campaign,
148            server: c.server,
149        }
150    }
151}
152
153/// `POST /login`
154#[derive(Debug, Clone, Serialize, Deserialize)]
155#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
156pub struct LoginRequest {
157    pub challenge: Uuid,
158    pub signature: Signature,
159}
160
161/// `POST /login`: a token, and what it says.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
164pub struct Session {
165    /// Send as `Authorization: Bearer <token>`.
166    pub token: String,
167    pub claims: Claims,
168}
169
170/// `POST /admin/campaigns` (the admin API): a campaign to create.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
173pub struct NewCampaign {
174    pub name: String,
175    pub edition: Edition,
176}
177
178/// `POST /admin/campaigns`: the campaign, and the one-time code its DM
179/// sets up their first device with.
180#[derive(Debug, Clone, Serialize, Deserialize)]
181#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
182pub struct CreatedCampaign {
183    pub campaign: Campaign,
184    pub setup_code: String,
185}
186
187/// The auth service's part of a campaign, as exported and imported by the
188/// admin API: the campaign and its devices, with their certificates.
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
190#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
191pub struct AuthExport {
192    pub campaign: Campaign,
193    pub created_at: DateTime<Utc>,
194    pub devices: Vec<Device>,
195    #[serde(default, skip_serializing_if = "CampaignSettings::is_default")]
196    pub settings: CampaignSettings,
197}
198
199/// `POST /setup/lookup`, and the admin API's
200/// `POST /admin/campaigns/{id}/setup-code`
201#[derive(Debug, Clone, Serialize, Deserialize)]
202#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
203pub struct SetupCode {
204    pub code: String,
205}
206
207/// `POST /setup`: the first DM device of a campaign, with the one-time code
208/// printed when the campaign was created.
209#[derive(Debug, Clone, Serialize, Deserialize)]
210#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
211pub struct SetupRequest {
212    pub code: String,
213    /// Self-signed, role DM.
214    pub cert: SignedCert,
215}
216
217/// A campaign: its id, name and edition.
218#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
219#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
220pub struct Campaign {
221    pub id: Uuid,
222    pub name: String,
223    /// Chosen when the campaign is created.
224    #[serde(default)]
225    pub edition: Edition,
226}
227
228/// A device of a campaign: its certificate, and when it was added and
229/// revoked.
230#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
231#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
232pub struct Device {
233    pub cert: SignedCert,
234    pub added_at: DateTime<Utc>,
235    #[serde(default, skip_serializing_if = "Option::is_none")]
236    pub revoked_at: Option<DateTime<Utc>>,
237}
238
239/// `GET /public-key`: the key tokens are signed with.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
242pub struct ServerKey {
243    pub key: PublicKey,
244}
245
246/// `POST /pairings` (DM only): a slot for pairing a new device.
247#[derive(Debug, Clone, Serialize, Deserialize)]
248#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
249pub struct PairingSlot {
250    pub slot: u16,
251    pub expires_at: DateTime<Utc>,
252}
253
254/// A message relayed between the two sides of a pairing:
255/// `PUT`/`GET /pairings/{slot}/{host|joiner}/{n}`. Opaque to the server.
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
257#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
258pub struct PairingMessage {
259    /// Unpadded base64url.
260    #[serde(with = "bytes::vec")]
261    #[cfg_attr(feature = "openapi", schema(value_type = String))]
262    pub data: Vec<u8>,
263}