feat: flow add static user/pwd auth (#6048)

* feat: flow add static user/pwd auth

* fix: not print password

* chore: rm explict Any bound

* refactor: per review

* refactor: move away from plugin

* refactor: not use any

* chore: per revieww

* chore: complete a todo

* chore: fix after rebase
This commit is contained in:
discord9
2025-05-07 13:20:37 +08:00
committed by GitHub
parent 4b5ab75312
commit cce1285b16
17 changed files with 197 additions and 18 deletions

2
Cargo.lock generated
View File

@@ -4264,6 +4264,7 @@ dependencies = [
"arrow-schema 54.3.1",
"async-recursion",
"async-trait",
"auth",
"bytes",
"cache",
"catalog",
@@ -8586,6 +8587,7 @@ dependencies = [
"common-base",
"common-error",
"datanode",
"flow",
"frontend",
"meta-srv",
"serde",

View File

@@ -36,7 +36,7 @@ pub fn userinfo_by_name(username: Option<String>) -> UserInfoRef {
}
pub fn user_provider_from_option(opt: &String) -> Result<UserProviderRef> {
let (name, content) = opt.split_once(':').context(InvalidConfigSnafu {
let (name, content) = opt.split_once(':').with_context(|| InvalidConfigSnafu {
value: opt.to_string(),
msg: "UserProviderOption must be in format `<option>:<value>`",
})?;
@@ -57,6 +57,24 @@ pub fn user_provider_from_option(opt: &String) -> Result<UserProviderRef> {
}
}
pub fn static_user_provider_from_option(opt: &String) -> Result<StaticUserProvider> {
let (name, content) = opt.split_once(':').with_context(|| InvalidConfigSnafu {
value: opt.to_string(),
msg: "UserProviderOption must be in format `<option>:<value>`",
})?;
match name {
STATIC_USER_PROVIDER => {
let provider = StaticUserProvider::new(content)?;
Ok(provider)
}
_ => InvalidConfigSnafu {
value: name.to_string(),
msg: format!("Invalid UserProviderOption, expect only {STATIC_USER_PROVIDER}"),
}
.fail(),
}
}
type Username<'a> = &'a str;
type HostOrIp<'a> = &'a str;

View File

@@ -38,6 +38,14 @@ pub enum Error {
location: Location,
},
#[snafu(display("Failed to convert to utf8"))]
FromUtf8 {
#[snafu(source)]
error: std::string::FromUtf8Error,
#[snafu(implicit)]
location: Location,
},
#[snafu(display("Authentication source failure"))]
AuthBackend {
#[snafu(implicit)]
@@ -85,7 +93,7 @@ impl ErrorExt for Error {
fn status_code(&self) -> StatusCode {
match self {
Error::InvalidConfig { .. } => StatusCode::InvalidArguments,
Error::IllegalParam { .. } => StatusCode::InvalidArguments,
Error::IllegalParam { .. } | Error::FromUtf8 { .. } => StatusCode::InvalidArguments,
Error::FileWatch { .. } => StatusCode::InvalidArguments,
Error::InternalState { .. } => StatusCode::Unexpected,
Error::Io { .. } => StatusCode::StorageUnavailable,

View File

@@ -22,10 +22,12 @@ mod user_provider;
pub mod tests;
pub use common::{
auth_mysql, user_provider_from_option, userinfo_by_name, HashedPassword, Identity, Password,
auth_mysql, static_user_provider_from_option, user_provider_from_option, userinfo_by_name,
HashedPassword, Identity, Password,
};
pub use permission::{PermissionChecker, PermissionReq, PermissionResp};
pub use user_info::UserInfo;
pub use user_provider::static_user_provider::StaticUserProvider;
pub use user_provider::UserProvider;
/// pub type alias

View File

@@ -15,15 +15,15 @@
use std::collections::HashMap;
use async_trait::async_trait;
use snafu::OptionExt;
use snafu::{OptionExt, ResultExt};
use crate::error::{InvalidConfigSnafu, Result};
use crate::error::{FromUtf8Snafu, InvalidConfigSnafu, Result};
use crate::user_provider::{authenticate_with_credential, load_credential_from_file};
use crate::{Identity, Password, UserInfoRef, UserProvider};
pub(crate) const STATIC_USER_PROVIDER: &str = "static_user_provider";
pub(crate) struct StaticUserProvider {
pub struct StaticUserProvider {
users: HashMap<String, Vec<u8>>,
}
@@ -60,6 +60,18 @@ impl StaticUserProvider {
.fail(),
}
}
/// Return a random username/password pair
/// This is useful for invoking from other components in the cluster
pub fn get_one_user_pwd(&self) -> Result<(String, String)> {
let kv = self.users.iter().next().context(InvalidConfigSnafu {
value: "",
msg: "Expect at least one pair of username and password",
})?;
let username = kv.0;
let pwd = String::from_utf8(kv.1.clone()).context(FromUtf8Snafu)?;
Ok((username.clone(), pwd))
}
}
#[async_trait]

View File

@@ -33,7 +33,8 @@ use common_telemetry::info;
use common_telemetry::logging::TracingOptions;
use common_version::{short_version, version};
use flow::{
FlownodeBuilder, FlownodeInstance, FlownodeServiceBuilder, FrontendClient, FrontendInvoker,
get_flow_auth_options, FlownodeBuilder, FlownodeInstance, FlownodeServiceBuilder,
FrontendClient, FrontendInvoker,
};
use meta_client::{MetaClientOptions, MetaClientType};
use snafu::{ensure, OptionExt, ResultExt};
@@ -82,6 +83,10 @@ impl App for Instance {
}
async fn start(&mut self) -> Result<()> {
plugins::start_flownode_plugins(self.flownode.flow_engine().plugins().clone())
.await
.context(StartFlownodeSnafu)?;
self.flownode.start().await.context(StartFlownodeSnafu)
}
@@ -151,6 +156,9 @@ struct StartCommand {
/// HTTP request timeout in seconds.
#[clap(long)]
http_timeout: Option<u64>,
/// User Provider cfg, for auth, currently only support static user provider
#[clap(long)]
user_provider: Option<String>,
}
impl StartCommand {
@@ -214,6 +222,10 @@ impl StartCommand {
opts.http.timeout = Duration::from_secs(http_timeout);
}
if let Some(user_provider) = &self.user_provider {
opts.user_provider = Some(user_provider.clone());
}
ensure!(
opts.node_id.is_some(),
MissingConfigSnafu {
@@ -238,9 +250,15 @@ impl StartCommand {
info!("Flownode start command: {:#?}", self);
info!("Flownode options: {:#?}", opts);
let plugin_opts = opts.plugins;
let mut opts = opts.component;
opts.grpc.detect_server_addr();
let mut plugins = Plugins::new();
plugins::setup_flownode_plugins(&mut plugins, &plugin_opts, &opts)
.await
.context(StartFlownodeSnafu)?;
let member_id = opts
.node_id
.context(MissingConfigSnafu { msg: "'node_id'" })?;
@@ -315,10 +333,12 @@ impl StartCommand {
);
let flow_metadata_manager = Arc::new(FlowMetadataManager::new(cached_meta_backend.clone()));
let frontend_client = FrontendClient::from_meta_client(meta_client.clone());
let flow_auth_header = get_flow_auth_options(&opts).context(StartFlownodeSnafu)?;
let frontend_client =
FrontendClient::from_meta_client(meta_client.clone(), flow_auth_header);
let flownode_builder = FlownodeBuilder::new(
opts.clone(),
Plugins::new(),
plugins,
table_metadata_manager,
catalog_manager.clone(),
flow_metadata_manager,

View File

@@ -13,6 +13,7 @@ arrow.workspace = true
arrow-schema.workspace = true
async-recursion = "1.0"
async-trait.workspace = true
auth.workspace = true
bytes.workspace = true
cache.workspace = true
catalog.workspace = true

View File

@@ -107,6 +107,7 @@ pub struct FlownodeOptions {
pub tracing: TracingOptions,
pub heartbeat: HeartbeatOptions,
pub query: QueryOptions,
pub user_provider: Option<String>,
}
impl Default for FlownodeOptions {
@@ -121,6 +122,7 @@ impl Default for FlownodeOptions {
tracing: TracingOptions::default(),
heartbeat: HeartbeatOptions::default(),
query: QueryOptions::default(),
user_provider: None,
}
}
}

View File

@@ -21,6 +21,7 @@ use api::v1::flow::{
};
use api::v1::region::InsertRequests;
use catalog::CatalogManager;
use common_base::Plugins;
use common_error::ext::BoxedError;
use common_meta::ddl::create_flow::FlowType;
use common_meta::error::Result as MetaResult;
@@ -63,6 +64,7 @@ pub struct FlowDualEngine {
flow_metadata_manager: Arc<FlowMetadataManager>,
catalog_manager: Arc<dyn CatalogManager>,
check_task: tokio::sync::Mutex<Option<ConsistentCheckTask>>,
plugins: Plugins,
}
impl FlowDualEngine {
@@ -71,6 +73,7 @@ impl FlowDualEngine {
batching_engine: Arc<BatchingEngine>,
flow_metadata_manager: Arc<FlowMetadataManager>,
catalog_manager: Arc<dyn CatalogManager>,
plugins: Plugins,
) -> Self {
Self {
streaming_engine,
@@ -79,9 +82,14 @@ impl FlowDualEngine {
flow_metadata_manager,
catalog_manager,
check_task: Mutex::new(None),
plugins,
}
}
pub fn plugins(&self) -> &Plugins {
&self.plugins
}
/// Determine if the engine is in distributed mode
pub fn is_distributed(&self) -> bool {
self.streaming_engine.node_id.is_some()

View File

@@ -38,7 +38,7 @@ use crate::batching_mode::{
GRPC_MAX_RETRIES,
};
use crate::error::{ExternalSnafu, InvalidRequestSnafu, NoAvailableFrontendSnafu, UnexpectedSnafu};
use crate::Error;
use crate::{Error, FlowAuthHeader};
/// Just like [`GrpcQueryHandler`] but use BoxedError
///
@@ -81,6 +81,7 @@ pub enum FrontendClient {
Distributed {
meta_client: Arc<MetaClient>,
chnl_mgr: ChannelManager,
auth: Option<FlowAuthHeader>,
},
Standalone {
/// for the sake of simplicity still use grpc even in standalone mode
@@ -101,7 +102,8 @@ impl FrontendClient {
)
}
pub fn from_meta_client(meta_client: Arc<MetaClient>) -> Self {
pub fn from_meta_client(meta_client: Arc<MetaClient>, auth: Option<FlowAuthHeader>) -> Self {
common_telemetry::info!("Frontend client build with auth={:?}", auth);
Self::Distributed {
meta_client,
chnl_mgr: {
@@ -110,6 +112,7 @@ impl FrontendClient {
.timeout(DEFAULT_BATCHING_ENGINE_QUERY_TIMEOUT);
ChannelManager::with_config(cfg)
},
auth,
}
}
@@ -186,6 +189,7 @@ impl FrontendClient {
let Self::Distributed {
meta_client: _,
chnl_mgr,
auth,
} = self
else {
return UnexpectedSnafu {
@@ -216,7 +220,13 @@ impl FrontendClient {
{
let addr = &node_info.peer.addr;
let client = Client::with_manager_and_urls(chnl_mgr.clone(), vec![addr.clone()]);
let database = Database::new(catalog, schema, client);
let database = {
let mut db = Database::new(catalog, schema, client);
if let Some(auth) = auth {
db.set_auth(auth.auth().clone());
}
db
};
let db = DatabaseWithPeer::new(database, node_info.peer.clone());
match db.try_select_one().await {
Ok(_) => return Ok(db),

View File

@@ -25,6 +25,42 @@ use crate::Error;
pub type FlowId = u64;
pub type TableName = [String; 3];
#[derive(Clone)]
pub struct FlowAuthHeader {
auth_schema: api::v1::auth_header::AuthScheme,
}
impl std::fmt::Debug for FlowAuthHeader {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.auth() {
api::v1::auth_header::AuthScheme::Basic(basic) => f
.debug_struct("Basic")
.field("username", &basic.username)
.field("password", &"<RETRACTED>")
.finish(),
api::v1::auth_header::AuthScheme::Token(_) => f
.debug_struct("Token")
.field("token", &"<RETRACTED>")
.finish(),
}
}
}
impl FlowAuthHeader {
pub fn from_user_pwd(username: &str, pwd: &str) -> Self {
Self {
auth_schema: api::v1::auth_header::AuthScheme::Basic(api::v1::Basic {
username: username.to_string(),
password: pwd.to_string(),
}),
}
}
pub fn auth(&self) -> &api::v1::auth_header::AuthScheme {
&self.auth_schema
}
}
/// The arguments to create a flow
#[derive(Debug, Clone)]
pub struct CreateFlowArgs {

View File

@@ -152,6 +152,9 @@ pub enum Error {
location: Location,
},
#[snafu(display("Invalid auth config"))]
IllegalAuthConfig { source: auth::error::Error },
#[snafu(display("Flow plan error: {reason}"))]
Plan {
reason: String,
@@ -330,9 +333,10 @@ impl ErrorExt for Error {
}
Self::MetaClientInit { source, .. } => source.status_code(),
Self::InvalidQuery { .. } | Self::InvalidRequest { .. } | Self::ParseAddr { .. } => {
StatusCode::InvalidArguments
}
Self::InvalidQuery { .. }
| Self::InvalidRequest { .. }
| Self::ParseAddr { .. }
| Self::IllegalAuthConfig { .. } => StatusCode::InvalidArguments,
Error::SubstraitEncodeLogicalPlan { source, .. } => source.status_code(),

View File

@@ -45,8 +45,10 @@ mod test_utils;
pub use adapter::{FlowConfig, FlowStreamingEngineRef, FlownodeOptions, StreamingEngine};
pub use batching_mode::frontend_client::{FrontendClient, GrpcQueryHandlerWithBoxedError};
pub use engine::FlowAuthHeader;
pub(crate) use engine::{CreateFlowArgs, FlowId, TableName};
pub use error::{Error, Result};
pub use server::{
FlownodeBuilder, FlownodeInstance, FlownodeServer, FlownodeServiceBuilder, FrontendInvoker,
get_flow_auth_options, FlownodeBuilder, FlownodeInstance, FlownodeServer,
FlownodeServiceBuilder, FrontendInvoker,
};

View File

@@ -57,13 +57,16 @@ use crate::batching_mode::engine::BatchingEngine;
use crate::engine::FlowEngine;
use crate::error::{
to_status_with_last_err, CacheRequiredSnafu, CreateFlowSnafu, ExternalSnafu, FlowNotFoundSnafu,
ListFlowsSnafu, ParseAddrSnafu, ShutdownServerSnafu, StartServerSnafu, UnexpectedSnafu,
IllegalAuthConfigSnafu, ListFlowsSnafu, ParseAddrSnafu, ShutdownServerSnafu, StartServerSnafu,
UnexpectedSnafu,
};
use crate::heartbeat::HeartbeatTask;
use crate::metrics::{METRIC_FLOW_PROCESSING_TIME, METRIC_FLOW_ROWS};
use crate::transform::register_function_to_query_engine;
use crate::utils::{SizeReportSender, StateReportHandler};
use crate::{CreateFlowArgs, Error, FlownodeOptions, FrontendClient, StreamingEngine};
use crate::{
CreateFlowArgs, Error, FlowAuthHeader, FlownodeOptions, FrontendClient, StreamingEngine,
};
pub const FLOW_NODE_SERVER_NAME: &str = "FLOW_NODE_SERVER";
/// wrapping flow node manager to avoid orphan rule with Arc<...>
@@ -310,6 +313,21 @@ impl FlownodeInstance {
}
}
pub fn get_flow_auth_options(fn_opts: &FlownodeOptions) -> Result<Option<FlowAuthHeader>, Error> {
if let Some(user_provider) = fn_opts.user_provider.as_ref() {
let static_provider = auth::static_user_provider_from_option(user_provider)
.context(IllegalAuthConfigSnafu)?;
let (usr, pwd) = static_provider
.get_one_user_pwd()
.context(IllegalAuthConfigSnafu)?;
let auth_header = FlowAuthHeader::from_user_pwd(&usr, &pwd);
return Ok(Some(auth_header));
}
Ok(None)
}
/// [`FlownodeInstance`] Builder
pub struct FlownodeBuilder {
opts: FlownodeOptions,
@@ -383,6 +401,7 @@ impl FlownodeBuilder {
batching,
self.flow_metadata_manager.clone(),
self.catalog_manager.clone(),
self.plugins.clone(),
);
let server = FlownodeServer::new(FlowService::new(Arc::new(dual)));

View File

@@ -14,6 +14,7 @@ cli.workspace = true
common-base.workspace = true
common-error.workspace = true
datanode.workspace = true
flow.workspace = true
frontend.workspace = true
meta-srv.workspace = true
serde.workspace = true

View File

@@ -0,0 +1,32 @@
// Copyright 2023 Greptime Team
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use common_base::Plugins;
use flow::error::Result;
use flow::FlownodeOptions;
use crate::options::PluginOptions;
#[allow(unused_mut)]
pub async fn setup_flownode_plugins(
_plugins: &mut Plugins,
_plugin_options: &[PluginOptions],
_fn_opts: &FlownodeOptions,
) -> Result<()> {
Ok(())
}
pub async fn start_flownode_plugins(_plugins: Plugins) -> Result<()> {
Ok(())
}

View File

@@ -14,12 +14,14 @@
mod cli;
mod datanode;
mod flownode;
mod frontend;
mod meta_srv;
mod options;
pub use cli::SubCommand;
pub use datanode::{setup_datanode_plugins, start_datanode_plugins};
pub use flownode::{setup_flownode_plugins, start_flownode_plugins};
pub use frontend::{setup_frontend_plugins, start_frontend_plugins};
pub use meta_srv::{setup_metasrv_plugins, start_metasrv_plugins};
pub use options::PluginOptions;