From 7bbb3ff9cfef4790bf536895822c8757224bd036 Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Tue, 1 Jul 2025 23:14:09 +0200 Subject: [PATCH 1/4] api: add instrospect endpoint Fixes #1091 --- doc/api/garage-admin-v2.json | 82 +++++++++++++++++++++++++++++ src/api/admin/admin_token.rs | 91 +++++++++++++++++++++++++++++++++ src/api/admin/api.rs | 24 +++++++++ src/api/admin/openapi.rs | 15 ++++++ src/api/admin/router_v2.rs | 1 + src/api/common/router_macros.rs | 15 ++++++ 6 files changed, 228 insertions(+) diff --git a/doc/api/garage-admin-v2.json b/doc/api/garage-admin-v2.json index d9b2622a..520bf17a 100644 --- a/doc/api/garage-admin-v2.json +++ b/doc/api/garage-admin-v2.json @@ -869,6 +869,40 @@ } } }, + "/v2/IntrospectAdminToken": { + "get": { + "tags": [ + "Admin API token" + ], + "description": "\nReturn information about the calling admin API token.\n ", + "operationId": "IntrospectAdminToken", + "parameters": [ + { + "name": "admin_token", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Information about the admin token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IntrospectAdminTokenResponse" + } + } + } + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/v2/GetNodeInfo": { "get": { "tags": [ @@ -2644,6 +2678,54 @@ } } }, + "IntrospectAdminTokenResponse": { + "type": "object", + "required": [ + "name", + "expired", + "scope" + ], + "properties": { + "created": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Creation date" + }, + "expiration": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Expiration time and date, formatted according to RFC 3339" + }, + "expired": { + "type": "boolean", + "description": "Whether this admin token is expired already" + }, + "id": { + "type": [ + "string", + "null" + ], + "description": "Identifier of the admin token (which is also a prefix of the full bearer token)" + }, + "name": { + "type": "string", + "description": "Name of the admin API token" + }, + "scope": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scope of the admin API token, a list of admin endpoint names (such as\n`GetClusterStatus`, etc), or the special value `*` to allow all\nadmin endpoints" + } + } + }, "ImportKeyRequest": { "type": "object", "required": [ diff --git a/src/api/admin/admin_token.rs b/src/api/admin/admin_token.rs index ac937eea..598c4bca 100644 --- a/src/api/admin/admin_token.rs +++ b/src/api/admin/admin_token.rs @@ -175,6 +175,79 @@ impl RequestHandler for DeleteAdminTokenRequest { } } +impl RequestHandler for IntrospectAdminTokenRequest { + type Response = IntrospectAdminTokenResponse; + + async fn handle( + self, + garage: &Arc, + _admin: &Admin, + ) -> Result { + let now = now_msec(); + + if garage + .config + .admin + .metrics_token + .as_ref() + .is_some_and(|s| s == &self.admin_token) + { + return Ok(IntrospectAdminTokenResponse { + id: None, + created: None, + name: "metrics_token (from daemon configuration)".into(), + expiration: None, + expired: false, + scope: vec!["Metrics".into()], + }); + } + + if garage + .config + .admin + .admin_token + .as_ref() + .is_some_and(|s| s == &self.admin_token) + { + return Ok(IntrospectAdminTokenResponse { + id: None, + created: None, + name: "admin_token (from daemon configuration)".into(), + expiration: None, + expired: false, + scope: vec!["*".into()], + }); + } + + let (prefix, _) = self.admin_token.split_once('.').unwrap(); + + let candidates = garage + .admin_token_table + .get_range( + &EmptyKey, + None, + Some(KeyFilter::MatchesAndNotDeleted( + prefix.clone().parse().unwrap(), + )), + 10, + EnumerationOrder::Forward, + ) + .await? + .into_iter() + .collect::>(); + if candidates.len() != 1 { + return Err(Error::bad_request(format!( + "{} matching admin tokens", + candidates.len() + ))); + } + Ok(my_admin_token_info_results( + &candidates.into_iter().next().unwrap(), + now, + )) + } +} + // ---- helpers ---- fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInfoResponse { @@ -233,3 +306,21 @@ fn apply_token_updates( Ok(()) } + +fn my_admin_token_info_results(token: &AdminApiToken, now: u64) -> IntrospectAdminTokenResponse { + let params = token.params().unwrap(); + + IntrospectAdminTokenResponse { + id: Some(token.prefix.clone()), + created: Some( + DateTime::from_timestamp_millis(params.created as i64) + .expect("invalid timestamp stored in db"), + ), + name: params.name.get().to_string(), + expiration: params.expiration.get().map(|x| { + DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db") + }), + expired: params.is_expired(now), + scope: params.scope.get().0.clone(), + } +} diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index d2faa618..d1322b79 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -56,6 +56,7 @@ admin_endpoints![ CreateAdminToken, UpdateAdminToken, DeleteAdminToken, + IntrospectAdminToken, // Layout operations GetClusterLayout, @@ -391,6 +392,29 @@ pub struct DeleteAdminTokenRequest { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeleteAdminTokenResponse; +#[derive(Debug, Clone, Serialize, Deserialize, IntoParams)] +pub struct IntrospectAdminTokenRequest { + pub admin_token: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct IntrospectAdminTokenResponse { + /// Identifier of the admin token (which is also a prefix of the full bearer token) + pub id: Option, + /// Creation date + pub created: Option>, + /// Name of the admin API token + pub name: String, + /// Expiration time and date, formatted according to RFC 3339 + pub expiration: Option>, + /// Whether this admin token is expired already + pub expired: bool, + /// Scope of the admin API token, a list of admin endpoint names (such as + /// `GetClusterStatus`, etc), or the special value `*` to allow all + /// admin endpoints + pub scope: Vec, +} // ********************************************** // Layout operations // ********************************************** diff --git a/src/api/admin/openapi.rs b/src/api/admin/openapi.rs index 890bfd6c..47ec035a 100644 --- a/src/api/admin/openapi.rs +++ b/src/api/admin/openapi.rs @@ -191,6 +191,20 @@ fn UpdateAdminToken() -> () {} )] fn DeleteAdminToken() -> () {} +#[utoipa::path(get, + path = "/v2/IntrospectAdminToken", + tag = "Admin API token", + description = " +Return information about the calling admin API token. + ", + params(IntrospectAdminTokenRequest), + responses( + (status = 200, description = "Information about the admin token", body = IntrospectAdminTokenResponse), + (status = 500, description = "Internal server error") + ), +)] +fn IntrospectAdminToken() -> () {} + // ********************************************** // Layout operations // ********************************************** @@ -872,6 +886,7 @@ impl Modify for SecurityAddon { CreateAdminToken, UpdateAdminToken, DeleteAdminToken, + IntrospectAdminToken, // Layout operations GetClusterLayout, GetClusterLayoutHistory, diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index 3051dae4..06836b46 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -40,6 +40,7 @@ impl AdminApiRequest { POST CreateAdminToken (body), POST UpdateAdminToken (body_field, query::id), POST DeleteAdminToken (query::id), + GET IntrospectAdminToken (admin_token), // Layout endpoints GET GetClusterLayout (), GET GetClusterLayoutHistory (), diff --git a/src/api/common/router_macros.rs b/src/api/common/router_macros.rs index f4a93c67..79b40e80 100644 --- a/src/api/common/router_macros.rs +++ b/src/api/common/router_macros.rs @@ -83,6 +83,21 @@ macro_rules! router_match { parse_json_body::< [<$api Request>], _, Error>($req).await? }) }}; + (@@gen_parse_request $api:ident, (admin_token), $query: expr, $req:expr) => {{ + paste!({ + let auth_header = $req.headers() + .get(hyper::header::AUTHORIZATION) + .ok_or_else(|| Error::bad_request("Missing Authorization header"))? + .to_str() + .map_err(|_| Error::bad_request("Invalid Authorization header"))?; + + let admin_token = auth_header.strip_prefix("Bearer ") + .ok_or_else(|| Error::bad_request("Authorization header must be Bearer token"))? + .to_string(); + + [< $api Request >] { admin_token } + }) + }}; (@@gen_parse_request $api:ident, (body_field, $($conv:ident $(($conv_arg:expr))? :: $param:ident),*), $query: expr, $req:expr) => {{ From 58a96dc687c5a677eb97f85916b670231d1ad470 Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Wed, 2 Jul 2025 20:53:25 +0200 Subject: [PATCH 2/4] api: correct openapi def --- doc/api/garage-admin-v2.json | 156 ++++++++++++++++------------------- src/api/admin/api.rs | 2 +- src/api/admin/openapi.rs | 1 - 3 files changed, 74 insertions(+), 85 deletions(-) diff --git a/doc/api/garage-admin-v2.json b/doc/api/garage-admin-v2.json index 520bf17a..5c26aa84 100644 --- a/doc/api/garage-admin-v2.json +++ b/doc/api/garage-admin-v2.json @@ -869,40 +869,6 @@ } } }, - "/v2/IntrospectAdminToken": { - "get": { - "tags": [ - "Admin API token" - ], - "description": "\nReturn information about the calling admin API token.\n ", - "operationId": "IntrospectAdminToken", - "parameters": [ - { - "name": "admin_token", - "in": "path", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Information about the admin token", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/IntrospectAdminTokenResponse" - } - } - } - }, - "500": { - "description": "Internal server error" - } - } - } - }, "/v2/GetNodeInfo": { "get": { "tags": [ @@ -1142,6 +1108,30 @@ } } }, + "/v2/IntrospectAdminToken": { + "get": { + "tags": [ + "Admin API token" + ], + "description": "\nReturn information about the calling admin API token.\n ", + "operationId": "IntrospectAdminToken", + "responses": { + "200": { + "description": "Information about the admin token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/IntrospectAdminTokenResponse" + } + } + } + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/v2/LaunchRepairOperation": { "post": { "tags": [ @@ -2678,54 +2668,6 @@ } } }, - "IntrospectAdminTokenResponse": { - "type": "object", - "required": [ - "name", - "expired", - "scope" - ], - "properties": { - "created": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Creation date" - }, - "expiration": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Expiration time and date, formatted according to RFC 3339" - }, - "expired": { - "type": "boolean", - "description": "Whether this admin token is expired already" - }, - "id": { - "type": [ - "string", - "null" - ], - "description": "Identifier of the admin token (which is also a prefix of the full bearer token)" - }, - "name": { - "type": "string", - "description": "Name of the admin API token" - }, - "scope": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Scope of the admin API token, a list of admin endpoint names (such as\n`GetClusterStatus`, etc), or the special value `*` to allow all\nadmin endpoints" - } - } - }, "ImportKeyRequest": { "type": "object", "required": [ @@ -2890,6 +2832,54 @@ } } }, + "IntrospectAdminTokenResponse": { + "type": "object", + "required": [ + "name", + "expired", + "scope" + ], + "properties": { + "created": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Creation date" + }, + "expiration": { + "type": [ + "string", + "null" + ], + "format": "date-time", + "description": "Expiration time and date, formatted according to RFC 3339" + }, + "expired": { + "type": "boolean", + "description": "Whether this admin token is expired already" + }, + "id": { + "type": [ + "string", + "null" + ], + "description": "Identifier of the admin token (which is also a prefix of the full bearer token)" + }, + "name": { + "type": "string", + "description": "Name of the admin API token" + }, + "scope": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Scope of the admin API token, a list of admin endpoint names (such as\n`GetClusterStatus`, etc), or the special value `*` to allow all\nadmin endpoints" + } + } + }, "KeyInfoBucketResponse": { "type": "object", "required": [ @@ -4464,4 +4454,4 @@ "bearerAuth": [] } ] -} +} \ No newline at end of file diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index d1322b79..7ad2c652 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -392,7 +392,7 @@ pub struct DeleteAdminTokenRequest { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeleteAdminTokenResponse; -#[derive(Debug, Clone, Serialize, Deserialize, IntoParams)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct IntrospectAdminTokenRequest { pub admin_token: String, } diff --git a/src/api/admin/openapi.rs b/src/api/admin/openapi.rs index 47ec035a..09780fa0 100644 --- a/src/api/admin/openapi.rs +++ b/src/api/admin/openapi.rs @@ -197,7 +197,6 @@ fn DeleteAdminToken() -> () {} description = " Return information about the calling admin API token. ", - params(IntrospectAdminTokenRequest), responses( (status = 200, description = "Information about the admin token", body = IntrospectAdminTokenResponse), (status = 500, description = "Internal server error") From 9a31b9c077389623e7d6614c21d6cba1487012d7 Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Thu, 3 Jul 2025 21:59:07 +0200 Subject: [PATCH 3/4] api: change endpoint name and allow it to be called even if not in current token scope --- doc/api/garage-admin-v2.json | 8 ++++---- src/api/admin/admin_token.rs | 14 +++++++------- src/api/admin/api.rs | 6 +++--- src/api/admin/api_server.rs | 3 ++- src/api/admin/openapi.rs | 8 ++++---- src/api/admin/router_v2.rs | 2 +- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/doc/api/garage-admin-v2.json b/doc/api/garage-admin-v2.json index 5c26aa84..9f140081 100644 --- a/doc/api/garage-admin-v2.json +++ b/doc/api/garage-admin-v2.json @@ -1108,20 +1108,20 @@ } } }, - "/v2/IntrospectAdminToken": { + "/v2/GetCurrentAdminTokenInfo": { "get": { "tags": [ "Admin API token" ], "description": "\nReturn information about the calling admin API token.\n ", - "operationId": "IntrospectAdminToken", + "operationId": "GetCurrentAdminTokenInfo", "responses": { "200": { "description": "Information about the admin token", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/IntrospectAdminTokenResponse" + "$ref": "#/components/schemas/GetCurrentAdminTokenInfoResponse" } } } @@ -2832,7 +2832,7 @@ } } }, - "IntrospectAdminTokenResponse": { + "GetCurrentAdminTokenInfoResponse": { "type": "object", "required": [ "name", diff --git a/src/api/admin/admin_token.rs b/src/api/admin/admin_token.rs index 598c4bca..a4fa17ea 100644 --- a/src/api/admin/admin_token.rs +++ b/src/api/admin/admin_token.rs @@ -175,14 +175,14 @@ impl RequestHandler for DeleteAdminTokenRequest { } } -impl RequestHandler for IntrospectAdminTokenRequest { - type Response = IntrospectAdminTokenResponse; +impl RequestHandler for GetCurrentAdminTokenInfoRequest { + type Response = GetCurrentAdminTokenInfoResponse; async fn handle( self, garage: &Arc, _admin: &Admin, - ) -> Result { + ) -> Result { let now = now_msec(); if garage @@ -192,7 +192,7 @@ impl RequestHandler for IntrospectAdminTokenRequest { .as_ref() .is_some_and(|s| s == &self.admin_token) { - return Ok(IntrospectAdminTokenResponse { + return Ok(GetCurrentAdminTokenInfoResponse { id: None, created: None, name: "metrics_token (from daemon configuration)".into(), @@ -209,7 +209,7 @@ impl RequestHandler for IntrospectAdminTokenRequest { .as_ref() .is_some_and(|s| s == &self.admin_token) { - return Ok(IntrospectAdminTokenResponse { + return Ok(GetCurrentAdminTokenInfoResponse { id: None, created: None, name: "admin_token (from daemon configuration)".into(), @@ -307,10 +307,10 @@ fn apply_token_updates( Ok(()) } -fn my_admin_token_info_results(token: &AdminApiToken, now: u64) -> IntrospectAdminTokenResponse { +fn my_admin_token_info_results(token: &AdminApiToken, now: u64) -> GetCurrentAdminTokenInfoResponse { let params = token.params().unwrap(); - IntrospectAdminTokenResponse { + GetCurrentAdminTokenInfoResponse { id: Some(token.prefix.clone()), created: Some( DateTime::from_timestamp_millis(params.created as i64) diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 7ad2c652..1af3b7ba 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -56,7 +56,7 @@ admin_endpoints![ CreateAdminToken, UpdateAdminToken, DeleteAdminToken, - IntrospectAdminToken, + GetCurrentAdminTokenInfo, // Layout operations GetClusterLayout, @@ -393,13 +393,13 @@ pub struct DeleteAdminTokenRequest { pub struct DeleteAdminTokenResponse; #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct IntrospectAdminTokenRequest { +pub struct GetCurrentAdminTokenInfoRequest { pub admin_token: String, } #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct IntrospectAdminTokenResponse { +pub struct GetCurrentAdminTokenInfoResponse { /// Identifier of the admin token (which is also a prefix of the full bearer token) pub id: Option, /// Creation date diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 14029423..9884500b 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -24,6 +24,7 @@ use garage_api_common::generic_server::*; use garage_api_common::helpers::*; use crate::api::*; +use crate::api::AdminApiRequest::GetCurrentAdminTokenInfo; use crate::error::*; use crate::router_v0; use crate::router_v1; @@ -273,7 +274,7 @@ fn verify_authorization( .get_local(&EmptyKey, &prefix.to_string())? .and_then(|k| k.state.into_option()) .filter(|p| !p.is_expired(now_msec())) - .filter(|p| p.has_scope(endpoint_name)) + .filter(|p| p.has_scope(endpoint_name) || endpoint_name == "GetCurrentAdminTokenInfo") .ok_or_else(|| Error::forbidden(invalid_msg))? .token_hash } else { diff --git a/src/api/admin/openapi.rs b/src/api/admin/openapi.rs index 09780fa0..248fd29f 100644 --- a/src/api/admin/openapi.rs +++ b/src/api/admin/openapi.rs @@ -192,17 +192,17 @@ fn UpdateAdminToken() -> () {} fn DeleteAdminToken() -> () {} #[utoipa::path(get, - path = "/v2/IntrospectAdminToken", + path = "/v2/GetCurrentAdminTokenInfo", tag = "Admin API token", description = " Return information about the calling admin API token. ", responses( - (status = 200, description = "Information about the admin token", body = IntrospectAdminTokenResponse), + (status = 200, description = "Information about the admin token", body = GetCurrentAdminTokenInfoResponse), (status = 500, description = "Internal server error") ), )] -fn IntrospectAdminToken() -> () {} +fn GetCurrentAdminTokenInfo() -> () {} // ********************************************** // Layout operations @@ -885,7 +885,7 @@ impl Modify for SecurityAddon { CreateAdminToken, UpdateAdminToken, DeleteAdminToken, - IntrospectAdminToken, + GetCurrentAdminTokenInfo, // Layout operations GetClusterLayout, GetClusterLayoutHistory, diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index 06836b46..3009d128 100644 --- a/src/api/admin/router_v2.rs +++ b/src/api/admin/router_v2.rs @@ -40,7 +40,7 @@ impl AdminApiRequest { POST CreateAdminToken (body), POST UpdateAdminToken (body_field, query::id), POST DeleteAdminToken (query::id), - GET IntrospectAdminToken (admin_token), + GET GetCurrentAdminTokenInfo (admin_token), // Layout endpoints GET GetClusterLayout (), GET GetClusterLayoutHistory (), From b4f6ab963c442022bbc528ece22f781604acc4a8 Mon Sep 17 00:00:00 2001 From: Xavier Stouder Date: Fri, 4 Jul 2025 21:36:34 +0200 Subject: [PATCH 4/4] api: correct according to review --- doc/api/garage-admin-v2.json | 99 ++++++++++-------------------------- src/api/admin/admin_token.rs | 82 +++++++++-------------------- src/api/admin/api.rs | 18 +------ src/api/admin/api_server.rs | 3 +- 4 files changed, 55 insertions(+), 147 deletions(-) diff --git a/doc/api/garage-admin-v2.json b/doc/api/garage-admin-v2.json index 9f140081..853cc1f8 100644 --- a/doc/api/garage-admin-v2.json +++ b/doc/api/garage-admin-v2.json @@ -816,6 +816,30 @@ } } }, + "/v2/GetCurrentAdminTokenInfo": { + "get": { + "tags": [ + "Admin API token" + ], + "description": "\nReturn information about the calling admin API token.\n ", + "operationId": "GetCurrentAdminTokenInfo", + "responses": { + "200": { + "description": "Information about the admin token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCurrentAdminTokenInfoResponse" + } + } + } + }, + "500": { + "description": "Internal server error" + } + } + } + }, "/v2/GetKeyInfo": { "get": { "tags": [ @@ -1108,30 +1132,6 @@ } } }, - "/v2/GetCurrentAdminTokenInfo": { - "get": { - "tags": [ - "Admin API token" - ], - "description": "\nReturn information about the calling admin API token.\n ", - "operationId": "GetCurrentAdminTokenInfo", - "responses": { - "200": { - "description": "Information about the admin token", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetCurrentAdminTokenInfoResponse" - } - } - } - }, - "500": { - "description": "Internal server error" - } - } - } - }, "/v2/LaunchRepairOperation": { "post": { "tags": [ @@ -2618,6 +2618,9 @@ } } }, + "GetCurrentAdminTokenInfoResponse": { + "$ref": "#/components/schemas/GetAdminTokenInfoResponse" + }, "GetKeyInfoResponse": { "type": "object", "required": [ @@ -2832,54 +2835,6 @@ } } }, - "GetCurrentAdminTokenInfoResponse": { - "type": "object", - "required": [ - "name", - "expired", - "scope" - ], - "properties": { - "created": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Creation date" - }, - "expiration": { - "type": [ - "string", - "null" - ], - "format": "date-time", - "description": "Expiration time and date, formatted according to RFC 3339" - }, - "expired": { - "type": "boolean", - "description": "Whether this admin token is expired already" - }, - "id": { - "type": [ - "string", - "null" - ], - "description": "Identifier of the admin token (which is also a prefix of the full bearer token)" - }, - "name": { - "type": "string", - "description": "Name of the admin API token" - }, - "scope": { - "type": "array", - "items": { - "type": "string" - }, - "description": "Scope of the admin API token, a list of admin endpoint names (such as\n`GetClusterStatus`, etc), or the special value `*` to allow all\nadmin endpoints" - } - } - }, "KeyInfoBucketResponse": { "type": "object", "required": [ diff --git a/src/api/admin/admin_token.rs b/src/api/admin/admin_token.rs index a4fa17ea..0f9c66d2 100644 --- a/src/api/admin/admin_token.rs +++ b/src/api/admin/admin_token.rs @@ -192,14 +192,16 @@ impl RequestHandler for GetCurrentAdminTokenInfoRequest { .as_ref() .is_some_and(|s| s == &self.admin_token) { - return Ok(GetCurrentAdminTokenInfoResponse { - id: None, - created: None, - name: "metrics_token (from daemon configuration)".into(), - expiration: None, - expired: false, - scope: vec!["Metrics".into()], - }); + return Ok(GetCurrentAdminTokenInfoResponse( + GetAdminTokenInfoResponse { + id: None, + created: None, + name: "metrics_token (from daemon configuration)".into(), + expiration: None, + expired: false, + scope: vec!["Metrics".into()], + }, + )); } if garage @@ -209,42 +211,24 @@ impl RequestHandler for GetCurrentAdminTokenInfoRequest { .as_ref() .is_some_and(|s| s == &self.admin_token) { - return Ok(GetCurrentAdminTokenInfoResponse { - id: None, - created: None, - name: "admin_token (from daemon configuration)".into(), - expiration: None, - expired: false, - scope: vec!["*".into()], - }); + return Ok(GetCurrentAdminTokenInfoResponse( + GetAdminTokenInfoResponse { + id: None, + created: None, + name: "admin_token (from daemon configuration)".into(), + expiration: None, + expired: false, + scope: vec!["*".into()], + }, + )); } let (prefix, _) = self.admin_token.split_once('.').unwrap(); + let token = get_existing_admin_token(&garage, &prefix.to_string()).await?; - let candidates = garage - .admin_token_table - .get_range( - &EmptyKey, - None, - Some(KeyFilter::MatchesAndNotDeleted( - prefix.clone().parse().unwrap(), - )), - 10, - EnumerationOrder::Forward, - ) - .await? - .into_iter() - .collect::>(); - if candidates.len() != 1 { - return Err(Error::bad_request(format!( - "{} matching admin tokens", - candidates.len() - ))); - } - Ok(my_admin_token_info_results( - &candidates.into_iter().next().unwrap(), - now, - )) + Ok(GetCurrentAdminTokenInfoResponse(admin_token_info_results( + &token, now, + ))) } } @@ -306,21 +290,3 @@ fn apply_token_updates( Ok(()) } - -fn my_admin_token_info_results(token: &AdminApiToken, now: u64) -> GetCurrentAdminTokenInfoResponse { - let params = token.params().unwrap(); - - GetCurrentAdminTokenInfoResponse { - id: Some(token.prefix.clone()), - created: Some( - DateTime::from_timestamp_millis(params.created as i64) - .expect("invalid timestamp stored in db"), - ), - name: params.name.get().to_string(), - expiration: params.expiration.get().map(|x| { - DateTime::from_timestamp_millis(x as i64).expect("invalid timestamp stored in db") - }), - expired: params.is_expired(now), - scope: params.scope.get().0.clone(), - } -} diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index 1af3b7ba..5524e002 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -399,22 +399,8 @@ pub struct GetCurrentAdminTokenInfoRequest { #[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] -pub struct GetCurrentAdminTokenInfoResponse { - /// Identifier of the admin token (which is also a prefix of the full bearer token) - pub id: Option, - /// Creation date - pub created: Option>, - /// Name of the admin API token - pub name: String, - /// Expiration time and date, formatted according to RFC 3339 - pub expiration: Option>, - /// Whether this admin token is expired already - pub expired: bool, - /// Scope of the admin API token, a list of admin endpoint names (such as - /// `GetClusterStatus`, etc), or the special value `*` to allow all - /// admin endpoints - pub scope: Vec, -} +pub struct GetCurrentAdminTokenInfoResponse(pub GetAdminTokenInfoResponse); + // ********************************************** // Layout operations // ********************************************** diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 9884500b..78d7b251 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -23,8 +23,8 @@ use garage_util::time::now_msec; use garage_api_common::generic_server::*; use garage_api_common::helpers::*; -use crate::api::*; use crate::api::AdminApiRequest::GetCurrentAdminTokenInfo; +use crate::api::*; use crate::error::*; use crate::router_v0; use crate::router_v1; @@ -274,6 +274,7 @@ fn verify_authorization( .get_local(&EmptyKey, &prefix.to_string())? .and_then(|k| k.state.into_option()) .filter(|p| !p.is_expired(now_msec())) + // GetCurrentAdminTokenInfo endpoint must be accessible even if it is not in the token scopes .filter(|p| p.has_scope(endpoint_name) || endpoint_name == "GetCurrentAdminTokenInfo") .ok_or_else(|| Error::forbidden(invalid_msg))? .token_hash