diff --git a/influxdb3/Cargo.toml b/influxdb3/Cargo.toml index 1d1ba43d62c..2f08e61a766 100644 --- a/influxdb3/Cargo.toml +++ b/influxdb3/Cargo.toml @@ -55,6 +55,7 @@ libc.workspace = true num_cpus.workspace = true parking_lot.workspace = true rand.workspace = true +reqwest.workspace = true rustls.workspace = true secrecy.workspace = true serde.workspace = true diff --git a/influxdb3/src/commands/delete.rs b/influxdb3/src/commands/delete.rs index 87a4b62f0fd..f0d0aead0f2 100644 --- a/influxdb3/src/commands/delete.rs +++ b/influxdb3/src/commands/delete.rs @@ -293,6 +293,13 @@ pub async fn command(config: Config) -> Result<(), Box> { println!("Trigger {} deleted successfully", trigger_name); } SubCommand::Token(TokenConfig { token_name, .. }) => { + if token_name == "_admin" { + println!( + "The operator token \"_admin\" is required and cannot be deleted. To regenerate an operator token, use: influxdb3 create token --admin --regenerate --token [TOKEN]" + ); + return Ok(()); + } + println!( "Are you sure you want to delete {:?}? Enter 'yes' to confirm", token_name @@ -303,7 +310,6 @@ pub async fn command(config: Config) -> Result<(), Box> { println!("Cannot delete token without confirmation"); } else { client.api_v3_configure_token_delete(&token_name).await?; - println!("Token {:?} deleted successfully", &token_name); } } diff --git a/influxdb3/tests/cli/mod.rs b/influxdb3/tests/cli/mod.rs index 1fc8a42a7fe..0fa1e3ebbe0 100644 --- a/influxdb3/tests/cli/mod.rs +++ b/influxdb3/tests/cli/mod.rs @@ -2,7 +2,7 @@ mod api; use crate::server::{ConfigProvider, TestServer, parse_token}; use assert_cmd::Command as AssertCmd; -use observability_deps::tracing::{debug, info}; +use observability_deps::tracing::debug; use pretty_assertions::assert_eq; use serde_json::{Value, json}; use std::fs::File; @@ -2991,7 +2991,7 @@ async fn test_create_admin_token_allowed_once() { .unwrap(); assert_contains!( &result, - "Failed to create token, error: ApiError { code: 500, message: \"token name already exists, _admin\" }" + "Failed to create token, error: ApiError { code: 409, message: \"token name already exists, _admin\" }" ); } @@ -3006,7 +3006,7 @@ async fn test_regenerate_admin_token() { // already has admin token, so it cannot be created again assert_contains!( &result, - "Failed to create token, error: ApiError { code: 500, message: \"token name already exists, _admin\" }" + "Failed to create token, error: ApiError { code: 409, message: \"token name already exists, _admin\" }" ); // regenerating token is allowed @@ -3060,7 +3060,7 @@ async fn test_delete_token() { let token = parse_token(result); let result = server - .run_with_confirmation( + .run( vec!["delete", "token"], &[ "--token-name", @@ -3072,7 +3072,10 @@ async fn test_delete_token() { ], ) .unwrap(); - info!(result, "test: deleted token using token name"); + assert_contains!( + result, + "The operator token \"_admin\" is required and cannot be deleted. To regenerate an operator token, use: influxdb3 create token --admin --regenerate --token [TOKEN]" + ); // you should be able to create the token again let result = server @@ -3087,7 +3090,10 @@ async fn test_delete_token() { args, ) .unwrap(); - assert_contains!(&result, "New token created successfully!"); + assert_contains!( + &result, + "Failed to create token, error: ApiError { code: 409, message: \"token name already exists, _admin\" }" + ); } #[test_log::test(tokio::test)] diff --git a/influxdb3/tests/server/configure.rs b/influxdb3/tests/server/configure.rs index 52b9e49d7ed..1833399bc64 100644 --- a/influxdb3/tests/server/configure.rs +++ b/influxdb3/tests/server/configure.rs @@ -1,5 +1,4 @@ use hyper::StatusCode; -use influxdb3_types::http::CreateTokenWithPermissionsResponse; use observability_deps::tracing::{debug, info}; use pretty_assertions::assert_eq; use serde_json::{Value, json}; @@ -1365,7 +1364,6 @@ async fn api_v3_configure_token_delete() { let delete_url = format!("{base}/api/v3/configure/token", base = server.client_addr()); let admin_token = server.token().expect("admin token to be present"); - let delete_result = client .delete(&delete_url) .bearer_auth(admin_token) @@ -1374,32 +1372,12 @@ async fn api_v3_configure_token_delete() { .await .unwrap(); info!(?delete_result, "test: result running the token delete"); + assert_eq!(delete_result.status(), StatusCode::METHOD_NOT_ALLOWED); - // create admin token again - let result = client.post(&create_url).send().await.unwrap(); - info!(?result, "test: result running the create token"); - assert_eq!(result.status(), StatusCode::CREATED); - let json: CreateTokenWithPermissionsResponse = result.json().await.unwrap(); - info!(?json, "test: result running the token delete"); - assert_eq!(json.id, 1); - - // delete again - let delete_result = client - .delete(&delete_url) - .bearer_auth(&json.token) - .query(&[("token_name", token_name)]) - .send() - .await - .unwrap(); - info!(?delete_result, "test: result running the token delete"); - - // create admin token once again + // create admin token again - this will fail as operator token already exists let result = client.post(&create_url).send().await.unwrap(); info!(?result, "test: result running the create token"); - assert_eq!(result.status(), StatusCode::CREATED); - let json: CreateTokenWithPermissionsResponse = result.json().await.unwrap(); - info!(?json, "test: result running the token delete"); - assert_eq!(json.id, 2); + assert_eq!(result.status(), StatusCode::CONFLICT); } #[test_log::test(tokio::test)] diff --git a/influxdb3_catalog/src/catalog.rs b/influxdb3_catalog/src/catalog.rs index b73a12caf98..a2fa16c1012 100644 --- a/influxdb3_catalog/src/catalog.rs +++ b/influxdb3_catalog/src/catalog.rs @@ -64,7 +64,7 @@ pub const TIME_COLUMN_NAME: &str = "time"; pub const INTERNAL_DB_NAME: &str = "_internal"; -const DEFAULT_ADMIN_TOKEN_NAME: &str = "_admin"; +const DEFAULT_OPERATOR_TOKEN_NAME: &str = "_admin"; /// Limit for the number of tag columns on a table pub(crate) const NUM_TAG_COLUMNS_LIMIT: usize = 250; @@ -463,7 +463,7 @@ impl Catalog { .read() .tokens .repo() - .get_by_name(DEFAULT_ADMIN_TOKEN_NAME); + .get_by_name(DEFAULT_OPERATOR_TOKEN_NAME); if default_admin_token.is_none() { return Err(CatalogError::MissingAdminTokenToUpdate); @@ -487,10 +487,10 @@ impl Catalog { .read() .tokens .repo() - .contains_name(DEFAULT_ADMIN_TOKEN_NAME) + .contains_name(DEFAULT_OPERATOR_TOKEN_NAME) { return Err(CatalogError::TokenNameAlreadyExists( - DEFAULT_ADMIN_TOKEN_NAME.to_owned(), + DEFAULT_OPERATOR_TOKEN_NAME.to_owned(), )); } @@ -506,7 +506,7 @@ impl Catalog { time_ns: created_at, ops: vec![TokenCatalogOp::CreateAdminToken(CreateAdminTokenDetails { token_id, - name: Arc::from(DEFAULT_ADMIN_TOKEN_NAME), + name: Arc::from(DEFAULT_OPERATOR_TOKEN_NAME), hash: hash.clone(), created_at, updated_at: None, @@ -522,7 +522,7 @@ impl Catalog { .read() .tokens .repo() - .get_by_name(DEFAULT_ADMIN_TOKEN_NAME) + .get_by_name(DEFAULT_OPERATOR_TOKEN_NAME) .expect("token info must be present after token creation by name") }; diff --git a/influxdb3_catalog/src/catalog/update.rs b/influxdb3_catalog/src/catalog/update.rs index d7332ccf53a..556a85ef14f 100644 --- a/influxdb3_catalog/src/catalog/update.rs +++ b/influxdb3_catalog/src/catalog/update.rs @@ -13,7 +13,7 @@ use super::{ }; use crate::{ CatalogError, Result, - catalog::NUM_TAG_COLUMNS_LIMIT, + catalog::{DEFAULT_OPERATOR_TOKEN_NAME, NUM_TAG_COLUMNS_LIMIT}, log::{ AddFieldsLog, CatalogBatch, CreateDatabaseLog, CreateTableLog, DatabaseCatalogOp, DeleteDistinctCacheLog, DeleteLastCacheLog, DeleteTokenDetails, DeleteTriggerLog, @@ -681,6 +681,11 @@ impl Catalog { pub async fn delete_token(&self, token_name: &str) -> Result { info!(token_name, "delete token"); + + if token_name == DEFAULT_OPERATOR_TOKEN_NAME { + return Err(CatalogError::CannotDeleteOperatorToken); + } + self.catalog_update_with_retry(|| { if !self.inner.read().tokens.repo().contains_name(token_name) { // maybe deleted by another node or genuinely not present diff --git a/influxdb3_catalog/src/error.rs b/influxdb3_catalog/src/error.rs index 004f0b9e215..a92200c0bc2 100644 --- a/influxdb3_catalog/src/error.rs +++ b/influxdb3_catalog/src/error.rs @@ -175,6 +175,9 @@ pub enum CatalogError { #[error("tried to stop a node ({node_id}) that is already stopped")] NodeAlreadyStopped { node_id: Arc }, + + #[error("cannot delete operator token")] + CannotDeleteOperatorToken, } impl CatalogError { diff --git a/influxdb3_server/src/http.rs b/influxdb3_server/src/http.rs index 6c259398e45..05edb16a2ec 100644 --- a/influxdb3_server/src/http.rs +++ b/influxdb3_server/src/http.rs @@ -310,6 +310,14 @@ impl IntoResponse for Error { fn into_response(self) -> Response { debug!(error = ?self, "API error"); match self { + Self::Catalog(err @ CatalogError::CannotDeleteOperatorToken) => Response::builder() + .status(StatusCode::METHOD_NOT_ALLOWED) + .body(Body::from(err.to_string())) + .unwrap(), + Self::Catalog(err @ CatalogError::TokenNameAlreadyExists { .. }) => Response::builder() + .status(StatusCode::CONFLICT) + .body(Body::from(err.to_string())) + .unwrap(), Self::Catalog(err) | Self::WriteBuffer(WriteBufferError::CatalogUpdateError(err)) => { err.into_response() }