refactor: move entity into smaller module
This commit is contained in:
149
src/authfile.rs
149
src/authfile.rs
@@ -1,25 +1,10 @@
|
||||
use crate::entity::Entity;
|
||||
use russh::keys::ssh_key::public::KeyData;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Display;
|
||||
use std::io::{BufRead, BufReader};
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use russh::keys::PublicKey;
|
||||
use russh::keys::ssh_key::public::KeyData;
|
||||
|
||||
pub fn sanitize_name(s: &str) -> String {
|
||||
let mut sanitized = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
let ok = c.is_ascii_alphanumeric() || "@_-.".contains(c);
|
||||
if !ok {
|
||||
continue;
|
||||
}
|
||||
sanitized.push(c);
|
||||
}
|
||||
sanitized
|
||||
}
|
||||
|
||||
pub async fn read(path: &Path) -> Result<AuthFile, Error> {
|
||||
let handle = std::fs::File::open(path)?;
|
||||
@@ -27,38 +12,13 @@ pub async fn read(path: &Path) -> Result<AuthFile, Error> {
|
||||
let mut entities = vec![];
|
||||
for line in reader.lines() {
|
||||
let line = line?;
|
||||
let entity: Entity = line.as_str().try_into()?;
|
||||
entities.push(entity);
|
||||
entities.push(line.parse()?);
|
||||
}
|
||||
let key_pool = build_key_data_pool(&entities);
|
||||
let entities = entities.into_iter().map(Arc::new).collect();
|
||||
Ok(AuthFile { entities, key_pool })
|
||||
}
|
||||
|
||||
impl TryFrom<&str> for Entity {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(value: &str) -> Result<Self, Self::Error> {
|
||||
let key = PublicKey::from_openssh(value)?;
|
||||
|
||||
let comment = key.comment();
|
||||
let (name, role) = match comment.rsplit_once(":") {
|
||||
Some((name, "admin")) => (name, Role::Admin),
|
||||
None => (comment, Role::Normal),
|
||||
_ => {
|
||||
return Err(Error::InvalidRole(comment.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let persona = Persona {
|
||||
name: sanitize_name(name),
|
||||
role,
|
||||
};
|
||||
let persona = Arc::new(RwLock::new(persona));
|
||||
Ok(Entity { persona, key })
|
||||
}
|
||||
}
|
||||
|
||||
fn build_key_data_pool(entities: &[Entity]) -> HashSet<KeyData> {
|
||||
entities.iter().map(|e| e.key_data()).collect()
|
||||
}
|
||||
@@ -68,109 +28,10 @@ pub struct AuthFile {
|
||||
pub key_pool: HashSet<KeyData>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Persona {
|
||||
name: String,
|
||||
role: Role,
|
||||
}
|
||||
|
||||
impl Persona {
|
||||
pub fn title(&self) -> String {
|
||||
format!("[{} {}]", self.name, self.role)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.name.to_owned()
|
||||
}
|
||||
|
||||
pub fn role(&self) -> Role {
|
||||
self.role
|
||||
}
|
||||
}
|
||||
|
||||
pub type ArcPersona = Arc<RwLock<Persona>>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Entity {
|
||||
// Requires interior mutability for changing name and role
|
||||
persona: ArcPersona,
|
||||
// The public key does not change for an entity over
|
||||
// the lifetime of the app
|
||||
key: PublicKey,
|
||||
}
|
||||
|
||||
impl Entity {
|
||||
/// NOTE: interior mutation on persona
|
||||
pub async fn set_role(&mut self, role: Role) {
|
||||
self.persona.write().await.role = role;
|
||||
}
|
||||
|
||||
/// NOTE: interior mutation on persona
|
||||
pub async fn set_name(&self, name: &str) {
|
||||
self.persona.write().await.name = name.to_string();
|
||||
}
|
||||
|
||||
pub async fn to_pubkey(&self) -> PublicKey {
|
||||
let mut original_key = self.key.clone();
|
||||
let persona = self.persona.read().await;
|
||||
let name = &persona.name;
|
||||
let role = if persona.role == Role::Admin {
|
||||
":admin"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
original_key.set_comment(format!("{name}{role}"));
|
||||
original_key
|
||||
}
|
||||
|
||||
pub async fn name(&self) -> String {
|
||||
self.persona.read().await.name.to_string()
|
||||
}
|
||||
pub async fn role(&self) -> Role {
|
||||
self.persona.read().await.role
|
||||
}
|
||||
|
||||
pub async fn title(&self) -> String {
|
||||
self.persona.read().await.title()
|
||||
}
|
||||
|
||||
pub fn key_data(&self) -> KeyData {
|
||||
self.key.key_data().clone()
|
||||
}
|
||||
|
||||
pub fn fingerprint(&self) -> String {
|
||||
self.key
|
||||
.fingerprint(russh::keys::HashAlg::Sha256)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn persona(&self) -> Arc<RwLock<Persona>> {
|
||||
self.persona.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("unable to read authorization file")]
|
||||
FileNotReadable(#[from] std::io::Error),
|
||||
#[error("failed to parse public key")]
|
||||
PublicKeyParsing(#[from] russh::keys::ssh_key::Error),
|
||||
#[error("invalid role specified in authorization file at line: {0}")]
|
||||
InvalidRole(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Copy)]
|
||||
pub enum Role {
|
||||
Admin,
|
||||
Normal,
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let role = match self {
|
||||
Role::Admin => "admin",
|
||||
Role::Normal => "normal",
|
||||
};
|
||||
write!(f, "{role}")
|
||||
}
|
||||
#[error("failed to parse entity: {0}")]
|
||||
PublicKeyParsing(#[from] crate::entity::Error),
|
||||
}
|
||||
|
||||
148
src/entity.rs
Normal file
148
src/entity.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
use russh::keys::PublicKey;
|
||||
use russh::keys::ssh_key::public::KeyData;
|
||||
#[derive(Clone, Debug, PartialEq, Copy)]
|
||||
pub enum Role {
|
||||
Admin,
|
||||
Normal,
|
||||
}
|
||||
|
||||
impl Display for Role {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let role = match self {
|
||||
Role::Admin => "admin",
|
||||
Role::Normal => "normal",
|
||||
};
|
||||
write!(f, "{role}")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Persona {
|
||||
name: String,
|
||||
role: Role,
|
||||
}
|
||||
|
||||
impl Persona {
|
||||
pub fn title(&self) -> String {
|
||||
format!("[{} {}]", self.name, self.role)
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.name.to_owned()
|
||||
}
|
||||
|
||||
pub fn role(&self) -> Role {
|
||||
self.role
|
||||
}
|
||||
}
|
||||
|
||||
pub type ArcPersona = Arc<RwLock<Persona>>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Entity {
|
||||
// Requires interior mutability for changing name and role
|
||||
persona: ArcPersona,
|
||||
// The public key does not change for an entity over
|
||||
// the lifetime of the app
|
||||
key: PublicKey,
|
||||
}
|
||||
|
||||
impl Entity {
|
||||
/// NOTE: interior mutation on persona
|
||||
pub async fn set_role(&mut self, role: Role) {
|
||||
self.persona.write().await.role = role;
|
||||
}
|
||||
|
||||
/// NOTE: interior mutation on persona
|
||||
pub async fn set_name(&self, name: &str) {
|
||||
self.persona.write().await.name = sanitize_name(name);
|
||||
}
|
||||
|
||||
pub async fn to_pubkey(&self) -> PublicKey {
|
||||
let mut original_key = self.key.clone();
|
||||
let persona = self.persona.read().await;
|
||||
let name = &persona.name;
|
||||
let role = if persona.role == Role::Admin {
|
||||
":admin"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
original_key.set_comment(format!("{name}{role}"));
|
||||
original_key
|
||||
}
|
||||
|
||||
pub async fn name(&self) -> String {
|
||||
self.persona.read().await.name.to_string()
|
||||
}
|
||||
pub async fn role(&self) -> Role {
|
||||
self.persona.read().await.role
|
||||
}
|
||||
|
||||
pub async fn title(&self) -> String {
|
||||
self.persona.read().await.title()
|
||||
}
|
||||
|
||||
pub fn key_data(&self) -> KeyData {
|
||||
self.key.key_data().clone()
|
||||
}
|
||||
|
||||
pub fn fingerprint(&self) -> String {
|
||||
self.key
|
||||
.fingerprint(russh::keys::HashAlg::Sha256)
|
||||
.to_string()
|
||||
}
|
||||
|
||||
pub fn persona(&self) -> Arc<RwLock<Persona>> {
|
||||
self.persona.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn sanitize_name(s: &str) -> String {
|
||||
let mut sanitized = String::with_capacity(s.len());
|
||||
for c in s.chars() {
|
||||
let ok = c.is_ascii_alphanumeric() || "@_-.".contains(c);
|
||||
if !ok {
|
||||
continue;
|
||||
}
|
||||
sanitized.push(c);
|
||||
}
|
||||
sanitized
|
||||
}
|
||||
|
||||
impl FromStr for Entity {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
let key = PublicKey::from_openssh(s)?;
|
||||
|
||||
let comment = key.comment();
|
||||
let (name, role) = match comment.rsplit_once(":") {
|
||||
Some((name, "admin")) => (name, Role::Admin),
|
||||
None => (comment, Role::Normal),
|
||||
_ => {
|
||||
return Err(Error::InvalidRole(comment.to_string()));
|
||||
}
|
||||
};
|
||||
|
||||
let persona = Persona {
|
||||
name: sanitize_name(name),
|
||||
role,
|
||||
};
|
||||
let persona = Arc::new(RwLock::new(persona));
|
||||
Ok(Entity { persona, key })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("failed to parse public key")]
|
||||
PublicKeyParsing(#[from] russh::keys::ssh_key::Error),
|
||||
#[error("invalid role specified in authorization file at line: {0}")]
|
||||
InvalidRole(String),
|
||||
}
|
||||
41
src/lookup.rs
Normal file
41
src/lookup.rs
Normal file
@@ -0,0 +1,41 @@
|
||||
use crate::Error;
|
||||
use crate::entity::Entity;
|
||||
use std::str::FromStr;
|
||||
pub enum EntityLookup {
|
||||
Name(String),
|
||||
Sha256(String),
|
||||
}
|
||||
|
||||
impl FromStr for EntityLookup {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let lookup = match s.split_once(':') {
|
||||
Some(("SHA256", digest)) if !digest.is_empty() && !digest.contains(':') => {
|
||||
EntityLookup::Sha256(s.to_string())
|
||||
}
|
||||
None => EntityLookup::Name(s.to_string()),
|
||||
_ => return Err(Error::EntityLookup(s.to_string())),
|
||||
};
|
||||
Ok(lookup)
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityLookup {
|
||||
pub async fn matches<T: AsRef<Entity>>(&self, entity: T) -> bool {
|
||||
let entity = entity.as_ref();
|
||||
match self {
|
||||
EntityLookup::Name(name) => {
|
||||
if entity.name().await.eq(name.as_str()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
EntityLookup::Sha256(digest) => {
|
||||
if entity.fingerprint().eq(digest.as_str()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
58
src/main.rs
58
src/main.rs
@@ -1,6 +1,5 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::path::Path;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::Result;
|
||||
@@ -20,9 +19,11 @@ use tokio::sync::RwLock;
|
||||
use tui_textarea::TextArea;
|
||||
|
||||
mod authfile;
|
||||
mod entity;
|
||||
mod lookup;
|
||||
mod terminal_handle;
|
||||
mod ui;
|
||||
use authfile::{ArcPersona, Entity};
|
||||
use entity::{ArcPersona, Entity};
|
||||
use terminal_handle::TerminalHandle;
|
||||
|
||||
type SshTerminal = Terminal<TermionBackend<TerminalHandle>>;
|
||||
@@ -100,6 +101,8 @@ pub enum Error {
|
||||
EntityLookup(String),
|
||||
#[error("user {0:?} is not an admin")]
|
||||
NotAnAdmin(String),
|
||||
#[error("failed to parse SSH key string to an entity")]
|
||||
EntityParsing(#[from] entity::Error),
|
||||
}
|
||||
|
||||
pub struct Client {
|
||||
@@ -146,7 +149,7 @@ impl AppServer {
|
||||
}
|
||||
|
||||
async fn check_role_and_reload(&mut self) -> Result<(), Error> {
|
||||
if self.entity().await.role().await != authfile::Role::Admin {
|
||||
if self.entity().await.role().await != entity::Role::Admin {
|
||||
return Err(Error::NotAnAdmin(self.entity().await.name().await));
|
||||
}
|
||||
self.reload().await
|
||||
@@ -259,8 +262,6 @@ impl AppServer {
|
||||
key_data_to_user.insert(key_data, entity);
|
||||
}
|
||||
Command::Rename { from, to } => {
|
||||
let to = authfile::sanitize_name(&to);
|
||||
|
||||
for ent in self.keychain.read().await.iter() {
|
||||
if ent.name().await != from {
|
||||
continue;
|
||||
@@ -681,66 +682,27 @@ impl Drop for AppServer {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum EntityLookup {
|
||||
Name(String),
|
||||
Sha256(String),
|
||||
}
|
||||
|
||||
impl FromStr for EntityLookup {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||
let lookup = match s.split_once(':') {
|
||||
Some(("SHA256", digest)) if !digest.is_empty() && !digest.contains(':') => {
|
||||
EntityLookup::Sha256(s.to_string())
|
||||
}
|
||||
None => EntityLookup::Name(s.to_string()),
|
||||
_ => return Err(Error::EntityLookup(s.to_string())),
|
||||
};
|
||||
Ok(lookup)
|
||||
}
|
||||
}
|
||||
|
||||
impl EntityLookup {
|
||||
pub async fn matches<T: AsRef<Entity>>(&self, entity: T) -> bool {
|
||||
let entity = entity.as_ref();
|
||||
match self {
|
||||
EntityLookup::Name(name) => {
|
||||
if entity.name().await.eq(name.as_str()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
EntityLookup::Sha256(digest) => {
|
||||
if entity.fingerprint().eq(digest.as_str()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Command {
|
||||
Add(Entity),
|
||||
Rename { from: String, to: String },
|
||||
Commit,
|
||||
Info(EntityLookup),
|
||||
Info(lookup::EntityLookup),
|
||||
}
|
||||
|
||||
impl Command {
|
||||
fn parse(text: &str, role: authfile::Role, name: String) -> Result<Option<Self>, Error> {
|
||||
fn parse(text: &str, role: entity::Role, name: String) -> Result<Option<Self>, Error> {
|
||||
if text == "/commit" {
|
||||
return Ok(Some(Self::Commit));
|
||||
}
|
||||
|
||||
let split = text.split_once(char::is_whitespace);
|
||||
let is_admin = role == authfile::Role::Admin;
|
||||
let is_admin = role == entity::Role::Admin;
|
||||
Ok(Some(match split {
|
||||
Some(("/add", payload)) => {
|
||||
if !is_admin {
|
||||
return Err(Error::NotAnAdmin(name));
|
||||
}
|
||||
Self::Add(payload.try_into()?)
|
||||
Self::Add(payload.parse()?)
|
||||
}
|
||||
Some(("/rename", payload)) => {
|
||||
if !is_admin {
|
||||
|
||||
Reference in New Issue
Block a user