From 04758bee99d8f03c5443d0ca790a7136277b4045 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Fri, 19 Sep 2025 11:12:48 +0200 Subject: [PATCH] Admin API to list upstream OAuth 2.0 providers --- crates/handlers/src/admin/mod.rs | 5 + crates/handlers/src/admin/model.rs | 76 +++ crates/handlers/src/admin/v1/mod.rs | 8 + .../admin/v1/upstream_oauth_providers/list.rs | 554 ++++++++++++++++++ .../admin/v1/upstream_oauth_providers/mod.rs | 8 + docs/api/spec.json | 240 ++++++++ 6 files changed, 891 insertions(+) create mode 100644 crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs create mode 100644 crates/handlers/src/admin/v1/upstream_oauth_providers/mod.rs diff --git a/crates/handlers/src/admin/mod.rs b/crates/handlers/src/admin/mod.rs index 2670d35ab..e5e158be3 100644 --- a/crates/handlers/src/admin/mod.rs +++ b/crates/handlers/src/admin/mod.rs @@ -91,6 +91,11 @@ fn finish(t: TransformOpenApi) -> TransformOpenApi { ), ..Default::default() }) + .tag(Tag { + name: "upstream-oauth-provider".to_owned(), + description: Some("Manage upstream OAuth 2.0 providers".to_owned()), + ..Tag::default() + }) .security_scheme("oauth2", oauth_security_scheme(None)) .security_scheme( "token", diff --git a/crates/handlers/src/admin/model.rs b/crates/handlers/src/admin/model.rs index 2f6648402..c21e22fd7 100644 --- a/crates/handlers/src/admin/model.rs +++ b/crates/handlers/src/admin/model.rs @@ -695,3 +695,79 @@ impl UserRegistrationToken { ] } } + +/// An upstream OAuth 2.0 provider +#[derive(Serialize, JsonSchema)] +pub struct UpstreamOAuthProvider { + #[serde(skip)] + id: Ulid, + + /// The OIDC issuer of the provider + issuer: Option, + + /// A human-readable name for the provider + human_name: Option, + + /// A brand identifier, e.g. "apple" or "google" + brand_name: Option, + + /// When the provider was created + created_at: DateTime, + + /// When the provider was disabled. If null, the provider is enabled. + disabled_at: Option>, +} + +impl From for UpstreamOAuthProvider { + fn from(provider: mas_data_model::UpstreamOAuthProvider) -> Self { + Self { + id: provider.id, + issuer: provider.issuer, + human_name: provider.human_name, + brand_name: provider.brand_name, + created_at: provider.created_at, + disabled_at: provider.disabled_at, + } + } +} + +impl Resource for UpstreamOAuthProvider { + const KIND: &'static str = "upstream-oauth-provider"; + const PATH: &'static str = "/api/admin/v1/upstream-oauth-providers"; + + fn id(&self) -> Ulid { + self.id + } +} + +impl UpstreamOAuthProvider { + /// Samples of upstream OAuth 2.0 providers + pub fn samples() -> [Self; 3] { + [ + Self { + id: Ulid::from_bytes([0x01; 16]), + issuer: Some("https://accounts.google.com".to_owned()), + human_name: Some("Google".to_owned()), + brand_name: Some("google".to_owned()), + created_at: DateTime::default(), + disabled_at: None, + }, + Self { + id: Ulid::from_bytes([0x02; 16]), + issuer: Some("https://appleid.apple.com".to_owned()), + human_name: Some("Apple ID".to_owned()), + brand_name: Some("apple".to_owned()), + created_at: DateTime::default(), + disabled_at: Some(DateTime::default()), + }, + Self { + id: Ulid::from_bytes([0x03; 16]), + issuer: None, + human_name: Some("Custom OAuth Provider".to_owned()), + brand_name: None, + created_at: DateTime::default(), + disabled_at: None, + }, + ] + } +} diff --git a/crates/handlers/src/admin/v1/mod.rs b/crates/handlers/src/admin/v1/mod.rs index afe71a05f..8a182bf2f 100644 --- a/crates/handlers/src/admin/v1/mod.rs +++ b/crates/handlers/src/admin/v1/mod.rs @@ -23,6 +23,7 @@ mod oauth2_sessions; mod policy_data; mod site_config; mod upstream_oauth_links; +mod upstream_oauth_providers; mod user_emails; mod user_registration_tokens; mod user_sessions; @@ -187,4 +188,11 @@ where self::upstream_oauth_links::delete_doc, ), ) + .api_route( + "/upstream-oauth-providers", + get_with( + self::upstream_oauth_providers::list, + self::upstream_oauth_providers::list_doc, + ), + ) } diff --git a/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs b/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs new file mode 100644 index 000000000..dc5f2cc9c --- /dev/null +++ b/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs @@ -0,0 +1,554 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. + +use aide::{OperationIo, transform::TransformOperation}; +use axum::{ + Json, + extract::{Query, rejection::QueryRejection}, + response::IntoResponse, +}; +use axum_macros::FromRequestParts; +use hyper::StatusCode; +use mas_axum_utils::record_error; +use mas_storage::{Page, upstream_oauth2::UpstreamOAuthProviderFilter}; +use schemars::JsonSchema; +use serde::Deserialize; + +use crate::{ + admin::{ + call_context::CallContext, + model::{Resource, UpstreamOAuthProvider}, + params::Pagination, + response::{ErrorResponse, PaginatedResponse}, + }, + impl_from_error_for_route, +}; + +#[derive(FromRequestParts, Deserialize, JsonSchema, OperationIo)] +#[serde(rename = "UpstreamOAuthProviderFilter")] +#[aide(input_with = "Query")] +#[from_request(via(Query), rejection(RouteError))] +pub struct FilterParams { + /// Retrieve providers that are (or are not) enabled + #[serde(rename = "filter[enabled]")] + enabled: Option, +} + +impl std::fmt::Display for FilterParams { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let mut sep = '?'; + + if let Some(enabled) = self.enabled { + write!(f, "{sep}filter[enabled]={enabled}")?; + sep = '&'; + } + + let _ = sep; + Ok(()) + } +} + +#[derive(Debug, thiserror::Error, OperationIo)] +#[aide(output_with = "Json")] +pub enum RouteError { + #[error(transparent)] + Internal(Box), + + #[error("Invalid filter parameters")] + InvalidFilter(#[from] QueryRejection), +} + +impl_from_error_for_route!(mas_storage::RepositoryError); + +impl IntoResponse for RouteError { + fn into_response(self) -> axum::response::Response { + let error = ErrorResponse::from_error(&self); + let sentry_event_id = record_error!(self, Self::Internal(_)); + let status = match self { + Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::InvalidFilter(_) => StatusCode::BAD_REQUEST, + }; + + (status, sentry_event_id, Json(error)).into_response() + } +} + +pub fn doc(operation: TransformOperation) -> TransformOperation { + operation + .id("listUpstreamOAuthProviders") + .summary("List upstream OAuth 2.0 providers") + .tag("upstream-oauth-provider") + .response_with::<200, Json>, _>(|t| { + let providers = UpstreamOAuthProvider::samples(); + let pagination = mas_storage::Pagination::first(providers.len()); + let page = Page { + edges: providers.into(), + has_next_page: true, + has_previous_page: false, + }; + + t.description("Paginated response of upstream OAuth 2.0 providers") + .example(PaginatedResponse::new( + page, + pagination, + 42, + UpstreamOAuthProvider::PATH, + )) + }) +} + +#[tracing::instrument(name = "handler.admin.v1.upstream_oauth_providers.list", skip_all)] +pub async fn handler( + CallContext { mut repo, .. }: CallContext, + Pagination(pagination): Pagination, + params: FilterParams, +) -> Result>, RouteError> { + let base = format!("{path}{params}", path = UpstreamOAuthProvider::PATH); + let filter = UpstreamOAuthProviderFilter::new(); + + let filter = match params.enabled { + Some(true) => filter.enabled_only(), + Some(false) => filter.disabled_only(), + None => filter, + }; + + let page = repo + .upstream_oauth_provider() + .list(filter, pagination) + .await?; + let count = repo.upstream_oauth_provider().count(filter).await?; + + Ok(Json(PaginatedResponse::new( + page.map(UpstreamOAuthProvider::from), + pagination, + count, + &base, + ))) +} + +#[cfg(test)] +mod tests { + use hyper::{Request, StatusCode}; + use mas_data_model::{ + UpstreamOAuthProviderClaimsImports, UpstreamOAuthProviderDiscoveryMode, + UpstreamOAuthProviderOnBackchannelLogout, UpstreamOAuthProviderPkceMode, + UpstreamOAuthProviderTokenAuthMethod, + }; + use mas_iana::jose::JsonWebSignatureAlg; + use mas_storage::{ + RepositoryAccess, + upstream_oauth2::{UpstreamOAuthProviderParams, UpstreamOAuthProviderRepository}, + }; + use oauth2_types::scope::{OPENID, Scope}; + use sqlx::PgPool; + + use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup}; + + async fn create_test_providers(state: &mut TestState) { + let mut repo = state.repository().await.unwrap(); + + // Create an enabled provider + let enabled_params = UpstreamOAuthProviderParams { + issuer: Some("https://accounts.google.com".to_owned()), + human_name: Some("Google".to_owned()), + brand_name: Some("google".to_owned()), + discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: UpstreamOAuthProviderPkceMode::Auto, + jwks_uri_override: None, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: true, + userinfo_signed_response_alg: None, + client_id: "google-client-id".to_owned(), + encrypted_client_secret: Some("encrypted-secret".to_owned()), + token_endpoint_signing_alg: None, + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretPost, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + response_mode: None, + scope: Scope::from_iter([OPENID]), + claims_imports: UpstreamOAuthProviderClaimsImports::default(), + additional_authorization_parameters: vec![], + forward_login_hint: false, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 0, + }; + + repo.upstream_oauth_provider() + .add(&mut state.rng(), &state.clock, enabled_params) + .await + .unwrap(); + + // Create a disabled provider + let disabled_params = UpstreamOAuthProviderParams { + issuer: Some("https://appleid.apple.com".to_owned()), + human_name: Some("Apple ID".to_owned()), + brand_name: Some("apple".to_owned()), + discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: UpstreamOAuthProviderPkceMode::S256, + jwks_uri_override: None, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: true, + userinfo_signed_response_alg: None, + client_id: "apple-client-id".to_owned(), + encrypted_client_secret: Some("encrypted-secret".to_owned()), + token_endpoint_signing_alg: None, + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretPost, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + response_mode: None, + scope: Scope::from_iter([OPENID]), + claims_imports: UpstreamOAuthProviderClaimsImports::default(), + additional_authorization_parameters: vec![], + forward_login_hint: false, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 1, + }; + + let disabled_provider = repo + .upstream_oauth_provider() + .add(&mut state.rng(), &state.clock, disabled_params) + .await + .unwrap(); + + // Disable the provider + repo.upstream_oauth_provider() + .disable(&state.clock, disabled_provider) + .await + .unwrap(); + + // Create another enabled provider + let another_enabled_params = UpstreamOAuthProviderParams { + issuer: Some("https://login.microsoftonline.com/common/v2.0".to_owned()), + human_name: Some("Microsoft".to_owned()), + brand_name: Some("microsoft".to_owned()), + discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc, + pkce_mode: UpstreamOAuthProviderPkceMode::Auto, + jwks_uri_override: None, + authorization_endpoint_override: None, + token_endpoint_override: None, + userinfo_endpoint_override: None, + fetch_userinfo: true, + userinfo_signed_response_alg: None, + client_id: "microsoft-client-id".to_owned(), + encrypted_client_secret: Some("encrypted-secret".to_owned()), + token_endpoint_signing_alg: None, + token_endpoint_auth_method: UpstreamOAuthProviderTokenAuthMethod::ClientSecretPost, + id_token_signed_response_alg: JsonWebSignatureAlg::Rs256, + response_mode: None, + scope: Scope::from_iter([OPENID]), + claims_imports: UpstreamOAuthProviderClaimsImports::default(), + additional_authorization_parameters: vec![], + forward_login_hint: false, + on_backchannel_logout: UpstreamOAuthProviderOnBackchannelLogout::DoNothing, + ui_order: 2, + }; + + repo.upstream_oauth_provider() + .add(&mut state.rng(), &state.clock, another_enabled_params) + .await + .unwrap(); + + Box::new(repo).save().await.unwrap(); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_list_all_providers(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + create_test_providers(&mut state).await; + + let request = Request::get("/api/admin/v1/upstream-oauth-providers") + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + // Should return all providers + assert_eq!(body["data"].as_array().unwrap().len(), 3); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 3 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG07HNEZXNQM2KNBNF6", + "attributes": { + "issuer": "https://appleid.apple.com", + "human_name": "Apple ID", + "brand_name": "apple", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": "2022-01-16T14:40:00Z" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6" + } + }, + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG09AVTNSQFMSR34AJC", + "attributes": { + "issuer": "https://login.microsoftonline.com/common/v2.0", + "human_name": "Microsoft", + "brand_name": "microsoft", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC" + } + }, + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?page[first]=10", + "first": "/api/admin/v1/upstream-oauth-providers?page[first]=10", + "last": "/api/admin/v1/upstream-oauth-providers?page[last]=10" + } + } + "#); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_filter_by_enabled_true(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + create_test_providers(&mut state).await; + + let request = Request::get("/api/admin/v1/upstream-oauth-providers?filter[enabled]=true") + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 2 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG09AVTNSQFMSR34AJC", + "attributes": { + "issuer": "https://login.microsoftonline.com/common/v2.0", + "human_name": "Microsoft", + "brand_name": "microsoft", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC" + } + }, + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=true&page[first]=10", + "first": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=true&page[first]=10", + "last": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=true&page[last]=10" + } + } + "#); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_filter_by_enabled_false(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + create_test_providers(&mut state).await; + + let request = Request::get("/api/admin/v1/upstream-oauth-providers?filter[enabled]=false") + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 1 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG07HNEZXNQM2KNBNF6", + "attributes": { + "issuer": "https://appleid.apple.com", + "human_name": "Apple ID", + "brand_name": "apple", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": "2022-01-16T14:40:00Z" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6" + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=false&page[first]=10", + "first": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=false&page[first]=10", + "last": "/api/admin/v1/upstream-oauth-providers?filter[enabled]=false&page[last]=10" + } + } + "#); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_pagination(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + create_test_providers(&mut state).await; + + // Test first page with limit of 2 + let request = Request::get("/api/admin/v1/upstream-oauth-providers?page[first]=2") + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 3 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG07HNEZXNQM2KNBNF6", + "attributes": { + "issuer": "https://appleid.apple.com", + "human_name": "Apple ID", + "brand_name": "apple", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": "2022-01-16T14:40:00Z" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG07HNEZXNQM2KNBNF6" + } + }, + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG09AVTNSQFMSR34AJC", + "attributes": { + "issuer": "https://login.microsoftonline.com/common/v2.0", + "human_name": "Microsoft", + "brand_name": "microsoft", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG09AVTNSQFMSR34AJC" + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?page[first]=2", + "first": "/api/admin/v1/upstream-oauth-providers?page[first]=2", + "last": "/api/admin/v1/upstream-oauth-providers?page[last]=2", + "next": "/api/admin/v1/upstream-oauth-providers?page[after]=01FSHN9AG09AVTNSQFMSR34AJC&page[first]=2" + } + } + "#); + + // Extract the ID of the last item for pagination + let last_item_id = body["data"][1]["id"].as_str().unwrap(); + let request = Request::get(format!( + "/api/admin/v1/upstream-oauth-providers?page[first]=2&page[after]={last_item_id}", + )) + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::OK); + let body: serde_json::Value = response.json::(); + + insta::assert_json_snapshot!(body, @r#" + { + "meta": { + "count": 3 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01FSHN9AG0MZAA6S4AF7CTV32E", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "2022-01-16T14:40:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01FSHN9AG0MZAA6S4AF7CTV32E" + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?page[after]=01FSHN9AG09AVTNSQFMSR34AJC&page[first]=2", + "first": "/api/admin/v1/upstream-oauth-providers?page[first]=2", + "last": "/api/admin/v1/upstream-oauth-providers?page[last]=2" + } + } + "#); + } + + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_invalid_filter(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let admin_token = state.token_with_scope("urn:mas:admin").await; + + let request = + Request::get("/api/admin/v1/upstream-oauth-providers?filter[enabled]=invalid") + .bearer(&admin_token) + .empty(); + + let response = state.request(request).await; + response.assert_status(StatusCode::BAD_REQUEST); + } +} diff --git a/crates/handlers/src/admin/v1/upstream_oauth_providers/mod.rs b/crates/handlers/src/admin/v1/upstream_oauth_providers/mod.rs new file mode 100644 index 000000000..a04301246 --- /dev/null +++ b/crates/handlers/src/admin/v1/upstream_oauth_providers/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2025 New Vector Ltd. +// +// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial +// Please see LICENSE files in the repository root for full details. + +mod list; + +pub use self::list::{doc as list_doc, handler as list}; diff --git a/docs/api/spec.json b/docs/api/spec.json index 3348cf722..166436454 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -3222,6 +3222,143 @@ } } } + }, + "/api/admin/v1/upstream-oauth-providers": { + "get": { + "tags": [ + "upstream-oauth-provider" + ], + "summary": "List upstream OAuth 2.0 providers", + "operationId": "listUpstreamOAuthProviders", + "parameters": [ + { + "in": "query", + "name": "page[before]", + "description": "Retrieve the items before the given ID", + "schema": { + "description": "Retrieve the items before the given ID", + "$ref": "#/components/schemas/ULID", + "nullable": true + }, + "style": "form" + }, + { + "in": "query", + "name": "page[after]", + "description": "Retrieve the items after the given ID", + "schema": { + "description": "Retrieve the items after the given ID", + "$ref": "#/components/schemas/ULID", + "nullable": true + }, + "style": "form" + }, + { + "in": "query", + "name": "page[first]", + "description": "Retrieve the first N items", + "schema": { + "description": "Retrieve the first N items", + "type": "integer", + "format": "uint", + "minimum": 1.0, + "nullable": true + }, + "style": "form" + }, + { + "in": "query", + "name": "page[last]", + "description": "Retrieve the last N items", + "schema": { + "description": "Retrieve the last N items", + "type": "integer", + "format": "uint", + "minimum": 1.0, + "nullable": true + }, + "style": "form" + }, + { + "in": "query", + "name": "filter[enabled]", + "description": "Retrieve providers that are (or are not) enabled", + "schema": { + "description": "Retrieve providers that are (or are not) enabled", + "type": "boolean", + "nullable": true + }, + "style": "form" + } + ], + "responses": { + "200": { + "description": "Paginated response of upstream OAuth 2.0 providers", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PaginatedResponse_for_UpstreamOAuthProvider" + }, + "example": { + "meta": { + "count": 42 + }, + "data": [ + { + "type": "upstream-oauth-provider", + "id": "01040G2081040G2081040G2081", + "attributes": { + "issuer": "https://accounts.google.com", + "human_name": "Google", + "brand_name": "google", + "created_at": "1970-01-01T00:00:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/01040G2081040G2081040G2081" + } + }, + { + "type": "upstream-oauth-provider", + "id": "02081040G2081040G2081040G2", + "attributes": { + "issuer": "https://appleid.apple.com", + "human_name": "Apple ID", + "brand_name": "apple", + "created_at": "1970-01-01T00:00:00Z", + "disabled_at": "1970-01-01T00:00:00Z" + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/02081040G2081040G2081040G2" + } + }, + { + "type": "upstream-oauth-provider", + "id": "030C1G60R30C1G60R30C1G60R3", + "attributes": { + "issuer": null, + "human_name": "Custom OAuth Provider", + "brand_name": null, + "created_at": "1970-01-01T00:00:00Z", + "disabled_at": null + }, + "links": { + "self": "/api/admin/v1/upstream-oauth-providers/030C1G60R30C1G60R30C1G60R3" + } + } + ], + "links": { + "self": "/api/admin/v1/upstream-oauth-providers?page[first]=3", + "first": "/api/admin/v1/upstream-oauth-providers?page[first]=3", + "last": "/api/admin/v1/upstream-oauth-providers?page[last]=3", + "next": "/api/admin/v1/upstream-oauth-providers?page[after]=030C1G60R30C1G60R30C1G60R3&page[first]=3" + } + } + } + } + } + } + } } }, "components": { @@ -4717,6 +4854,105 @@ "$ref": "#/components/schemas/SelfLinks" } } + }, + "UpstreamOAuthProviderFilter": { + "type": "object", + "properties": { + "filter[enabled]": { + "description": "Retrieve providers that are (or are not) enabled", + "type": "boolean", + "nullable": true + } + } + }, + "PaginatedResponse_for_UpstreamOAuthProvider": { + "description": "A top-level response with a page of resources", + "type": "object", + "required": [ + "data", + "links", + "meta" + ], + "properties": { + "meta": { + "description": "Response metadata", + "$ref": "#/components/schemas/PaginationMeta" + }, + "data": { + "description": "The list of resources", + "type": "array", + "items": { + "$ref": "#/components/schemas/SingleResource_for_UpstreamOAuthProvider" + } + }, + "links": { + "description": "Related links", + "$ref": "#/components/schemas/PaginationLinks" + } + } + }, + "SingleResource_for_UpstreamOAuthProvider": { + "description": "A single resource, with its type, ID, attributes and related links", + "type": "object", + "required": [ + "attributes", + "id", + "links", + "type" + ], + "properties": { + "type": { + "description": "The type of the resource", + "type": "string" + }, + "id": { + "description": "The ID of the resource", + "$ref": "#/components/schemas/ULID" + }, + "attributes": { + "description": "The attributes of the resource", + "$ref": "#/components/schemas/UpstreamOAuthProvider" + }, + "links": { + "description": "Related links", + "$ref": "#/components/schemas/SelfLinks" + } + } + }, + "UpstreamOAuthProvider": { + "description": "An upstream OAuth 2.0 provider", + "type": "object", + "required": [ + "created_at" + ], + "properties": { + "issuer": { + "description": "The OIDC issuer of the provider", + "type": "string", + "nullable": true + }, + "human_name": { + "description": "A human-readable name for the provider", + "type": "string", + "nullable": true + }, + "brand_name": { + "description": "A brand identifier, e.g. \"apple\" or \"google\"", + "type": "string", + "nullable": true + }, + "created_at": { + "description": "When the provider was created", + "type": "string", + "format": "date-time" + }, + "disabled_at": { + "description": "When the provider was disabled. If null, the provider is enabled.", + "type": "string", + "format": "date-time", + "nullable": true + } + } } } }, @@ -4768,6 +5004,10 @@ { "name": "upstream-oauth-link", "description": "Manage links between local users and identities from upstream OAuth 2.0 providers" + }, + { + "name": "upstream-oauth-provider", + "description": "Manage upstream OAuth 2.0 providers" } ] }