api: add instrospect endpoint

Fixes #1091
This commit is contained in:
Xavier Stouder 2025-07-01 23:14:09 +02:00
parent f04af18193
commit 7bbb3ff9cf
6 changed files with 228 additions and 0 deletions

View File

@ -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": [

View File

@ -175,6 +175,79 @@ impl RequestHandler for DeleteAdminTokenRequest {
}
}
impl RequestHandler for IntrospectAdminTokenRequest {
type Response = IntrospectAdminTokenResponse;
async fn handle(
self,
garage: &Arc<Garage>,
_admin: &Admin,
) -> Result<IntrospectAdminTokenResponse, Error> {
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::<Vec<_>>();
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(),
}
}

View File

@ -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<String>,
/// Creation date
pub created: Option<DateTime<Utc>>,
/// Name of the admin API token
pub name: String,
/// Expiration time and date, formatted according to RFC 3339
pub expiration: Option<DateTime<Utc>>,
/// 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<String>,
}
// **********************************************
// Layout operations
// **********************************************

View File

@ -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,

View File

@ -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 (),

View File

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