clickup_cli/commands/
acl.rs1use crate::client::ClickUpClient;
2use crate::commands::auth::resolve_token;
3use crate::commands::workspace::resolve_workspace;
4use crate::error::CliError;
5use crate::output::OutputConfig;
6use crate::Cli;
7use clap::Subcommand;
8
9#[derive(Subcommand)]
10pub enum AclCommands {
11 Update {
13 object_type: String,
15 object_id: String,
17 #[arg(long)]
19 private: Option<bool>,
20 #[arg(long = "grant-user")]
22 grant_user: Vec<String>,
23 #[arg(long = "grant-group")]
25 grant_group: Vec<String>,
26 #[arg(long = "revoke-user")]
28 revoke_user: Vec<String>,
29 #[arg(long = "revoke-group")]
31 revoke_group: Vec<String>,
32 #[arg(long)]
34 body: Option<String>,
35 },
36}
37
38fn permission_to_level(name: &str) -> Result<u8, CliError> {
39 match name.to_lowercase().as_str() {
42 "read" | "1" => Ok(1),
43 "comment" | "3" => Ok(3),
44 "edit" | "4" => Ok(4),
45 "create" | "5" => Ok(5),
46 other => Err(CliError::ClientError {
47 message: format!(
48 "Unknown permission '{}'. Valid: read (1), comment (3), edit (4), create (5).",
49 other
50 ),
51 status: 0,
52 }),
53 }
54}
55
56fn parse_grant(raw: &str, kind: &'static str) -> Result<serde_json::Value, CliError> {
57 let (id, level) = match raw.split_once(':') {
59 Some((id, level)) => (id.trim().to_string(), permission_to_level(level.trim())?),
60 None => (raw.trim().to_string(), 1u8),
61 };
62 Ok(serde_json::json!({
63 "kind": kind,
64 "id": id,
65 "permission_level": level,
66 }))
67}
68
69fn revoke_entry(id: &str, kind: &'static str) -> serde_json::Value {
70 serde_json::json!({
74 "kind": kind,
75 "id": id,
76 "permission_level": 0,
77 })
78}
79
80pub async fn execute(command: AclCommands, cli: &Cli) -> Result<(), CliError> {
81 let token = resolve_token(cli)?;
82 let client = ClickUpClient::new(&token, cli.timeout)?;
83 let output = OutputConfig::from_cli(&cli.output, &cli.fields, cli.no_header, cli.quiet);
84
85 match command {
86 AclCommands::Update {
87 object_type,
88 object_id,
89 private,
90 grant_user,
91 grant_group,
92 revoke_user,
93 revoke_group,
94 body,
95 } => {
96 let team_id = resolve_workspace(cli)?;
97
98 let request_body = if let Some(raw) = body {
104 serde_json::from_str(&raw).map_err(|e| CliError::ClientError {
105 message: format!("Invalid JSON body: {}", e),
106 status: 0,
107 })?
108 } else {
109 let mut b = serde_json::Map::new();
110 if let Some(p) = private {
111 b.insert("private".into(), serde_json::Value::Bool(p));
112 }
113 let mut entries: Vec<serde_json::Value> = Vec::new();
114 for raw in grant_user {
115 entries.push(parse_grant(&raw, "user")?);
116 }
117 for raw in grant_group {
118 entries.push(parse_grant(&raw, "group")?);
119 }
120 for id in revoke_user {
121 entries.push(revoke_entry(&id, "user"));
122 }
123 for id in revoke_group {
124 entries.push(revoke_entry(&id, "group"));
125 }
126 if !entries.is_empty() {
127 b.insert("entries".into(), serde_json::Value::Array(entries));
128 }
129 serde_json::Value::Object(b)
130 };
131
132 let resp = client
133 .patch(
134 &format!(
135 "/v3/workspaces/{}/{}/{}/acls",
136 team_id, object_type, object_id
137 ),
138 &request_body,
139 )
140 .await?;
141
142 if cli.output == "json" {
143 println!("{}", serde_json::to_string_pretty(&resp).unwrap());
144 } else {
145 output.print_message(&format!("ACL updated for {} {}", object_type, object_id));
146 }
147 Ok(())
148 }
149 }
150}