1use std::io::Write as _;
17
18use clap::{Args, Parser, Subcommand, ValueEnum};
19use mkit_attest::grant::{Visibility, VisibilityStatement};
20use mkit_transport_connect::{VisibilityChoice, VisibilityRequest};
21
22use crate::clap_shim;
23use crate::commands::{error, usage_error};
24use crate::exit;
25use crate::grants::cli::{Ctx, finish_wait, parse_timeout, print_statement};
26use crate::grants::now_ms;
27use crate::grants::owner::{Kind, OwnerArgs, Produced, produce};
28use crate::grants::remote::{Driven, check_audiences, drive, interruptible_sleep, resolve_target};
29use crate::grants::spec::{build_visibility, canonical_audiences, statement_lifetime_ms};
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
32enum VisibilityArg {
33 Public,
34 Private,
35}
36
37#[derive(Debug, Parser)]
38#[command(
39 name = "mkit visibility",
40 about = "Switch a repository between public and private."
41)]
42struct VisibilityOpts {
43 #[command(subcommand)]
44 command: VisibilityCommand,
45}
46
47#[derive(Debug, Subcommand)]
48enum VisibilityCommand {
49 Set(SetOpts),
51}
52
53#[derive(Debug, Args)]
54struct SetOpts {
55 remote: String,
57 #[arg(value_enum)]
59 visibility: VisibilityArg,
60 #[arg(long)]
63 statement: bool,
64 #[arg(long, value_name = "ORIGIN", requires = "statement")]
67 audience: Vec<String>,
68 #[arg(long, value_name = "DURATION", default_value = "5m")]
70 timeout: String,
71 #[command(flatten)]
72 owner: OwnerArgs,
73}
74
75#[must_use]
76pub fn run(args: &[String]) -> u8 {
77 let opts = match clap_shim::parse::<VisibilityOpts>("mkit visibility", args) {
78 Ok(opts) => opts,
79 Err(code) => return code,
80 };
81 match opts.command {
82 VisibilityCommand::Set(opts) => set(&opts),
83 }
84}
85
86fn owner_flags_used(owner: &OwnerArgs) -> bool {
87 owner.scheme.is_some()
88 || owner.print_statement
89 || owner.statement_file.is_some()
90 || owner.signature.is_some()
91 || owner.webauthn_assertion.is_some()
92}
93
94#[allow(clippy::too_many_lines)] fn set(opts: &SetOpts) -> u8 {
96 let ctx = match Ctx::load() {
97 Ok(ctx) => ctx,
98 Err(code) => return code,
99 };
100 let timeout = match parse_timeout(&opts.timeout) {
101 Ok(t) => t,
102 Err(e) => return usage_error(&format!("--timeout: {e}")),
103 };
104 if !opts.statement && owner_flags_used(&opts.owner) {
105 return usage_error(
106 "--scheme, --print-statement, --statement-file, --signature and --webauthn-assertion sign a statement: add --statement",
107 );
108 }
109 let target = match resolve_target(&ctx.layered, Some(&opts.remote)) {
110 Ok(target) => target,
111 Err(e) => return usage_error(&e),
112 };
113 let choice = match opts.visibility {
114 VisibilityArg::Public => VisibilityChoice::Public,
115 VisibilityArg::Private => VisibilityChoice::Private,
116 };
117
118 let outcome = if opts.statement {
119 let tx = match ctx.open_unsigned(&target) {
120 Ok(tx) => tx,
121 Err(e) => return error(&e, exit::UNAVAILABLE),
122 };
123 let repository = tx.repository().clone();
124 let Some(namespace) = repository.namespace().copied() else {
125 return usage_error(
126 "a visibility statement names a full <namespace>/<name> repository: use a remote URL that includes it",
127 );
128 };
129 let audiences = if opts.audience.is_empty() {
130 vec![tx.origin().to_owned()]
131 } else {
132 match canonical_audiences(&opts.audience) {
133 Ok(a) => a,
134 Err(e) => return usage_error(&e),
135 }
136 };
137 if let Err(e) = check_audiences(&audiences, Some(&target)) {
138 return error(&e, exit::USAGE);
139 }
140 let plan = match ctx.plan(&opts.owner, Some(namespace)) {
141 Ok(plan) => plan,
142 Err(e) => return error(&e, exit::USAGE),
143 };
144 let wanted = match opts.visibility {
145 VisibilityArg::Public => Visibility::Public,
146 VisibilityArg::Private => Visibility::Private,
147 };
148 let now = now_ms();
149 let lifetime_ms = statement_lifetime_ms(timeout);
150 let signed = match produce(
151 plan,
152 |_| {
153 build_visibility(&repository, wanted, &audiences, now, lifetime_ms)?
154 .encode()
155 .map_err(crate::grants::spec::statement_error)
156 },
157 Kind::Visibility(&repository),
158 &ctx.relying_parties,
159 now,
160 ) {
161 Ok(Produced::Signed(signed)) => signed,
162 Ok(Produced::Print {
163 statement,
164 namespace,
165 }) => return print_statement(&statement, &namespace),
166 Err(e) => return error(&e, exit::DATAERR),
167 };
168 match VisibilityStatement::parse(&signed.statement) {
169 Ok(s) if s.repository == repository && s.visibility == wanted => {
170 if let Err(e) = check_audiences(&s.audiences, Some(&target)) {
171 return error(&e, exit::USAGE);
172 }
173 }
174 Ok(_) => {
175 return error(
176 "the imported statement is for a different repository or visibility than the command asks for",
177 exit::DATAERR,
178 );
179 }
180 Err(e) => return error(&format!("invalid statement: {e}"), exit::DATAERR),
181 }
182 drive(
183 || tx.set_repo_visibility(VisibilityRequest::Statement(&signed.header)),
184 timeout,
185 interruptible_sleep,
186 )
187 } else {
188 let tx = match ctx.open_signed(&target) {
189 Ok(tx) => tx,
190 Err(e) => return error(&e, exit::NOPERM),
191 };
192 drive(
193 || tx.set_repo_visibility(VisibilityRequest::Envelope(choice)),
194 timeout,
195 interruptible_sleep,
196 )
197 };
198 let outcome = match outcome {
199 Ok(outcome) => outcome,
200 Err(e) => {
201 let hint = match e {
202 mkit_core::protocol::TransportError::AccessDenied => {
203 " (only the repository owner can change visibility; a grant never can)"
204 }
205 _ => "",
206 };
207 return error(&format!("SetRepoVisibility: {e}{hint}"), exit::NOPERM);
208 }
209 };
210 if let Some(code) = finish_wait(&outcome, "the visibility change") {
211 return code;
212 }
213 if !matches!(outcome, Driven::Done(())) {
214 return exit::GENERAL_ERROR;
215 }
216 let mut stdout = std::io::stdout().lock();
217 let _ = writeln!(
218 stdout,
219 "{} is now {}",
220 target
221 .repository()
222 .map_or_else(|| target.endpoint.clone(), |r| r.to_string()),
223 match opts.visibility {
224 VisibilityArg::Public => "public",
225 VisibilityArg::Private => "private",
226 }
227 );
228 exit::OK
229}