Skip to content

Commit 8ff24b7

Browse files
Merge pull request #91 from redis-field-engineering/feat/phase-3-next-priorities
feat: add comprehensive SSO/SAML management and fix Cloud database backup
2 parents 3258075 + 5cf364c commit 8ff24b7

File tree

4 files changed

+341
-6
lines changed

4 files changed

+341
-6
lines changed

.github/workflows/ci.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,16 @@ jobs:
5454
- name: Install Rust
5555
uses: dtolnay/rust-toolchain@stable
5656

57+
- name: Cache cargo
58+
uses: Swatinem/rust-cache@v2
59+
5760
- name: Install tarpaulin
58-
run: cargo install cargo-tarpaulin
61+
uses: taiki-e/install-action@v2
62+
with:
63+
tool: cargo-tarpaulin
5964

6065
- name: Generate coverage
61-
run: cargo tarpaulin --workspace --all-features --out xml
66+
run: cargo tarpaulin --workspace --all-features --out xml --timeout 300
6267

6368
- name: Upload coverage to Codecov
6469
uses: codecov/codecov-action@v3

CLAUDE.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -454,4 +454,5 @@ Based on Redis Enterprise REST API v7+ documentation:
454454
3. Run `cargo test --workspace` to ensure everything is working
455455
4. Check for outdated dependencies: `cargo outdated`
456456
5. Review GitHub issues for priority tasks
457-
- remember to run clippy and fmt before pushing to github
457+
- remember to run clippy and fmt before pushing to github
458+
- these shoulnd't be in prs: 🤖 Generated with Claude Code

crates/redisctl/src/cli.rs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ pub enum CloudCommands {
167167
#[command(subcommand)]
168168
command: PrivateServiceConnectCommands,
169169
},
170+
/// SSO/SAML management
171+
Sso {
172+
#[command(subcommand)]
173+
command: SsoCommands,
174+
},
170175
}
171176

172177
#[derive(Subcommand)]
@@ -1130,3 +1135,128 @@ pub enum PrivateServiceConnectCommands {
11301135
force: bool,
11311136
},
11321137
}
1138+
1139+
#[derive(Subcommand)]
1140+
pub enum SsoCommands {
1141+
/// Show SSO configuration
1142+
Show,
1143+
/// Update SSO configuration
1144+
Update {
1145+
/// Provider name (e.g., okta, azure)
1146+
#[arg(long)]
1147+
provider: String,
1148+
/// SSO login URL
1149+
#[arg(long)]
1150+
login_url: String,
1151+
/// SSO logout URL
1152+
#[arg(long)]
1153+
logout_url: Option<String>,
1154+
/// Enable SSO
1155+
#[arg(long)]
1156+
enabled: Option<bool>,
1157+
},
1158+
/// Delete SSO configuration
1159+
Delete {
1160+
/// Skip confirmation
1161+
#[arg(long)]
1162+
force: bool,
1163+
},
1164+
/// Test SSO configuration
1165+
Test {
1166+
/// Test user email
1167+
#[arg(long)]
1168+
email: String,
1169+
},
1170+
1171+
// SAML specific commands
1172+
/// Show SAML configuration
1173+
SamlShow,
1174+
/// Update SAML configuration
1175+
SamlUpdate {
1176+
/// SAML issuer URL
1177+
#[arg(long)]
1178+
issuer: String,
1179+
/// SAML SSO URL
1180+
#[arg(long)]
1181+
sso_url: String,
1182+
/// SAML certificate content
1183+
#[arg(long)]
1184+
certificate: Option<String>,
1185+
},
1186+
/// Get SAML metadata
1187+
SamlMetadata,
1188+
/// Upload SAML certificate
1189+
SamlUploadCert {
1190+
/// Certificate file path or content
1191+
certificate: String,
1192+
},
1193+
1194+
// User mapping commands
1195+
/// List SSO users
1196+
UserList,
1197+
/// Show SSO user details
1198+
UserShow {
1199+
/// User ID
1200+
id: u32,
1201+
},
1202+
/// Create SSO user mapping
1203+
UserCreate {
1204+
/// SSO user email
1205+
#[arg(long)]
1206+
email: String,
1207+
/// Local user ID to map to
1208+
#[arg(long)]
1209+
local_user_id: u32,
1210+
/// User role
1211+
#[arg(long)]
1212+
role: String,
1213+
},
1214+
/// Update SSO user mapping
1215+
UserUpdate {
1216+
/// User ID
1217+
id: u32,
1218+
/// Local user ID to map to
1219+
#[arg(long)]
1220+
local_user_id: Option<u32>,
1221+
/// User role
1222+
#[arg(long)]
1223+
role: Option<String>,
1224+
},
1225+
/// Delete SSO user mapping
1226+
UserDelete {
1227+
/// User ID
1228+
id: u32,
1229+
/// Skip confirmation
1230+
#[arg(long)]
1231+
force: bool,
1232+
},
1233+
1234+
// Group mapping commands
1235+
/// List SSO groups
1236+
GroupList,
1237+
/// Create SSO group mapping
1238+
GroupCreate {
1239+
/// SSO group name
1240+
#[arg(long)]
1241+
name: String,
1242+
/// Local role to map to
1243+
#[arg(long)]
1244+
role: String,
1245+
},
1246+
/// Update SSO group mapping
1247+
GroupUpdate {
1248+
/// Group ID
1249+
id: u32,
1250+
/// Local role to map to
1251+
#[arg(long)]
1252+
role: String,
1253+
},
1254+
/// Delete SSO group mapping
1255+
GroupDelete {
1256+
/// Group ID
1257+
id: u32,
1258+
/// Skip confirmation
1259+
#[arg(long)]
1260+
force: bool,
1261+
},
1262+
}

