diff --git a/doc/api/garage-admin-v2.json b/doc/api/garage-admin-v2.json index d9b2622a..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": [ @@ -2594,6 +2618,9 @@ } } }, + "GetCurrentAdminTokenInfoResponse": { + "$ref": "#/components/schemas/GetAdminTokenInfoResponse" + }, "GetKeyInfoResponse": { "type": "object", "required": [ @@ -4382,4 +4409,4 @@ "bearerAuth": [] } ] -} +} \ No newline at end of file diff --git a/src/api/admin/admin_token.rs b/src/api/admin/admin_token.rs index ac937eea..0f9c66d2 100644 --- a/src/api/admin/admin_token.rs +++ b/src/api/admin/admin_token.rs @@ -175,6 +175,63 @@ impl RequestHandler for DeleteAdminTokenRequest { } } +impl RequestHandler for GetCurrentAdminTokenInfoRequest { + type Response = GetCurrentAdminTokenInfoResponse; + + 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(GetCurrentAdminTokenInfoResponse( + GetAdminTokenInfoResponse { + 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(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?; + + Ok(GetCurrentAdminTokenInfoResponse(admin_token_info_results( + &token, now, + ))) + } +} + // ---- helpers ---- fn admin_token_info_results(token: &AdminApiToken, now: u64) -> GetAdminTokenInfoResponse { diff --git a/src/api/admin/api.rs b/src/api/admin/api.rs index d2faa618..5524e002 100644 --- a/src/api/admin/api.rs +++ b/src/api/admin/api.rs @@ -56,6 +56,7 @@ admin_endpoints![ CreateAdminToken, UpdateAdminToken, DeleteAdminToken, + GetCurrentAdminTokenInfo, // Layout operations GetClusterLayout, @@ -391,6 +392,15 @@ pub struct DeleteAdminTokenRequest { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeleteAdminTokenResponse; +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GetCurrentAdminTokenInfoRequest { + pub admin_token: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)] +#[serde(rename_all = "camelCase")] +pub struct GetCurrentAdminTokenInfoResponse(pub GetAdminTokenInfoResponse); + // ********************************************** // Layout operations // ********************************************** diff --git a/src/api/admin/api_server.rs b/src/api/admin/api_server.rs index 14029423..78d7b251 100644 --- a/src/api/admin/api_server.rs +++ b/src/api/admin/api_server.rs @@ -23,6 +23,7 @@ use garage_util::time::now_msec; use garage_api_common::generic_server::*; use garage_api_common::helpers::*; +use crate::api::AdminApiRequest::GetCurrentAdminTokenInfo; use crate::api::*; use crate::error::*; use crate::router_v0; @@ -273,7 +274,8 @@ 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)) + // 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 } else { diff --git a/src/api/admin/openapi.rs b/src/api/admin/openapi.rs index 890bfd6c..248fd29f 100644 --- a/src/api/admin/openapi.rs +++ b/src/api/admin/openapi.rs @@ -191,6 +191,19 @@ fn UpdateAdminToken() -> () {} )] fn DeleteAdminToken() -> () {} +#[utoipa::path(get, + 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 = GetCurrentAdminTokenInfoResponse), + (status = 500, description = "Internal server error") + ), +)] +fn GetCurrentAdminTokenInfo() -> () {} + // ********************************************** // Layout operations // ********************************************** @@ -872,6 +885,7 @@ impl Modify for SecurityAddon { CreateAdminToken, UpdateAdminToken, DeleteAdminToken, + GetCurrentAdminTokenInfo, // Layout operations GetClusterLayout, GetClusterLayoutHistory, diff --git a/src/api/admin/router_v2.rs b/src/api/admin/router_v2.rs index 3051dae4..3009d128 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 GetCurrentAdminTokenInfo (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) => {{