diff --git a/Cargo.lock b/Cargo.lock index f28ea2043..baf07d50b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3513,6 +3513,7 @@ dependencies = [ "tokio", "tonic", "utils", + "xline", "xline-client", "xlineapi", ] diff --git a/xline-client/src/types/auth.rs b/xline-client/src/types/auth.rs index bd92da448..7f1b1b8ee 100644 --- a/xline-client/src/types/auth.rs +++ b/xline-client/src/types/auth.rs @@ -213,7 +213,7 @@ impl From for xlineapi::AuthUserRevokeRoleRequest { } /// Request for `AuthRoleAdd` -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct AuthRoleAddRequest { /// Inner request pub(crate) inner: xlineapi::AuthRoleAddRequest, @@ -239,7 +239,7 @@ impl From for xlineapi::AuthRoleAddRequest { } /// Request for `AuthRoleGet` -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct AuthRoleGetRequest { /// Inner request pub(crate) inner: xlineapi::AuthRoleGetRequest, @@ -265,7 +265,7 @@ impl From for xlineapi::AuthRoleGetRequest { } /// Request for `AuthRoleDelete` -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct AuthRoleDeleteRequest { /// Inner request pub(crate) inner: xlineapi::AuthRoleDeleteRequest, @@ -291,7 +291,7 @@ impl From for xlineapi::AuthRoleDeleteRequest { } /// Request for `AuthRoleGrantPermission` -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct AuthRoleGrantPermissionRequest { /// Inner request pub(crate) inner: xlineapi::AuthRoleGrantPermissionRequest, @@ -321,7 +321,7 @@ impl From for xlineapi::AuthRoleGrantPermissionR } /// Request for `AuthRoleRevokePermission` -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct AuthRoleRevokePermissionRequest { /// Inner request pub(crate) inner: xlineapi::AuthRoleRevokePermissionRequest, diff --git a/xlinectl/Cargo.toml b/xlinectl/Cargo.toml index 95036e3d5..16c3bb0f9 100644 --- a/xlinectl/Cargo.toml +++ b/xlinectl/Cargo.toml @@ -19,5 +19,6 @@ thiserror = "1.0.37" tokio = { version = "1", features = ["rt"] } tonic = "0.9.2" utils = { path = "../utils" } +xline = { path = "../xline" } xline-client = { path = "../xline-client" } xlineapi = { path = "../xlineapi" } diff --git a/xlinectl/README.md b/xlinectl/README.md index ab681d8a2..1284ea918 100644 --- a/xlinectl/README.md +++ b/xlinectl/README.md @@ -436,18 +436,165 @@ auth status ``` ### ROLE +Role related commands ### ROLE ADD +Create a new role + +#### Usage + +```bash +add +``` + +#### Output + +``` +Role added +``` + +#### Examples + +```bash +# add a new role named 'foo' +./xlinectl --user=root:root role add foo +Role added +``` ### ROLE GET +List role information + +#### Usage + +```bash +get +``` + +#### Output + +``` +Permmision: + +[range_end0] +Permmision: + +[range_end1] +... +``` + +#### Examples + +```bash +./xlinectl --user=root:root grant_perm foo Read key +./xlinectl --user=root:root grant_perm foo ReadWrite key1 +# Get role named 'foo' +./xlinectl --user=root:root role get foo +Permission: Read +key +Permission: Read +key1 +``` ### ROLE DELETE +Delete a role + +#### Usage + +```bash +delete +``` + +#### Output + +``` +Role deleted +``` + +#### Examples +```bash +./xlinectl --user=root:root role add foo +# delete the role named `foo` +./xlinectl --user=root:root role delete foo +Role deleted +``` ### ROLE LIST +List all roles + +#### Usage + +```bash +list +``` + +#### Output + +``` + + +... +``` + +#### Examples + +```bash +./xlinectl --user=root:root role add foo +./xlinectl --user=root:root role add foo1 +# list all roles +./xlinectl --user=root:root role list +foo +foo1 +``` ### ROLE GRANT-PERMISSION +Grant permission to a role, including Read, Write or ReadWrite + +#### Usage + +```bash +grant_perm [options] [range_end] +``` + +#### Options +- prefix -- Get keys with matching prefix +- from_key -- Get keys that are greater than or equal to the given key using byte compare (conflicts with `range_end`) + +#### Output + +``` +Permission granted +``` + +#### Examples + +```bash +# Grant read permission to role 'foo' for key 'bar' with read permission +./xlinectl --user=root:root role grant_perm foo READ bar +Permission granted +``` ### ROLE REVOKE-PERMISSION +Revoke permission from a role + +#### Usage + +```bash +revoke_perm [range_end] +``` + +#### Output + +``` +Permission revoked +``` + +#### Examples + +```bash +# Revoke permission from role 'foo' for the range from 'bar' to 'bar2' +./xlinectl --user=root:root role revoke_perm foo bar bar2 +Permission revoked +``` ### USER @@ -502,8 +649,8 @@ get [options] #### Examples ```bash -./etcdctl --user=root:root user grant_role foo role0 -./etcdctl --user=root:root user grant_role foo role1 +./xlinectl --user=root:root user grant_role foo role0 +./xlinectl --user=root:root user grant_role foo role1 # Get a user named `foo` ./xlinectl --user=root:root user get foo role0 diff --git a/xlinectl/src/command/mod.rs b/xlinectl/src/command/mod.rs index 6cd7d33ef..7ca7b65af 100644 --- a/xlinectl/src/command/mod.rs +++ b/xlinectl/src/command/mod.rs @@ -8,6 +8,8 @@ pub(crate) mod get; pub(crate) mod lease; /// Put command pub(crate) mod put; +/// Role command +pub(crate) mod role; /// Snapshot command pub(crate) mod snapshot; /// User command diff --git a/xlinectl/src/command/role/add.rs b/xlinectl/src/command/role/add.rs new file mode 100644 index 000000000..e52c7b5e8 --- /dev/null +++ b/xlinectl/src/command/role/add.rs @@ -0,0 +1,46 @@ +use clap::{arg, ArgMatches, Command}; +use xline_client::{error::Result, types::auth::AuthRoleAddRequest, Client}; + +use crate::utils::printer::Printer; + +/// Definition of `add` command +pub(super) fn command() -> Command { + Command::new("add") + .about("Create a new role") + .arg(arg!( "The name of the role")) +} + +/// Build request from matches +pub(super) fn build_request(matches: &ArgMatches) -> AuthRoleAddRequest { + let name = matches.get_one::("name").expect("required"); + AuthRoleAddRequest::new(name) +} + +/// Execute the command +pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { + let req = build_request(matches); + let resp = client.auth_client().role_add(req).await?; + resp.print(); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_case_struct; + + test_case_struct!(AuthRoleAddRequest); + + #[test] + fn valid() { + let test_cases = vec![TestCase::new( + vec!["add", "Admin"], + Some(AuthRoleAddRequest::new("Admin")), + )]; + + for case in test_cases { + case.run_test(); + } + } +} diff --git a/xlinectl/src/command/role/delete.rs b/xlinectl/src/command/role/delete.rs new file mode 100644 index 000000000..c210d7cf2 --- /dev/null +++ b/xlinectl/src/command/role/delete.rs @@ -0,0 +1,46 @@ +use clap::{arg, ArgMatches, Command}; +use xline_client::{error::Result, types::auth::AuthRoleDeleteRequest, Client}; + +use crate::utils::printer::Printer; + +/// Definition of `delete` command +pub(super) fn command() -> Command { + Command::new("delete") + .about("delete a role") + .arg(arg!( "The name of the role")) +} + +/// Build request from matches +pub(super) fn build_request(matches: &ArgMatches) -> AuthRoleDeleteRequest { + let name = matches.get_one::("name").expect("required"); + AuthRoleDeleteRequest::new(name) +} + +/// Execute the command +pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { + let req = build_request(matches); + let resp = client.auth_client().role_delete(req).await?; + resp.print(); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_case_struct; + + test_case_struct!(AuthRoleDeleteRequest); + + #[test] + fn valid() { + let test_cases = vec![TestCase::new( + vec!["delete", "Admin"], + Some(AuthRoleDeleteRequest::new("Admin")), + )]; + + for case in test_cases { + case.run_test(); + } + } +} diff --git a/xlinectl/src/command/role/get.rs b/xlinectl/src/command/role/get.rs new file mode 100644 index 000000000..6da8842c3 --- /dev/null +++ b/xlinectl/src/command/role/get.rs @@ -0,0 +1,46 @@ +use clap::{arg, ArgMatches, Command}; +use xline_client::{error::Result, types::auth::AuthRoleGetRequest, Client}; + +use crate::utils::printer::Printer; + +/// Definition of `get` command +pub(super) fn command() -> Command { + Command::new("get") + .about("Get a new role") + .arg(arg!( "The name of the role")) +} + +/// Build request from matches +pub(super) fn build_request(matches: &ArgMatches) -> AuthRoleGetRequest { + let name = matches.get_one::("name").expect("required"); + AuthRoleGetRequest::new(name) +} + +/// Execute the command +pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { + let req = build_request(matches); + let resp = client.auth_client().role_get(req).await?; + resp.print(); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_case_struct; + + test_case_struct!(AuthRoleGetRequest); + + #[test] + fn valid() { + let test_cases = vec![TestCase::new( + vec!["get", "Admin"], + Some(AuthRoleGetRequest::new("Admin")), + )]; + + for case in test_cases { + case.run_test(); + } + } +} diff --git a/xlinectl/src/command/role/grant_perm.rs b/xlinectl/src/command/role/grant_perm.rs new file mode 100644 index 000000000..6f2e019ee --- /dev/null +++ b/xlinectl/src/command/role/grant_perm.rs @@ -0,0 +1,98 @@ +use clap::{arg, ArgMatches, Command}; +use xline_client::{ + error::Result, + types::auth::{AuthRoleGrantPermissionRequest, Permission}, + Client, +}; +use xlineapi::Type; + +use crate::utils::printer::Printer; + +/// Definition of `grant_perm` command +pub(super) fn command() -> Command { + Command::new("grant_perm") + .about("Grant permission to a role") + .arg(arg!( "The name of the role")) + .arg(arg!( "The type of the permission").value_parser(["Read", "Write", "ReadWrite"])) + .arg(arg!( "The Key")) + .arg(arg!([range_end] "Range end of the key")) + .arg(arg!(--prefix "Get keys with matching prefix")) + .arg( + arg!(--from_key "Get keys that are greater than or equal to the given key using byte compare") + .conflicts_with("range_end") + ) +} + +/// Build request from matches +pub(super) fn build_request(matches: &ArgMatches) -> AuthRoleGrantPermissionRequest { + let name = matches.get_one::("name").expect("required"); + let perm_type_local = matches.get_one::("perm_type").expect("required"); + let key = matches.get_one::("key").expect("required"); + let range_end = matches.get_one::("range_end"); + let prefix = matches.get_flag("prefix"); + let from_key = matches.get_flag("from_key"); + + let perm_type = match perm_type_local.as_str() { + "Read" => Type::Read, + "Write" => Type::Write, + "ReadWrite" => Type::Readwrite, + _ => unreachable!("should be checked by clap"), + }; + + let mut perm = Permission::new(perm_type, key.as_bytes()); + + if let Some(range_end) = range_end { + perm = perm.with_range_end(range_end.as_bytes()); + }; + + if prefix { + perm = perm.with_prefix(); + } + + if from_key { + perm = perm.with_from_key(); + } + + AuthRoleGrantPermissionRequest::new(name, perm) +} + +/// Execute the command +pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { + let req = build_request(matches); + let resp = client.auth_client().role_grant_permission(req).await?; + resp.print(); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_case_struct; + + test_case_struct!(AuthRoleGrantPermissionRequest); + + #[test] + fn valid() { + let test_cases = vec![ + TestCase::new( + vec!["grant_perm", "Admin", "Read", "key1", "key2"], + Some(AuthRoleGrantPermissionRequest::new( + "Admin", + Permission::new(Type::Read, "key1").with_range_end("key2"), + )), + ), + TestCase::new( + vec!["grant_perm", "Admin", "Write", "key3", "--from_key"], + Some(AuthRoleGrantPermissionRequest::new( + "Admin", + Permission::new(Type::Write, "key3").with_from_key(), + )), + ), + ]; + + for case in test_cases { + case.run_test(); + } + } +} diff --git a/xlinectl/src/command/role/list.rs b/xlinectl/src/command/role/list.rs new file mode 100644 index 000000000..b48f8dd26 --- /dev/null +++ b/xlinectl/src/command/role/list.rs @@ -0,0 +1,17 @@ +use clap::{ArgMatches, Command}; +use xline_client::{error::Result, Client}; + +use crate::utils::printer::Printer; + +/// Definition of `list` command +pub(super) fn command() -> Command { + Command::new("list").about("List all roles") +} + +/// Execute the command +pub(super) async fn execute(client: &mut Client, _matches: &ArgMatches) -> Result<()> { + let resp = client.auth_client().role_list().await?; + resp.print(); + + Ok(()) +} diff --git a/xlinectl/src/command/role/mod.rs b/xlinectl/src/command/role/mod.rs new file mode 100644 index 000000000..b89b23d11 --- /dev/null +++ b/xlinectl/src/command/role/mod.rs @@ -0,0 +1,36 @@ +use clap::{ArgMatches, Command}; +use xline_client::{error::Result, Client}; + +use crate::handle_matches; + +/// Role add command +pub(super) mod add; +/// Role delete command +pub(super) mod delete; +/// Role get command +pub(super) mod get; +/// Role grant permission command +pub(super) mod grant_perm; +/// Role list command +pub(super) mod list; +/// Role revoke permission command +pub(super) mod revoke_perm; + +/// Definition of `auth` command +pub(crate) fn command() -> Command { + Command::new("role") + .about("Role related commands") + .subcommand(add::command()) + .subcommand(delete::command()) + .subcommand(get::command()) + .subcommand(grant_perm::command()) + .subcommand(list::command()) + .subcommand(revoke_perm::command()) +} + +/// Execute the command +pub(crate) async fn execute(mut client: &mut Client, matches: &ArgMatches) -> Result<()> { + handle_matches!(matches, client, { add, delete, get, grant_perm, list, revoke_perm }); + + Ok(()) +} diff --git a/xlinectl/src/command/role/revoke_perm.rs b/xlinectl/src/command/role/revoke_perm.rs new file mode 100644 index 000000000..9c9717495 --- /dev/null +++ b/xlinectl/src/command/role/revoke_perm.rs @@ -0,0 +1,63 @@ +use clap::{arg, ArgMatches, Command}; +use xline_client::{error::Result, types::auth::AuthRoleRevokePermissionRequest, Client}; + +use crate::utils::printer::Printer; + +/// Definition of `revoke_perm` command +pub(super) fn command() -> Command { + Command::new("revoke_perm") + .about("Revoke permission from a role") + .arg(arg!( "The name of the role")) + .arg(arg!( "The Key")) + .arg(arg!([range_end] "Range end of the key")) +} + +/// Build request from matches +pub(super) fn build_request(matches: &ArgMatches) -> AuthRoleRevokePermissionRequest { + let name = matches.get_one::("name").expect("required"); + let key = matches.get_one::("key").expect("required"); + let range_end = matches.get_one::("range_end"); + + let mut request = AuthRoleRevokePermissionRequest::new(name, key.as_bytes()); + + if let Some(range_end) = range_end { + request = request.with_range_end(range_end.as_bytes()); + }; + + request +} + +/// Execute the command +pub(super) async fn execute(client: &mut Client, matches: &ArgMatches) -> Result<()> { + let req = build_request(matches); + let resp = client.auth_client().role_revoke_permission(req).await?; + resp.print(); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_case_struct; + + test_case_struct!(AuthRoleRevokePermissionRequest); + + #[test] + fn valid() { + let test_cases = vec![ + TestCase::new( + vec!["revoke_perm", "Admin", "key1", "key2"], + Some(AuthRoleRevokePermissionRequest::new("Admin", "key1").with_range_end("key2")), + ), + TestCase::new( + vec!["revoke_perm", "Admin", "key3"], + Some(AuthRoleRevokePermissionRequest::new("Admin", "key3")), + ), + ]; + + for case in test_cases { + case.run_test(); + } + } +} diff --git a/xlinectl/src/main.rs b/xlinectl/src/main.rs index 33e52a203..bf0f98848 100644 --- a/xlinectl/src/main.rs +++ b/xlinectl/src/main.rs @@ -159,7 +159,7 @@ use ext_utils::config::ClientConfig; use xline_client::{Client, ClientOptions}; use crate::{ - command::{auth, delete, get, lease, put, snapshot, user}, + command::{auth, delete, get, lease, put, role, snapshot, user}, utils::{ parser::parse_user, printer::{set_printer_type, PrinterType}, @@ -232,6 +232,7 @@ fn cli() -> Command { .subcommand(snapshot::command()) .subcommand(auth::command()) .subcommand(user::command()) + .subcommand(role::command()) } #[tokio::main] @@ -258,6 +259,6 @@ async fn main() -> Result<()> { set_printer_type(printer_type); let mut client = Client::connect(endpoints, options).await?; - handle_matches!(matches, client, { get, put, delete, lease, snapshot, auth, user }); + handle_matches!(matches, client, { get, put, delete, lease, snapshot, auth, user, role }); Ok(()) } diff --git a/xlinectl/src/utils/printer.rs b/xlinectl/src/utils/printer.rs index 3aa383ea3..00379b435 100644 --- a/xlinectl/src/utils/printer.rs +++ b/xlinectl/src/utils/printer.rs @@ -1,12 +1,13 @@ use std::sync::OnceLock; use xlineapi::{ - AuthDisableResponse, AuthEnableResponse, AuthRoleGetResponse, AuthStatusResponse, - AuthUserAddResponse, AuthUserChangePasswordResponse, AuthUserDeleteResponse, - AuthUserGetResponse, AuthUserGrantRoleResponse, AuthUserListResponse, - AuthUserRevokeRoleResponse, DeleteRangeResponse, KeyValue, LeaseGrantResponse, - LeaseKeepAliveResponse, LeaseLeasesResponse, LeaseRevokeResponse, LeaseTimeToLiveResponse, - PutResponse, RangeResponse, ResponseHeader, + AuthDisableResponse, AuthEnableResponse, AuthRoleAddResponse, AuthRoleDeleteResponse, + AuthRoleGetResponse, AuthRoleGrantPermissionResponse, AuthRoleListResponse, + AuthRoleRevokePermissionResponse, AuthStatusResponse, AuthUserAddResponse, + AuthUserChangePasswordResponse, AuthUserDeleteResponse, AuthUserGetResponse, + AuthUserGrantRoleResponse, AuthUserListResponse, AuthUserRevokeRoleResponse, + DeleteRangeResponse, KeyValue, LeaseGrantResponse, LeaseKeepAliveResponse, LeaseLeasesResponse, + LeaseRevokeResponse, LeaseTimeToLiveResponse, PutResponse, RangeResponse, ResponseHeader, }; /// The global printer type config @@ -237,18 +238,39 @@ impl Printer for AuthUserGetResponse { } impl Printer for AuthRoleGetResponse { - fn simple(&self) {} + fn simple(&self) { + for perm in &self.perm { + println!("Permission: {}", perm_type(perm.perm_type)); + SimplePrinter::utf8(&perm.key); + if !perm.range_end.is_empty() { + SimplePrinter::utf8(&perm.range_end); + } + } + } fn field(&self) { FieldPrinter::header(self.header.as_ref()); for perm in &self.perm { - println!("perm type: {}", perm.perm_type); + println!("perm type: {}", perm_type(perm.perm_type)); FieldPrinter::key(&perm.key); - FieldPrinter::range_end(&perm.range_end); + if !perm.range_end.is_empty() { + FieldPrinter::range_end(&perm.range_end); + } } } } +/// Convert perm type to string +fn perm_type(perm: i32) -> String { + match perm { + 0 => "Read", + 1 => "Write", + 2 => "ReadWrite", + _ => "Unknown", + } + .to_owned() +} + impl Printer for AuthUserGrantRoleResponse { fn simple(&self) { println!("Role granted"); @@ -298,6 +320,65 @@ impl Printer for AuthUserRevokeRoleResponse { } } +impl Printer for AuthRoleAddResponse { + fn simple(&self) { + println!("Role added"); + } + + fn field(&self) { + FieldPrinter::header(self.header.as_ref()); + println!("Role added"); + } +} + +impl Printer for AuthRoleDeleteResponse { + fn simple(&self) { + println!("Role deleted"); + } + + fn field(&self) { + FieldPrinter::header(self.header.as_ref()); + println!("Role deleted"); + } +} + +impl Printer for AuthRoleGrantPermissionResponse { + fn simple(&self) { + println!("Permission granted"); + } + + fn field(&self) { + FieldPrinter::header(self.header.as_ref()); + println!("Permission granted"); + } +} + +impl Printer for AuthRoleListResponse { + fn simple(&self) { + for role in &self.roles { + println!("{role}"); + } + } + + fn field(&self) { + FieldPrinter::header(self.header.as_ref()); + for role in &self.roles { + println!("{role}"); + } + } +} + +impl Printer for AuthRoleRevokePermissionResponse { + fn simple(&self) { + println!("Permission revoked"); + } + + fn field(&self) { + FieldPrinter::header(self.header.as_ref()); + println!("Permission revoked"); + } +} + /// Simple Printer of common response types struct SimplePrinter;