crates/redisctl/src/commands/cloud.rs

Lines changed: 202 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use crate::cli::{
66
AccountCommands, AclCommands, ApiKeyCommands, BackupCommands, CloudAccountCommands,
77
CloudCommands, CrdbCommands, DatabaseCommands, FixedPlanCommands, FlexiblePlanCommands,
88
LogsCommands, MetricsCommands, PeeringCommands, PrivateServiceConnectCommands, RegionCommands,
9-
SubscriptionCommands, TaskCommands, TransitGatewayCommands, UserCommands,
9+
SsoCommands, SubscriptionCommands, TaskCommands, TransitGatewayCommands, UserCommands,
1010
};
1111
use crate::commands::api::handle_cloud_api;
1212

@@ -76,6 +76,9 @@ pub async fn handle_cloud_command(
7676
CloudCommands::PrivateServiceConnect { command } => {
7777
handle_private_service_connect_command(command, profile, output_format, query).await
7878
}
79+
CloudCommands::Sso { command } => {
80+
handle_sso_command(command, profile, output_format, query).await
81+
}
7982
}
8083
}
8184

@@ -166,8 +169,21 @@ pub async fn handle_database_command(
166169
.await?;
167170
println!("Database {} deleted successfully", id);
168171
}
169-
DatabaseCommands::Backup { id: _ } => {
170-
anyhow::bail!("Backup operations not yet implemented for Cloud databases");
172+
DatabaseCommands::Backup { id } => {
173+
let (subscription_id, database_id) = parse_database_id(&id)?;
174+
let backup_data = serde_json::json!({
175+
"description": format!("Database backup for {}", id)
176+
});
177+
let task = client
178+
.post_raw(
179+
&format!(
180+
"/subscriptions/{}/databases/{}/backup",
181+
subscription_id, database_id
182+
),
183+
backup_data,
184+
)
185+
.await?;
186+
print_output(task, output_format, query)?;
171187
}
172188
DatabaseCommands::Import { id, url } => {
173189
let (subscription_id, database_id) = parse_database_id(&id)?;
@@ -1414,3 +1430,186 @@ pub async fn handle_private_service_connect_command(
14141430

14151431
Ok(())
14161432
}
1433+
1434+
pub async fn handle_sso_command(
1435+
command: SsoCommands,
1436+
profile: &Profile,
1437+
output_format: OutputFormat,
1438+
query: Option<&str>,
1439+
) -> Result<()> {
1440+
let client = create_cloud_client(profile)?;
1441+
1442+
match command {
1443+
SsoCommands::Show => {
1444+
let sso_config = client.get_raw("/sso").await?;
1445+
print_output(sso_config, output_format, query)?;
1446+
}
1447+
SsoCommands::Update {
1448+
provider,
1449+
login_url,
1450+
logout_url,
1451+
enabled,
1452+
} => {
1453+
let mut update_data = serde_json::json!({
1454+
"provider": provider,
1455+
"loginUrl": login_url
1456+
});
1457+
1458+
if let Some(logout_url) = logout_url {
1459+
update_data["logoutUrl"] = serde_json::Value::String(logout_url);
1460+
}
1461+
if let Some(enabled) = enabled {
1462+
update_data["enabled"] = serde_json::Value::Bool(enabled);
1463+
}
1464+
1465+
let sso_config = client.put_raw("/sso", update_data).await?;
1466+
print_output(sso_config, output_format, query)?;
1467+
}
1468+
SsoCommands::Delete { force } => {
1469+
if !force {
1470+
println!("Are you sure you want to delete SSO configuration? Use --force to skip confirmation.");
1471+
return Ok(());
1472+
}
1473+
client.delete_raw("/sso").await?;
1474+
println!("SSO configuration deleted successfully");
1475+
}
1476+
SsoCommands::Test { email } => {
1477+
let test_data = serde_json::json!({
1478+
"email": email
1479+
});
1480+
let result = client.post_raw("/sso/test", test_data).await?;
1481+
print_output(result, output_format, query)?;
1482+
}
1483+
1484+
// SAML specific commands
1485+
SsoCommands::SamlShow => {
1486+
let saml_config = client.get_raw("/sso/saml").await?;
1487+
print_output(saml_config, output_format, query)?;
1488+
}
1489+
SsoCommands::SamlUpdate {
1490+
issuer,
1491+
sso_url,
1492+
certificate,
1493+
} => {
1494+
let mut update_data = serde_json::json!({
1495+
"issuer": issuer,
1496+
"ssoUrl": sso_url
1497+
});
1498+
1499+
if let Some(certificate) = certificate {
1500+
update_data["certificate"] = serde_json::Value::String(certificate);
1501+
}
1502+
1503+
let saml_config = client.put_raw("/sso/saml", update_data).await?;
1504+
print_output(saml_config, output_format, query)?;
1505+
}
1506+
SsoCommands::SamlMetadata => {
1507+
let metadata = client.get_raw("/sso/saml/metadata").await?;
1508+
print_output(metadata, output_format, query)?;
1509+
}
1510+
SsoCommands::SamlUploadCert { certificate } => {
1511+
let cert_data = serde_json::json!({
1512+
"certificate": certificate
1513+
});
1514+
let result = client.post_raw("/sso/saml/certificate", cert_data).await?;
1515+
print_output(result, output_format, query)?;
1516+
}
1517+
1518+
// User mapping commands
1519+
SsoCommands::UserList => {
1520+
let users = client.get_raw("/sso/users").await?;
1521+
print_output(users, output_format, query)?;
1522+
}
1523+
SsoCommands::UserShow { id } => {
1524+
let user = client.get_raw(&format!("/sso/users/{}", id)).await?;
1525+
print_output(user, output_format, query)?;
1526+
}
1527+
SsoCommands::UserCreate {
1528+
email,
1529+
local_user_id,
1530+
role,
1531+
} => {
1532+
let create_data = serde_json::json!({
1533+
"email": email,
1534+
"localUserId": local_user_id,
1535+
"role": role
1536+
});
1537+
let user = client.post_raw("/sso/users", create_data).await?;
1538+
print_output(user, output_format, query)?;
1539+
}
1540+
SsoCommands::UserUpdate {
1541+
id,
1542+
local_user_id,
1543+
role,
1544+
} => {
1545+
let mut update_data = serde_json::Map::new();
1546+
if let Some(local_user_id) = local_user_id {
1547+
update_data.insert(
1548+
"localUserId".to_string(),
1549+
serde_json::Value::Number(local_user_id.into()),
1550+
);
1551+
}
1552+
if let Some(role) = role {
1553+
update_data.insert("role".to_string(), serde_json::Value::String(role));
1554+
}
1555+
if update_data.is_empty() {
1556+
anyhow::bail!("No update fields provided");
1557+
}
1558+
1559+
let user = client
1560+
.put_raw(
1561+
&format!("/sso/users/{}", id),
1562+
serde_json::Value::Object(update_data),
1563+
)
1564+
.await?;
1565+
print_output(user, output_format, query)?;
1566+
}
1567+
SsoCommands::UserDelete { id, force } => {
1568+
if !force {
1569+
println!(
1570+
"Are you sure you want to delete SSO user mapping {}? Use --force to skip confirmation.",
1571+
id
1572+
);
1573+
return Ok(());
1574+
}
1575+
client.delete_raw(&format!("/sso/users/{}", id)).await?;
1576+
println!("SSO user mapping {} deleted successfully", id);
1577+
}
1578+
1579+
// Group mapping commands
1580+
SsoCommands::GroupList => {
1581+
let groups = client.get_raw("/sso/groups").await?;
1582+
print_output(groups, output_format, query)?;
1583+
}
1584+
SsoCommands::GroupCreate { name, role } => {
1585+
let create_data = serde_json::json!({
1586+
"name": name,
1587+
"role": role
1588+
});
1589+
let group = client.post_raw("/sso/groups", create_data).await?;
1590+
print_output(group, output_format, query)?;
1591+
}
1592+
SsoCommands::GroupUpdate { id, role } => {
1593+
let update_data = serde_json::json!({
1594+
"role": role
1595+
});
1596+
let group = client
1597+
.put_raw(&format!("/sso/groups/{}", id), update_data)
1598+
.await?;
1599+
print_output(group, output_format, query)?;
1600+
}
1601+
SsoCommands::GroupDelete { id, force } => {
1602+
if !force {
1603+
println!(
1604+
"Are you sure you want to delete SSO group mapping {}? Use --force to skip confirmation.",
1605+
id
1606+
);
1607+
return Ok(());
1608+
}
1609+
client.delete_raw(&format!("/sso/groups/{}", id)).await?;
1610+
println!("SSO group mapping {} deleted successfully", id);
1611+
}
1612+
}
1613+
1614+
Ok(())
1615+
}

0 commit comments

Comments
 (0)