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) => {{