Commit

Author:

Hash:

Timestamp:

+114 -113 +/-7 browse

Kevin Schoon [me@kevinschoon.com]

32e63078700254c5e70fc5fe54350e68cdd14b16

Sat, 26 Jul 2025 19:31:32 +0000 (3 months ago)

cleanup ayllu-keys, bring ayllu-shell inline with identity crate
1diff --git a/Cargo.lock b/Cargo.lock
2index d3fc63c..798cd97 100644
3--- a/Cargo.lock
4+++ b/Cargo.lock
5 @@ -384,12 +384,12 @@ version = "0.1.0"
6 dependencies = [
7 "ayllu_config",
8 "ayllu_git",
9+ "ayllu_identity",
10 "clap 4.5.41",
11 "dialoguer",
12 "nix",
13 "serde",
14 "thiserror 2.0.12",
15- "tracing",
16 "url",
17 ]
18
19 diff --git a/ayllu-keys/src/main.rs b/ayllu-keys/src/main.rs
20index 914bdf4..02e38b9 100644
21--- a/ayllu-keys/src/main.rs
22+++ b/ayllu-keys/src/main.rs
23 @@ -58,6 +58,31 @@ fn authorized_keys(home_dir: &Path) -> ExitCode {
24 }
25 }
26
27+ fn identify<'a>(config: &'a Config, ca_key_type: &str, certificate: &str) -> Option<&'a Identity> {
28+ let user_key =
29+ match ayllu_identity::PublicKey::parse(&format!("{} {}", ca_key_type, certificate)) {
30+ Ok(key) => key,
31+ Err(e) => {
32+ eprintln!("Cannot parse SSH key from sshd: {e:?}");
33+ return None;
34+ }
35+ };
36+
37+ match config.identities.iter().find(|identity| {
38+ identity
39+ .authorized_keys
40+ .iter()
41+ .find(|authorized_key| authorized_key.0 == user_key)
42+ .is_some()
43+ }) {
44+ Some(identity) => Some(identity),
45+ None => {
46+ eprintln!("Cannot identity a valid Ayllu user");
47+ None
48+ }
49+ }
50+ }
51+
52 fn main() -> ExitCode {
53 let args = Arguments::parse();
54
55 @@ -85,37 +110,44 @@ fn main() -> ExitCode {
56 args.username, args.home_dir, args.ca_key_type, args.certificate
57 );
58
59- let user_key = match ayllu_identity::PublicKey::parse(&format!(
60- "{} {}",
61- args.ca_key_type, args.certificate
62- )) {
63- Ok(key) => key,
64- Err(e) => {
65- eprintln!("Cannot parse SSH key from sshd: {e:?}");
66- return ExitCode::FAILURE;
67- }
68- };
69-
70- let identity = match config.identities.iter().find(|identity| {
71- identity
72- .authorized_keys
73- .iter()
74- .find(|authorized_key| authorized_key.0 == user_key)
75- .is_some()
76- }) {
77- Some(identity) => identity,
78- None => {
79- eprintln!("Cannot identity a valid Ayllu user");
80- return ExitCode::FAILURE;
81- }
82- };
83-
84- identity.authorized_keys.iter().for_each(|authorized_key| {
85+ if let Some(identity) = identify(&config, &args.ca_key_type, &args.certificate) {
86+ identity.authorized_keys.iter().for_each(|authorized_key| {
87 println!(
88- "restrict,pty,command=\"{ayllu_shell_path} --config {GLOBAL_AYLLU_CONFIG}\",environment=\"AYLLU_USERNAME={}\" {}",
89+ "restrict,pty,command=\"{ayllu_shell_path} --config {GLOBAL_AYLLU_CONFIG} --username={}\" {}",
90 identity.username, authorized_key.0
91 );
92 });
93+ }
94
95 ExitCode::SUCCESS
96 }
97+
98+ #[cfg(test)]
99+ mod test {
100+
101+ use ayllu_identity::{PublicKey, WrappedKey};
102+
103+ use super::*;
104+
105+ #[test]
106+ fn identify_user() {
107+ let public_key = PublicKey::parse("ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDk35l66zSsC9hgnL5T5svFHLauTZNZ595ihvfBGj81+ demo@example.org").unwrap();
108+ let cfg = Config {
109+ ayllu_shell: None,
110+ identities: vec![Identity {
111+ username: String::from("hello"),
112+ authorized_keys: vec![WrappedKey(public_key)],
113+ email: String::from("hello@example.org"),
114+ avatar: None,
115+ tagline: None,
116+ profiles: Vec::new(),
117+ }],
118+ };
119+ assert!(identify(
120+ &cfg,
121+ "ssh-ed25519",
122+ "AAAAC3NzaC1lZDI1NTE5AAAAIDk35l66zSsC9hgnL5T5svFHLauTZNZ595ihvfBGj81+"
123+ )
124+ .is_some_and(|identity| { identity.username == "hello" }));
125+ }
126+ }
127 diff --git a/ayllu-shell/Cargo.toml b/ayllu-shell/Cargo.toml
128index 76f0b2b..ad9b18f 100644
129--- a/ayllu-shell/Cargo.toml
130+++ b/ayllu-shell/Cargo.toml
131 @@ -7,11 +7,11 @@ edition = "2021"
132
133 ayllu_config = { path = "../crates/config" }
134 ayllu_git = { path = "../crates/git"}
135+ ayllu_identity = { path = "../crates/identity" }
136
137 clap = { workspace = true }
138 dialoguer = { version = "0.11.0", default-features = false }
139 nix = { version = "0.30.1", default-features = false, features = ["user"] }
140 serde = { workspace = true }
141 thiserror.workspace = true
142- tracing = { workspace = true }
143- url = "2.5.4"
144+ url = {workspace = true}
145 diff --git a/ayllu-shell/src/config.rs b/ayllu-shell/src/config.rs
146index c1709fd..0737340 100644
147--- a/ayllu-shell/src/config.rs
148+++ b/ayllu-shell/src/config.rs
149 @@ -1,6 +1,7 @@
150 use std::path::{Path, PathBuf};
151
152 use ayllu_config::Configurable;
153+ use ayllu_identity::Identity;
154 use serde::{Deserialize, Serialize};
155
156 #[derive(Default, Deserialize, Serialize, Clone, Debug)]
157 @@ -11,37 +12,26 @@ pub struct Collection {
158 pub hidden: Option<bool>,
159 }
160
161- /// An identity defines a known user in the Ayllu environment
162- #[derive(Serialize, Deserialize, Clone)]
163- pub struct Identity {
164- pub username: String,
165- #[serde(default = "Identity::default_shell")]
166- pub shell: PathBuf,
167- pub authorized_keys: Option<Vec<String>>,
168- #[serde(default = "Identity::default_repositories_path")]
169- pub repositories_path: PathBuf,
170- }
171-
172- impl Identity {
173- fn default_shell() -> PathBuf {
174- Path::new("/bin/sh").to_path_buf()
175- }
176-
177- fn default_repositories_path() -> PathBuf {
178- Path::new("repos").to_path_buf()
179- }
180- }
181-
182 /// Various Shell configuration
183 #[derive(Serialize, Deserialize, Clone, Default)]
184 pub struct Shell {
185 /// Message of the Day displayed for each interactive Ayllu session
186 pub motd: String,
187+ /// Path to remove removed repositories
188+ #[serde(default = "Shell::default_trash_path")]
189+ pub trash_path: PathBuf,
190 }
191
192- #[derive(Serialize, Deserialize, Clone, Default)]
193+ impl Shell {
194+ fn default_trash_path() -> PathBuf {
195+ Path::new("/usr/share/ayllu/.trash").to_path_buf()
196+ }
197+ }
198+
199+ #[derive(Serialize, Deserialize, Clone)]
200 pub struct Config {
201 pub log_level: String,
202+ #[serde(default = "Vec::new")]
203 pub identities: Vec<Identity>,
204 #[serde(default = "Shell::default")]
205 pub shell: Shell,
206 diff --git a/ayllu-shell/src/main.rs b/ayllu-shell/src/main.rs
207index 6a05851..1ffff2f 100644
208--- a/ayllu-shell/src/main.rs
209+++ b/ayllu-shell/src/main.rs
210 @@ -7,27 +7,41 @@ mod error;
211 mod ui;
212
213 #[derive(Parser, Debug)]
214- #[clap(version, about, long_about = "Ayllu Shell Access")]
215+ #[clap(
216+ version,
217+ about,
218+ long_about = r#"
219+
220+ Ayllu Shell
221+
222+ ayllu-shell provides a restricted shell for performing operations against
223+ an Ayllu server. Although it may be invoked directly, typically it is
224+ expected to be paired with ayllu-keys and an OpenSSH server.
225+ "#
226+ )]
227 struct Arguments {
228 /// logging level [ERROR,WARN,INFO,DEBUG,TRACE]
229 #[clap(short, long)]
230 level: Option<String>,
231 #[clap(short, long)]
232 config: Option<PathBuf>,
233+ /// Specify the user identity to assume
234+ #[clap(short, long)]
235+ username: Option<String>,
236 }
237
238 fn main() -> Result<(), Box<dyn std::error::Error>> {
239 let args = Arguments::parse();
240 let config: config::Config = ayllu_config::Reader::load(args.config.as_deref())?;
241- // If AYLLU_USERNAME is set then it's authenticated via ayllu-keys and we assume that identity
242- // otherwise we assume the identity of the user who is calling it.
243- let username = std::env::var("AYLLU_USERNAME").unwrap_or(std::env::var("USER").unwrap());
244+ let username = args
245+ .username
246+ .unwrap_or(std::env::var("USER").expect("USER is not set"));
247 let identity = config
248 .identities
249 .iter()
250- .find(|identity| identity.username == username)
251- .unwrap();
252+ .find(|identity| identity.username == username);
253 print!("{}", config.shell.motd);
254+ println!("\nYou are authenticated as: {}\n", username);
255 let menu = ui::Prompt {
256 config: &config,
257 identity,
258 diff --git a/ayllu-shell/src/ui.rs b/ayllu-shell/src/ui.rs
259index c612791..9adeb54 100644
260--- a/ayllu-shell/src/ui.rs
261+++ b/ayllu-shell/src/ui.rs
262 @@ -4,10 +4,11 @@ use std::{
263 };
264
265 use ayllu_git::{Scanner, Sites, Wrapper};
266+ use ayllu_identity::Identity;
267 use dialoguer::{theme::ColorfulTheme, Confirm, Input, MultiSelect, Select};
268
269 use crate::{
270- config::{Collection, Config, Identity},
271+ config::{Collection, Config},
272 error::Error,
273 };
274
275 @@ -53,27 +54,6 @@ mod helpers {
276
277 use super::*;
278
279- fn get_collection_path(identity: &Identity, collection: &Path) -> PathBuf {
280- if collection.is_absolute() {
281- collection.to_path_buf()
282- } else {
283- // Commands have to be run as the configured user validated by ayllu-keys
284- let current_user = std::env::var("USER").expect("USER is not set");
285- let user = nix::unistd::User::from_name(&current_user)
286- .expect("Cannot find user")
287- .expect("Unknown User");
288- if identity.repositories_path.is_relative() {
289- user.dir.join(&identity.repositories_path).join(collection)
290- } else {
291- identity.repositories_path.clone().join(collection)
292- }
293- }
294- }
295-
296- fn get_trash(identity: &Identity) -> PathBuf {
297- get_collection_path(identity, Path::new(".trash"))
298- }
299-
300 pub struct Repository<'a> {
301 pub collection: &'a Collection,
302 pub name: String,
303 @@ -81,15 +61,11 @@ mod helpers {
304 pub path: PathBuf,
305 }
306
307- pub fn repositories<'a>(
308- identity: &Identity,
309- collections: &'a [Collection],
310- ) -> Result<Vec<Repository<'a>>, Error> {
311+ pub fn repositories<'a>(collections: &'a [Collection]) -> Result<Vec<Repository<'a>>, Error> {
312 Ok(collections
313 .iter()
314 .try_fold(Vec::new(), |mut accm, collection| {
315- let collection_path = get_collection_path(identity, &collection.path);
316- let scanner: Scanner = TryInto::try_into(collection_path.as_path())?;
317+ let scanner: Scanner = TryInto::try_into(collection.path.as_path())?;
318 let repositories =
319 scanner
320 .into_iter()
321 @@ -110,45 +86,32 @@ mod helpers {
322 })?)
323 }
324
325- pub fn open(
326- identity: &Identity,
327- collection: &Collection,
328- name: &str,
329- ) -> Result<Wrapper, Error> {
330- let collection_path = get_collection_path(identity, &collection.path);
331- let repository = Wrapper::new(&collection_path.join(name))?;
332+ pub fn open(collection: &Collection, name: &str) -> Result<Wrapper, Error> {
333+ let repository = Wrapper::new(&collection.path.join(name))?;
334 Ok(repository)
335 }
336
337 pub fn create(
338- identity: &Identity,
339 collection: &Collection,
340 config: &ayllu_git::Config,
341 name: &str,
342 bare: bool,
343 ) -> Result<Wrapper, Error> {
344- let collection_path = get_collection_path(identity, &collection.path);
345- let repository = Wrapper::create(&collection_path.join(Path::new(name)), bare)?;
346+ let repository = Wrapper::create(&collection.path.join(Path::new(name)), bare)?;
347 repository.apply_config(config)?;
348 Ok(repository)
349 }
350
351- pub fn r#move(
352- identity: &Identity,
353- src: &Collection,
354- dst: &Collection,
355- name: &str,
356- ) -> Result<(), Error> {
357- let src_path = get_collection_path(identity, src.path.as_path()).join(Path::new(name));
358- let dst_path = get_collection_path(identity, dst.path.as_path()).join(Path::new(name));
359+ pub fn r#move(src: &Collection, dst: &Collection, name: &str) -> Result<(), Error> {
360+ let src_path = src.path.join(Path::new(name));
361+ let dst_path = dst.path.join(Path::new(name));
362 std::fs::rename(&src_path, &dst_path)?;
363 println!("Moved {:?} --> {:?}", src_path, dst_path);
364 Ok(())
365 }
366
367- pub fn remove(identity: &Identity, src: &Collection, name: &str) -> Result<(), Error> {
368- let src_path = get_collection_path(identity, &src.path.as_path()).join(Path::new(name));
369- let trash_base = get_trash(identity);
370+ pub fn remove(src: &Collection, name: &str, trash_base: &Path) -> Result<(), Error> {
371+ let src_path = src.path.join(Path::new(name));
372 if !trash_base.exists() {
373 std::fs::create_dir_all(&trash_base)?;
374 }
375 @@ -258,7 +221,8 @@ mod menu {
376
377 pub struct Prompt<'a> {
378 pub config: &'a Config,
379- pub identity: &'a Identity,
380+ #[allow(dead_code)]
381+ pub identity: Option<&'a Identity>,
382 }
383
384 impl Prompt<'_> {
385 @@ -304,12 +268,12 @@ impl Prompt<'_> {
386 branch: Some(branch),
387 });
388 }
389- helpers::create(self.identity, collection, &configuration, &name, bare)?;
390+ helpers::create(collection, &configuration, &name, bare)?;
391 println!("Repository Created Successfully");
392 self.execute(None, None)
393 }
394 Some(menu::Item::Edit { collection, name }) => {
395- let repository = helpers::open(self.identity, collection, &name)?;
396+ let repository = helpers::open(collection, &name)?;
397 let mut config = repository.config()?;
398 config.description = maybe_string(&string_input_default!(
399 "Description",
400 @@ -365,7 +329,7 @@ impl Prompt<'_> {
401 .interact()
402 .unwrap()
403 {
404- helpers::remove(self.identity, collection, name)?;
405+ helpers::remove(collection, name, &self.config.shell.trash_path)?;
406 self.execute(None, None)
407 } else {
408 self.execute(None, None)
409 @@ -373,7 +337,7 @@ impl Prompt<'_> {
410 }
411 Some(menu::Item::Browse) => {
412 let repositories: Vec<menu::Item> =
413- helpers::repositories(self.identity, self.config.collections.as_slice())?
414+ helpers::repositories(self.config.collections.as_slice())?
415 .iter()
416 .map(|repo| menu::Item::Repository {
417 collection: repo.collection,
418 @@ -390,7 +354,7 @@ impl Prompt<'_> {
419 Some(menu::Item::Move { collection, name }) => {
420 match menu::select_collection(self.config.collections.as_slice()) {
421 Some(other) => {
422- helpers::r#move(self.identity, collection, &other, name)?;
423+ helpers::r#move(collection, &other, name)?;
424 self.execute(None, None)
425 }
426 None => self.execute(None, None),
427 diff --git a/scripts/ayllu_shell_test.sh b/scripts/ayllu_shell_test.sh
428index f03e69a..c015cc0 100755
429--- a/scripts/ayllu_shell_test.sh
430+++ b/scripts/ayllu_shell_test.sh
431 @@ -30,6 +30,7 @@ chmod 644 /home/demo/.ssh/authorized_keys
432 cat /etc/ayllu/config.example.toml > /etc/ayllu/config.toml
433 echo "[[identities]]" >> /etc/ayllu/config.toml
434 echo username = \"demo\" >> /etc/ayllu/config.toml
435+ echo email = \"demo@example.org\" >> /etc/ayllu/config.toml
436 echo authorized_keys = [\"$PUBLIC_KEY\"] >> /etc/ayllu/config.toml
437 /usr/sbin/sshd $DEBUGGING -D -o PermitTTY=yes -o PermitUserEnvironment=AYLLU_USERNAME -o PasswordAuthentication=no -o AuthorizedKeysCommand="$KEYS_COMMAND" -o AuthorizedKeysCommandUser=root
438 EOF