Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
resolver = "2"
members = [
"sandcrate-backend",
"sandcrate-web",
"sandcrate-cli",
"sandcrate-plugin"
]
68 changes: 62 additions & 6 deletions sandcrate-backend/src/api.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,74 @@
use axum::{routing::get, Json, Router};
use axum::{routing::get, Json, Router, extract::State};
use serde::Serialize;
use std::sync::Arc;
use std::fs;
use std::path::Path;

use crate::plugin::list_plugins;
use crate::auth::AuthConfig;

#[derive(Serialize)]
struct Plugin {
id: String,
name: String,
filename: String,
size: u64,
created_at: String,
}

#[derive(Serialize)]
struct PluginList {
plugins: Vec<String>,
plugins: Vec<Plugin>,
}

async fn get_plugins() -> Json<PluginList> {
let plugins = list_plugins();
async fn get_plugins(
State(_config): State<Arc<AuthConfig>>,
) -> Json<PluginList> {
let plugins_dir = Path::new("../assets/plugins");
let mut plugins = Vec::new();

if let Ok(entries) = fs::read_dir(plugins_dir) {
for entry in entries {
if let Ok(entry) = entry {
let path = entry.path();
if let Some(extension) = path.extension() {
if extension == "wasm" {
if let Ok(metadata) = fs::metadata(&path) {
let filename = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();

let name = filename.replace(".wasm", "");
let id = name.clone();

let created_at = metadata.created()
.ok()
.and_then(|time| time.duration_since(std::time::UNIX_EPOCH).ok())
.map(|duration| {
chrono::DateTime::from_timestamp(duration.as_secs() as i64, 0)
.unwrap_or_default()
.format("%Y-%m-%d %H:%M:%S")
.to_string()
})
.unwrap_or_else(|| "Unknown".to_string());

plugins.push(Plugin {
id,
name,
filename,
size: metadata.len(),
created_at,
});
}
}
}
}
}
}

Json(PluginList { plugins })
}

pub fn routes() -> Router {
pub fn routes() -> Router<Arc<AuthConfig>> {
Router::new().route("/plugins", get(get_plugins))
}
140 changes: 132 additions & 8 deletions sandcrate-backend/src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@ use axum::{
extract::State,
http::StatusCode,
response::Json,
routing::post,
routing::{get, post},
Router,
};
use axum_extra::{
extract::TypedHeader,
headers::{authorization::Bearer, Authorization},
};
use chrono::{Duration, Utc};
use jsonwebtoken::{encode, EncodingKey, Header};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, Validation};
use pam::Authenticator;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
Expand All @@ -27,10 +31,19 @@ pub struct LoginRequest {
#[derive(Debug, Serialize)]
pub struct LoginResponse {
pub token: String,
pub username: String,
pub user: UserInfo,
pub expires_at: String,
}

#[derive(Debug, Serialize)]
pub struct UserInfo {
pub id: String,
pub username: String,
pub name: String,
pub role: String,
pub is_admin: bool,
}

#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub error: String,
Expand All @@ -48,17 +61,45 @@ impl AuthConfig {
}
}

// Check if user has sudo/root privileges
fn check_user_privileges(username: &str) -> (bool, String) {
// Check if user is root
if username == "root" {
return (true, "root".to_string());
}

// Check if user has sudo privileges
let output = std::process::Command::new("sudo")
.args(["-l", "-U", username])
.output();

match output {
Ok(output) => {
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
if output_str.contains("(ALL : ALL)") || output_str.contains("(root)") {
return (true, "sudo".to_string());
}
}
}
Err(_) => {}
}

(false, "user".to_string())
}

pub async fn login(
State(config): State<Arc<AuthConfig>>,
Json(payload): Json<LoginRequest>,
) -> Result<Json<LoginResponse>, (StatusCode, Json<ErrorResponse>)> {
println!("Login attempt for user: {}", payload.username);
// Authenticate using PAM
let mut authenticator = Authenticator::with_password("login")
.map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to initialize PAM".to_string(),
error: "Failed to initialize PAM authentication".to_string(),
}),
)
})?;
Expand All @@ -69,6 +110,24 @@ pub async fn login(

match authenticator.authenticate() {
Ok(_) => {
// Check user privileges
let (is_admin, role) = check_user_privileges(&payload.username);

// Get user's real name from system
let real_name = std::process::Command::new("getent")
.args(["passwd", &payload.username])
.output()
.ok()
.and_then(|output| {
if output.status.success() {
let line = String::from_utf8_lossy(&output.stdout);
line.split(':').nth(4).map(|s| s.trim().to_string())
} else {
None
}
})
.unwrap_or_else(|| payload.username.clone());

// Generate JWT token
let now = Utc::now();
let expires_at = now + Duration::hours(24);
Expand All @@ -88,26 +147,91 @@ pub async fn login(
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "Failed to generate token".to_string(),
error: "Failed to generate authentication token".to_string(),
}),
)
})?;

let user_info = UserInfo {
id: payload.username.clone(),
username: payload.username.clone(),
name: real_name,
role,
is_admin,
};

println!("Login successful for user: {}", payload.username);
Ok(Json(LoginResponse {
token,
username: payload.username,
user: user_info,
expires_at: expires_at.to_rfc3339(),
}))
}
Err(_) => Err((
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "Invalid credentials".to_string(),
error: "Invalid username or password".to_string(),
}),
)),
}
}

pub async fn validate_token(
State(config): State<Arc<AuthConfig>>,
TypedHeader(Authorization(bearer)): TypedHeader<Authorization<Bearer>>,
) -> Result<Json<UserInfo>, (StatusCode, Json<ErrorResponse>)> {
let token = bearer.token();

println!("Validating token: {}", token);

let token_data = decode::<Claims>(
token,
&DecodingKey::from_secret(config.jwt_secret.as_ref()),
&Validation::default(),
)
.map_err(|e| {
println!("Token validation error: {:?}", e);
(
StatusCode::UNAUTHORIZED,
Json(ErrorResponse {
error: "Invalid or expired token".to_string(),
}),
)
})?;

let username = token_data.claims.sub;
println!("Token validated for user: {}", username);

let (is_admin, role) = check_user_privileges(&username);

let real_name = std::process::Command::new("getent")
.args(["passwd", &username])
.output()
.ok()
.and_then(|output| {
if output.status.success() {
let line = String::from_utf8_lossy(&output.stdout);
line.split(':').nth(4).map(|s| s.trim().to_string())
} else {
None
}
})
.unwrap_or_else(|| username.clone());

let user_info = UserInfo {
id: username.clone(),
username,
name: real_name,
role,
is_admin,
};

println!("Returning user info: {:?}", user_info);
Ok(Json(user_info))
}

pub fn auth_routes() -> Router<Arc<AuthConfig>> {
Router::new().route("/login", post(login))
Router::new()
.route("/login", post(login))
.route("/validate", get(validate_token))
}
Empty file removed sandcrate-backend/src/main.rs:3:38
Empty file.
Loading
Loading