feat(credential): support custom credential ID generation (#1984)

introduces support for custom credential ID generation, allowing users to specify their own credential IDs instead of relying solely on auto-generated UUIDs.
This commit is contained in:
KKRainbow
2026-03-12 00:48:24 +08:00
committed by GitHub
parent 330659e449
commit c8f3c5d6aa
4 changed files with 93 additions and 11 deletions
+9
View File
@@ -355,6 +355,11 @@ enum CredentialSubCommand {
Generate {
#[arg(long, help = "TTL in seconds (required)")]
ttl: i64,
#[arg(
long,
help = "custom credential ID, return existing credential if already generated"
)]
credential_id: Option<String>,
#[arg(long, value_delimiter = ',', help = "ACL groups (comma-separated)")]
groups: Option<Vec<String>>,
#[arg(
@@ -1417,12 +1422,14 @@ impl CommandHandler<'_> {
async fn handle_credential_generate(
&self,
ttl: i64,
credential_id: Option<String>,
groups: Vec<String>,
allow_relay: bool,
allowed_proxy_cidrs: Vec<String>,
) -> Result<(), Error> {
let client = self.get_credential_client().await?;
let request = GenerateCredentialRequest {
credential_id,
groups,
allow_relay,
allowed_proxy_cidrs,
@@ -2362,6 +2369,7 @@ async fn main() -> Result<(), Error> {
SubCommand::Credential(credential_args) => match &credential_args.sub_command {
CredentialSubCommand::Generate {
ttl,
credential_id,
groups,
allow_relay,
allowed_proxy_cidrs,
@@ -2369,6 +2377,7 @@ async fn main() -> Result<(), Error> {
handler
.handle_credential_generate(
*ttl,
credential_id.clone(),
groups.clone().unwrap_or_default(),
*allow_relay,
allowed_proxy_cidrs.clone().unwrap_or_default(),
+74 -5
View File
@@ -15,6 +15,8 @@ use crate::proto::peer_rpc::{TrustedCredentialPubkey, TrustedCredentialPubkeyPro
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CredentialEntry {
pubkey: String,
#[serde(default)]
secret: String,
groups: Vec<String>,
allow_relay: bool,
allowed_proxy_cidrs: Vec<String>,
@@ -44,9 +46,47 @@ impl CredentialManager {
allowed_proxy_cidrs: Vec<String>,
ttl: Duration,
) -> (String, String) {
self.generate_credential_with_id(groups, allow_relay, allowed_proxy_cidrs, ttl, None)
}
pub fn generate_credential_with_id(
&self,
groups: Vec<String>,
allow_relay: bool,
allowed_proxy_cidrs: Vec<String>,
ttl: Duration,
credential_id: Option<String>,
) -> (String, String) {
let mut credentials = self.credentials.lock().unwrap();
let id = if let Some(id) = credential_id
.map(|x| x.trim().to_string())
.filter(|x| !x.is_empty())
{
if let Some(existing) = credentials.get(&id) {
if !existing.secret.is_empty() {
return (id, existing.secret.clone());
}
}
id
} else {
uuid::Uuid::new_v4().to_string()
};
let (entry, secret) = Self::build_entry(groups, allow_relay, allowed_proxy_cidrs, ttl);
credentials.insert(id.clone(), entry);
drop(credentials);
self.save_to_disk();
(id, secret)
}
fn build_entry(
groups: Vec<String>,
allow_relay: bool,
allowed_proxy_cidrs: Vec<String>,
ttl: Duration,
) -> (CredentialEntry, String) {
let private = StaticSecret::random_from_rng(rand::rngs::OsRng);
let public = PublicKey::from(&private);
let id = uuid::Uuid::new_v4().to_string();
let pubkey = BASE64_STANDARD.encode(public.as_bytes());
let secret = BASE64_STANDARD.encode(private.as_bytes());
@@ -58,16 +98,14 @@ impl CredentialManager {
let entry = CredentialEntry {
pubkey,
secret: secret.clone(),
groups,
allow_relay,
allowed_proxy_cidrs,
expiry_unix,
created_at_unix: now,
};
self.credentials.lock().unwrap().insert(id.clone(), entry);
self.save_to_disk();
(id, secret)
(entry, secret)
}
pub fn revoke_credential(&self, credential_id: &str) -> bool {
@@ -404,4 +442,35 @@ mod tests {
let list = mgr.list_credentials();
assert_eq!(list.len(), 1);
}
#[test]
fn test_generate_with_specified_id_reuses_existing_result() {
let mgr = CredentialManager::new(None);
let fixed_id = "fixed-credential-id".to_string();
let (id1, secret1) = mgr.generate_credential_with_id(
vec!["group-a".to_string()],
false,
vec!["10.0.0.0/24".to_string()],
Duration::from_secs(3600),
Some(fixed_id.clone()),
);
let (id2, secret2) = mgr.generate_credential_with_id(
vec!["group-b".to_string()],
true,
vec!["192.168.0.0/16".to_string()],
Duration::from_secs(7200),
Some(fixed_id.clone()),
);
assert_eq!(id1, fixed_id);
assert_eq!(id2, fixed_id);
assert_eq!(secret1, secret2);
let list = mgr.list_credentials();
assert_eq!(list.len(), 1);
assert_eq!(list[0].credential_id, fixed_id);
assert_eq!(list[0].groups, vec!["group-a".to_string()]);
assert!(!list[0].allow_relay);
assert_eq!(list[0].allowed_proxy_cidrs, vec!["10.0.0.0/24".to_string()]);
}
}
+9 -6
View File
@@ -232,12 +232,15 @@ impl CredentialManageRpc for PeerManagerRpcService {
)));
};
let (id, secret) = global_ctx.get_credential_manager().generate_credential(
request.groups,
request.allow_relay,
request.allowed_proxy_cidrs,
ttl,
);
let (id, secret) = global_ctx
.get_credential_manager()
.generate_credential_with_id(
request.groups,
request.allow_relay,
request.allowed_proxy_cidrs,
ttl,
request.credential_id,
);
global_ctx.issue_event(crate::common::global_ctx::GlobalCtxEvent::CredentialChanged);
+1
View File
@@ -300,6 +300,7 @@ message GenerateCredentialRequest {
bool allow_relay = 2; // optional: allow relay through credential node
repeated string allowed_proxy_cidrs = 3; // optional: restrict proxy_cidrs
int64 ttl_seconds = 4; // must be > 0: credential TTL in seconds (0 / omitted is invalid)
optional string credential_id = 5; // optional: user-specified credential id, reused if already exists
}
message GenerateCredentialResponse {