Skip to content

Commit 2b81c8a

Browse files
authored
Admin API for adding and removing upstream oauth links (#4255)
2 parents 1e4ce8f + 7c4a9bf commit 2b81c8a

File tree

13 files changed

+1027
-15
lines changed

13 files changed

+1027
-15
lines changed

crates/data-model/src/upstream_oauth2/session.rs

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ pub enum UpstreamOAuthAuthorizationSessionState {
3030
extra_callback_parameters: Option<serde_json::Value>,
3131
userinfo: Option<serde_json::Value>,
3232
},
33+
Unlinked {
34+
completed_at: DateTime<Utc>,
35+
consumed_at: Option<DateTime<Utc>>,
36+
unlinked_at: DateTime<Utc>,
37+
id_token: Option<String>,
38+
},
3339
}
3440

3541
impl UpstreamOAuthAuthorizationSessionState {
@@ -57,7 +63,9 @@ impl UpstreamOAuthAuthorizationSessionState {
5763
extra_callback_parameters,
5864
userinfo,
5965
}),
60-
Self::Completed { .. } | Self::Consumed { .. } => Err(InvalidTransitionError),
66+
Self::Completed { .. } | Self::Consumed { .. } | Self::Unlinked { .. } => {
67+
Err(InvalidTransitionError)
68+
}
6169
}
6270
}
6371

@@ -85,7 +93,9 @@ impl UpstreamOAuthAuthorizationSessionState {
8593
extra_callback_parameters,
8694
userinfo,
8795
}),
88-
Self::Pending | Self::Consumed { .. } => Err(InvalidTransitionError),
96+
Self::Pending | Self::Consumed { .. } | Self::Unlinked { .. } => {
97+
Err(InvalidTransitionError)
98+
}
8999
}
90100
}
91101

@@ -98,7 +108,7 @@ impl UpstreamOAuthAuthorizationSessionState {
98108
#[must_use]
99109
pub fn link_id(&self) -> Option<Ulid> {
100110
match self {
101-
Self::Pending => None,
111+
Self::Pending | Self::Unlinked { .. } => None,
102112
Self::Completed { link_id, .. } | Self::Consumed { link_id, .. } => Some(*link_id),
103113
}
104114
}
@@ -114,9 +124,9 @@ impl UpstreamOAuthAuthorizationSessionState {
114124
pub fn completed_at(&self) -> Option<DateTime<Utc>> {
115125
match self {
116126
Self::Pending => None,
117-
Self::Completed { completed_at, .. } | Self::Consumed { completed_at, .. } => {
118-
Some(*completed_at)
119-
}
127+
Self::Completed { completed_at, .. }
128+
| Self::Consumed { completed_at, .. }
129+
| Self::Unlinked { completed_at, .. } => Some(*completed_at),
120130
}
121131
}
122132

@@ -130,9 +140,9 @@ impl UpstreamOAuthAuthorizationSessionState {
130140
pub fn id_token(&self) -> Option<&str> {
131141
match self {
132142
Self::Pending => None,
133-
Self::Completed { id_token, .. } | Self::Consumed { id_token, .. } => {
134-
id_token.as_deref()
135-
}
143+
Self::Completed { id_token, .. }
144+
| Self::Consumed { id_token, .. }
145+
| Self::Unlinked { id_token, .. } => id_token.as_deref(),
136146
}
137147
}
138148

@@ -145,7 +155,7 @@ impl UpstreamOAuthAuthorizationSessionState {
145155
#[must_use]
146156
pub fn extra_callback_parameters(&self) -> Option<&serde_json::Value> {
147157
match self {
148-
Self::Pending => None,
158+
Self::Pending | Self::Unlinked { .. } => None,
149159
Self::Completed {
150160
extra_callback_parameters,
151161
..
@@ -160,7 +170,7 @@ impl UpstreamOAuthAuthorizationSessionState {
160170
#[must_use]
161171
pub fn userinfo(&self) -> Option<&serde_json::Value> {
162172
match self {
163-
Self::Pending => None,
173+
Self::Pending | Self::Unlinked { .. } => None,
164174
Self::Completed { userinfo, .. } | Self::Consumed { userinfo, .. } => userinfo.as_ref(),
165175
}
166176
}
@@ -177,6 +187,22 @@ impl UpstreamOAuthAuthorizationSessionState {
177187
match self {
178188
Self::Pending | Self::Completed { .. } => None,
179189
Self::Consumed { consumed_at, .. } => Some(*consumed_at),
190+
Self::Unlinked { consumed_at, .. } => *consumed_at,
191+
}
192+
}
193+
194+
/// Get the time at which the upstream OAuth 2.0 authorization session was
195+
/// unlinked.
196+
///
197+
/// Returns `None` if the upstream OAuth 2.0 authorization session state is
198+
/// not [`Unlinked`].
199+
///
200+
/// [`Unlinked`]: UpstreamOAuthAuthorizationSessionState::Unlinked
201+
#[must_use]
202+
pub fn unlinked_at(&self) -> Option<DateTime<Utc>> {
203+
match self {
204+
Self::Pending | Self::Completed { .. } | Self::Consumed { .. } => None,
205+
Self::Unlinked { unlinked_at, .. } => Some(*unlinked_at),
180206
}
181207
}
182208

@@ -206,6 +232,15 @@ impl UpstreamOAuthAuthorizationSessionState {
206232
pub fn is_consumed(&self) -> bool {
207233
matches!(self, Self::Consumed { .. })
208234
}
235+
236+
/// Returns `true` if the upstream OAuth 2.0 authorization session state is
237+
/// [`Unlinked`].
238+
///
239+
/// [`Unlinked`]: UpstreamOAuthAuthorizationSessionState::Unlinked
240+
#[must_use]
241+
pub fn is_unlinked(&self) -> bool {
242+
matches!(self, Self::Unlinked { .. })
243+
}
209244
}
210245

211246
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]

crates/handlers/src/admin/v1/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ mod user_emails;
2626
mod user_sessions;
2727
mod users;
2828

29+
#[allow(clippy::too_many_lines)]
2930
pub fn router<S>() -> ApiRouter<S>
3031
where
3132
S: Clone + Send + Sync + 'static,
@@ -123,13 +124,21 @@ where
123124
get_with(
124125
self::upstream_oauth_links::list,
125126
self::upstream_oauth_links::list_doc,
127+
)
128+
.post_with(
129+
self::upstream_oauth_links::add,
130+
self::upstream_oauth_links::add_doc,
126131
),
127132
)
128133
.api_route(
129134
"/upstream-oauth-links/{id}",
130135
get_with(
131136
self::upstream_oauth_links::get,
132137
self::upstream_oauth_links::get_doc,
138+
)
139+
.delete_with(
140+
self::upstream_oauth_links::delete,
141+
self::upstream_oauth_links::delete_doc,
133142
),
134143
)
135144
}

0 commit comments

Comments
 (0)