garage_api_admin 2.1.0

Admin API server crate for the Garage object store
Documentation
use std::sync::Arc;

use chrono::{DateTime, Utc};

use garage_table::*;
use garage_util::time::now_msec;

use garage_model::admin_token_table::*;
use garage_model::garage::Garage;

use crate::api::*;
use crate::error::*;
use crate::{Admin, RequestHandler};

impl RequestHandler for ListAdminTokensRequest {
	type Response = ListAdminTokensResponse;

	async fn handle(
		self,
		garage: &Arc<Garage>,
		_admin: &Admin,
	) -> Result<ListAdminTokensResponse, Error> {
		let now = now_msec();

		let mut res = garage
			.admin_token_table
			.get_range(
				&EmptyKey,
				None,
				Some(KeyFilter::Deleted(DeletedFilter::NotDeleted)),
				10000,
				EnumerationOrder::Forward,
			)
			.await?
			.iter()
			.map(|t| admin_token_info_results(t, now))
			.collect::<Vec<_>>();

		if garage.config.admin.metrics_token.is_some() {
			res.insert(
				0,
				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.is_some() {
			res.insert(
				0,
				GetAdminTokenInfoResponse {
					id: None,
					created: None,
					name: "admin_token (from daemon configuration)".into(),
					expiration: None,
					expired: false,
					scope: vec!["*".into()],
				},
			);
		}

		Ok(ListAdminTokensResponse(res))
	}
}

impl RequestHandler for GetAdminTokenInfoRequest {
	type Response = GetAdminTokenInfoResponse;

	async fn handle(
		self,
		garage: &Arc<Garage>,
		_admin: &Admin,
	) -> Result<GetAdminTokenInfoResponse, Error> {
		let token = match (self.id, self.search) {
			(Some(id), None) => get_existing_admin_token(garage, &id).await?,
			(None, Some(search)) => {
				let candidates = garage
					.admin_token_table
					.get_range(
						&EmptyKey,
						None,
						Some(KeyFilter::MatchesAndNotDeleted(search.to_string())),
						10,
						EnumerationOrder::Forward,
					)
					.await?
					.into_iter()
					.collect::<Vec<_>>();
				if candidates.len() != 1 {
					return Err(Error::bad_request(format!(
						"{} matching admin tokens",
						candidates.len()
					)));
				}
				candidates.into_iter().next().unwrap()
			}
			_ => {
				return Err(Error::bad_request(
					"Either id or search must be provided (but not both)",
				));
			}
		};

		Ok(admin_token_info_results(&token, now_msec()))
	}
}

impl RequestHandler for CreateAdminTokenRequest {
	type Response = CreateAdminTokenResponse;

	async fn handle(
		self,
		garage: &Arc<Garage>,
		_admin: &Admin,
	) -> Result<CreateAdminTokenResponse, Error> {
		let (mut token, secret) = if self.0.name.is_some() {
			AdminApiToken::new("")
		} else {
			AdminApiToken::new(&format!("token_{}", Utc::now().format("%Y%m%d_%H%M")))
		};

		apply_token_updates(&mut token, self.0)?;

		garage.admin_token_table.insert(&token).await?;

		Ok(CreateAdminTokenResponse {
			secret_token: secret,
			info: admin_token_info_results(&token, now_msec()),
		})
	}
}

impl RequestHandler for UpdateAdminTokenRequest {
	type Response = UpdateAdminTokenResponse;

	async fn handle(
		self,
		garage: &Arc<Garage>,
		_admin: &Admin,
	) -> Result<UpdateAdminTokenResponse, Error> {
		let mut token = get_existing_admin_token(&garage, &self.id).await?;

		apply_token_updates(&mut token, self.body)?;

		garage.admin_token_table.insert(&token).await?;

		Ok(UpdateAdminTokenResponse(admin_token_info_results(
			&token,
			now_msec(),
		)))
	}
}

impl RequestHandler for DeleteAdminTokenRequest {
	type Response = DeleteAdminTokenResponse;

	async fn handle(
		self,
		garage: &Arc<Garage>,
		_admin: &Admin,
	) -> Result<DeleteAdminTokenResponse, Error> {
		let token = get_existing_admin_token(&garage, &self.id).await?;

		garage
			.admin_token_table
			.insert(&AdminApiToken::delete(token.prefix))
			.await?;

		Ok(DeleteAdminTokenResponse)
	}
}

impl RequestHandler for GetCurrentAdminTokenInfoRequest {
	type Response = GetCurrentAdminTokenInfoResponse;

	async fn handle(
		self,
		garage: &Arc<Garage>,
		_admin: &Admin,
	) -> Result<GetCurrentAdminTokenInfoResponse, Error> {
		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 {
	let params = token.params().unwrap();

	GetAdminTokenInfoResponse {
		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(),
	}
}

async fn get_existing_admin_token(garage: &Garage, id: &String) -> Result<AdminApiToken, Error> {
	garage
		.admin_token_table
		.get(&EmptyKey, id)
		.await?
		.filter(|k| !k.state.is_deleted())
		.ok_or_else(|| Error::NoSuchAdminToken(id.to_string()))
}

fn apply_token_updates(
	token: &mut AdminApiToken,
	updates: UpdateAdminTokenRequestBody,
) -> Result<(), Error> {
	if updates.never_expires && updates.expiration.is_some() {
		return Err(Error::bad_request(
			"cannot specify `expiration` and `never_expires`",
		));
	}

	let params = token.params_mut().unwrap();

	if let Some(name) = updates.name {
		params.name.update(name);
	}
	if let Some(expiration) = updates.expiration {
		params
			.expiration
			.update(Some(expiration.timestamp_millis() as u64));
	}
	if updates.never_expires {
		params.expiration.update(None);
	}
	if let Some(scope) = updates.scope {
		params.scope.update(AdminApiTokenScope(scope));
	}

	Ok(())
}