use anyhow::{Context, Result};
use clap::{Parser, Subcommand, ValueEnum};
use crate::atlassian::confluence_api::ConfluenceApi;
use crate::cli::atlassian::helpers::create_client;
#[derive(Parser)]
pub struct RestrictionCommand {
#[command(subcommand)]
pub command: RestrictionSubcommands,
}
#[derive(Subcommand)]
pub enum RestrictionSubcommands {
Get(GetCommand),
Grant(SubjectCommand),
Revoke(SubjectCommand),
}
impl RestrictionCommand {
pub async fn execute(self) -> Result<()> {
match self.command {
RestrictionSubcommands::Get(cmd) => cmd.execute().await,
RestrictionSubcommands::Grant(cmd) => cmd.execute(true).await,
RestrictionSubcommands::Revoke(cmd) => cmd.execute(false).await,
}
}
}
#[derive(Copy, Clone, Debug, ValueEnum)]
pub enum RestrictionOperation {
Read,
Update,
}
impl RestrictionOperation {
fn as_str(self) -> &'static str {
match self {
Self::Read => "read",
Self::Update => "update",
}
}
}
#[derive(Parser)]
pub struct GetCommand {
pub id: String,
}
impl GetCommand {
async fn execute(self) -> Result<()> {
let (client, _instance_url) = create_client()?;
let api = ConfluenceApi::new(client);
let value = api.get_content_restrictions(&self.id).await?;
let yaml =
serde_yaml::to_string(&value).context("Failed to serialize restrictions as YAML")?;
print!("{yaml}");
Ok(())
}
}
#[derive(Parser)]
pub struct SubjectCommand {
pub id: String,
#[arg(long, value_enum)]
pub operation: RestrictionOperation,
#[arg(long, conflicts_with = "group")]
pub account_id: Option<String>,
#[arg(long, conflicts_with = "account_id")]
pub group: Option<String>,
}
impl SubjectCommand {
async fn execute(self, grant: bool) -> Result<()> {
let (client, _instance_url) = create_client()?;
let api = ConfluenceApi::new(client);
let op = self.operation.as_str();
if grant {
api.grant_content_restriction(
&self.id,
op,
self.account_id.as_deref(),
self.group.as_deref(),
)
.await?;
println!(
"Granted {op} restriction on page {} to {}.",
self.id,
self.subject()
);
} else {
api.revoke_content_restriction(
&self.id,
op,
self.account_id.as_deref(),
self.group.as_deref(),
)
.await?;
println!(
"Revoked {op} restriction on page {} from {}.",
self.id,
self.subject()
);
}
Ok(())
}
fn subject(&self) -> String {
match (&self.account_id, &self.group) {
(Some(a), _) => format!("user {a}"),
(_, Some(g)) => format!("group {g}"),
_ => "(none)".to_string(),
}
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn restriction_command_get_variant() {
let cmd = RestrictionCommand {
command: RestrictionSubcommands::Get(GetCommand {
id: "12345".to_string(),
}),
};
assert!(matches!(cmd.command, RestrictionSubcommands::Get(_)));
}
#[test]
fn restriction_command_grant_variant() {
let cmd = RestrictionCommand {
command: RestrictionSubcommands::Grant(SubjectCommand {
id: "12345".to_string(),
operation: RestrictionOperation::Read,
account_id: Some("acc-1".to_string()),
group: None,
}),
};
assert!(matches!(cmd.command, RestrictionSubcommands::Grant(_)));
}
#[test]
fn operation_as_str_maps() {
assert_eq!(RestrictionOperation::Read.as_str(), "read");
assert_eq!(RestrictionOperation::Update.as_str(), "update");
}
#[test]
fn subject_labels() {
let user = SubjectCommand {
id: "1".to_string(),
operation: RestrictionOperation::Read,
account_id: Some("acc".to_string()),
group: None,
};
assert_eq!(user.subject(), "user acc");
let group = SubjectCommand {
id: "1".to_string(),
operation: RestrictionOperation::Read,
account_id: None,
group: Some("devs".to_string()),
};
assert_eq!(group.subject(), "group devs");
}
#[test]
fn subject_label_without_account_or_group() {
let neither = SubjectCommand {
id: "1".to_string(),
operation: RestrictionOperation::Update,
account_id: None,
group: None,
};
assert_eq!(neither.subject(), "(none)");
}
#[tokio::test]
async fn get_execute_prints_restrictions() {
use crate::test_support::atlassian_env::AtlassianEnvGuard;
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path(
"/wiki/rest/api/content/12345/restriction",
))
.respond_with(
wiremock::ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"results": [{"operation": "read"}]})),
)
.mount(&server)
.await;
let _env = AtlassianEnvGuard::new(&server.uri(), "u@t.com", "tok");
GetCommand {
id: "12345".to_string(),
}
.execute()
.await
.unwrap();
}
#[tokio::test]
async fn grant_execute_puts_user_restriction() {
use crate::test_support::atlassian_env::AtlassianEnvGuard;
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("PUT"))
.and(wiremock::matchers::path(
"/wiki/rest/api/content/12345/restriction/byOperation/update/user",
))
.respond_with(wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({})))
.mount(&server)
.await;
let _env = AtlassianEnvGuard::new(&server.uri(), "u@t.com", "tok");
RestrictionCommand {
command: RestrictionSubcommands::Grant(SubjectCommand {
id: "12345".to_string(),
operation: RestrictionOperation::Update,
account_id: Some("acc-1".to_string()),
group: None,
}),
}
.execute()
.await
.unwrap();
}
#[tokio::test]
async fn revoke_execute_deletes_group_restriction() {
use crate::test_support::atlassian_env::AtlassianEnvGuard;
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("DELETE"))
.and(wiremock::matchers::path(
"/wiki/rest/api/content/12345/restriction/byOperation/read/group/devs",
))
.respond_with(wiremock::ResponseTemplate::new(204))
.mount(&server)
.await;
let _env = AtlassianEnvGuard::new(&server.uri(), "u@t.com", "tok");
RestrictionCommand {
command: RestrictionSubcommands::Revoke(SubjectCommand {
id: "12345".to_string(),
operation: RestrictionOperation::Read,
account_id: None,
group: Some("devs".to_string()),
}),
}
.execute()
.await
.unwrap();
}
}