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
7 changes: 1 addition & 6 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,4 @@ OIDC_REDIRECT_URL=http://localhost:8080/auth/callback

# Session configuration
SESSION_COOKIE_SECRET=replace-with-a-long-random-secret
SESSION_TTL_HOURS=12

# Gamma Groups to allow admin access for.
GAMMA_ADMIN_GROUPS=digit,didit
GAMMA_API_CLIENT_ID=replace-with-your-gamma-client-id
GAMMA_API_KEY=replace-with-your-gamma-api-key
SESSION_TTL_HOURS=12
16 changes: 8 additions & 8 deletions src/admin_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use dioxus::prelude::*;
#[server]
pub async fn list_icons() -> Result<Vec<IconRecord>, ServerFnError> {
use crate::db;
crate::auth::server::require_admin_request().await?;
crate::auth::server::require_authenticated_request().await?;
db::list_icons(db::pool())
.await
.map_err(|e| ServerFnError::new(format!("DB error: {e}")))
Expand All @@ -27,7 +27,7 @@ pub async fn add_icon(
use crate::db;
use base64::Engine;

crate::auth::server::require_admin_request().await?;
crate::auth::server::require_authenticated_request().await?;

let name = name.trim().to_lowercase();
if name.is_empty() {
Expand Down Expand Up @@ -71,7 +71,7 @@ pub async fn update_icon(
use crate::db;
use base64::Engine;

crate::auth::server::require_admin_request().await?;
crate::auth::server::require_authenticated_request().await?;

let name = new_name
.as_deref()
Expand Down Expand Up @@ -116,7 +116,7 @@ pub async fn update_icon(
#[server]
pub async fn delete_icon(id: i64) -> Result<(), ServerFnError> {
use crate::db;
crate::auth::server::require_admin_request().await?;
crate::auth::server::require_authenticated_request().await?;
db::delete_icon(db::pool(), id)
.await
.map_err(|e| ServerFnError::new(format!("DB error: {e}")))
Expand All @@ -125,7 +125,7 @@ pub async fn delete_icon(id: i64) -> Result<(), ServerFnError> {
#[server]
pub async fn list_manual_services() -> Result<Vec<ManualServiceRecord>, ServerFnError> {
use crate::db;
crate::auth::server::require_admin_request().await?;
crate::auth::server::require_authenticated_request().await?;
db::list_manual_services(db::pool())
.await
.map_err(|e| ServerFnError::new(format!("DB error: {e}")))
Expand All @@ -142,7 +142,7 @@ pub async fn add_manual_service(
) -> Result<ManualServiceRecord, ServerFnError> {
use crate::db;

crate::auth::server::require_admin_request().await?;
crate::auth::server::require_authenticated_request().await?;

let input =
ServiceInput::from_parts(title, url, description, category, github_url, icon_id).await?;
Expand Down Expand Up @@ -172,7 +172,7 @@ pub async fn update_manual_service(
) -> Result<ManualServiceRecord, ServerFnError> {
use crate::db;

crate::auth::server::require_admin_request().await?;
crate::auth::server::require_authenticated_request().await?;

let input =
ServiceInput::from_parts(title, url, description, category, github_url, icon_id).await?;
Expand All @@ -194,7 +194,7 @@ pub async fn update_manual_service(
#[server]
pub async fn delete_manual_service(id: i64) -> Result<(), ServerFnError> {
use crate::db;
crate::auth::server::require_admin_request().await?;
crate::auth::server::require_authenticated_request().await?;
db::delete_manual_service(db::pool(), id)
.await
.map_err(|e| ServerFnError::new(format!("DB error: {e}")))
Expand Down
29 changes: 3 additions & 26 deletions src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ pub fn AdminRoute() -> Element {
div { class: "admin-page",
div { class: "loading-container",
div { class: "loading-spinner" }
p { class: "loading-message", "Checking admin access..." }
p { class: "loading-message", "Loading..." }
}
}
},
Expand All @@ -45,30 +45,7 @@ fn AdminRouteContent() -> Element {
let auth_status = use_server_future(get_auth_status)?;

match auth_status() {
Some(Ok(status)) if status.authenticated && status.is_admin => rsx! { Admin {} },
Some(Ok(status)) if status.authenticated => rsx! {
div { class: "admin-page",
nav { class: "header-nav",
h1 { class: "header-title", "findIT" }
a { class: "admin-nav-back", href: "/", "← Back to dashboard" }
}
main { class: "main-content",
div { class: "error-container",
h1 { class: "error-title", "Access Denied" }
p { class: "error-message",
if let Some(name) = status.display_name {
"Signed in as {name}. You do not have permission to view the admin page. If you believe this is an error, please contact the administrator."
} else {
"You do not have permission to view the admin page."
}
}
div { class: "error-actions",
a { class: "btn-primary", href: "/", "Return to dashboard" }
}
}
}
}
},
Some(Ok(status)) if status.authenticated => rsx! { Admin {} },
Some(Ok(_)) => {
// Unauthenticated: Redirect to Gamma login via client-side script for immediate effect
rsx! {
Expand All @@ -93,7 +70,7 @@ fn AdminRouteContent() -> Element {
div { class: "admin-page",
div { class: "loading-container",
div { class: "loading-spinner" }
p { class: "loading-message", "Checking admin access..." }
p { class: "loading-message", "Loading..." }
}
}
},
Expand Down
94 changes: 0 additions & 94 deletions src/auth.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ pub async fn get_auth_status() -> Result<AuthStatus, ServerFnError> {
let status = AuthStatus {
authenticated: session.is_some(),
display_name: session.clone().and_then(|session| session.display_name),
is_admin: session.map_or(false, |session| session.is_admin),
};
return Ok(status);
}
Expand All @@ -20,7 +19,6 @@ pub async fn get_auth_status() -> Result<AuthStatus, ServerFnError> {
Ok(AuthStatus {
authenticated: false,
display_name: None,
is_admin: false,
})
}

Expand Down Expand Up @@ -66,7 +64,6 @@ pub mod server {
#[derive(Clone, Debug)]
pub struct AuthSession {
pub display_name: Option<String>,
pub is_admin: bool,
}

#[derive(Clone, Debug)]
Expand Down Expand Up @@ -189,22 +186,13 @@ pub mod server {
.or(userinfo.email);

let session_token = random_token();
let admin_groups = &config::get().gamma_admin_groups;
let is_admin = if !admin_groups.is_empty() {
check_if_admin(&auth_state, &subject, admin_groups)
.await
.unwrap_or(false)
} else {
false
};

db::create_auth_session(
db::pool(),
&session_token,
&subject,
&issuer,
display_name.as_deref(),
is_admin,
auth_state.session_ttl_hours,
)
.await
Expand Down Expand Up @@ -234,14 +222,6 @@ pub mod server {
.ok_or_else(|| ServerFnError::new("Authentication required"))
}

pub async fn require_admin_request() -> Result<AuthSession, ServerFnError> {
let session = require_authenticated_request().await?;
if !session.is_admin {
return Err(ServerFnError::new("Forbidden: Admin access required"));
}
Ok(session)
}

pub async fn require_optional_session() -> Result<Option<AuthSession>, ServerFnError> {
let ctx = FullstackContext::current()
.ok_or_else(|| ServerFnError::new("Missing request context"))?;
Expand Down Expand Up @@ -410,80 +390,6 @@ pub mod server {
.map_err(|err| format!("Failed to parse Gamma userinfo response: {err}"))
}

#[derive(Deserialize)]
struct GammaSuperGroupInfo {
name: String,
}

#[derive(Deserialize)]
struct GammaGroupInfo {
#[serde(rename = "superGroup", default)]
super_group: Option<GammaSuperGroupInfo>,
}

#[derive(Deserialize)]
struct GammaGroupMember {
group: GammaGroupInfo,
}

#[derive(Deserialize)]
struct GammaUserInfoWithGroups {
groups: Vec<GammaGroupMember>,
}

async fn check_if_admin(
auth_state: &AuthState,
uuid: &str,
admin_groups: &[String],
) -> Result<bool, String> {
let url = format!(
"{}/api/info/v1/users/{}",
config::get().oidc_issuer_url.trim_end_matches('/'),
uuid
);

let response = auth_state
.oidc_http_client
.get(&url)
.header(
"Authorization",
format!(
"pre-shared {}:{}",
config::get().gamma_api_client_id,
config::get().gamma_api_key
),
)
.header(reqwest::header::ACCEPT, "application/json")
.send()
.await
.map_err(|err| format!("Failed to reach Gamma groups API: {err}"))?;

if !response.status().is_success() {
let status = response.status();
let body = response.text().await.unwrap_or_default();
return Err(format!("Gamma groups API returned {status}. Body: {body}"));
}

let body = response
.text()
.await
.map_err(|err| format!("Failed to read Gamma groups API response: {err}"))?;

let user_info: GammaUserInfoWithGroups = serde_json::from_str(&body)
.map_err(|err| format!("Failed to parse Gamma groups response: {err}"))?;

let mut is_admin = false;
for member in &user_info.groups {
if let Some(super_group) = &member.group.super_group {
if admin_groups.iter().any(|ag| ag == &super_group.name) {
is_admin = true;
}
}
}

Ok(is_admin)
}

fn is_secure_cookie() -> bool {
match config::get().oidc_redirect_url.parse::<Uri>() {
Ok(uri) => uri.scheme_str() == Some("https"),
Expand Down
32 changes: 0 additions & 32 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,6 @@ pub struct Config {
pub session_cookie_secret: String,
#[serde(default = "default_session_ttl_hours")]
pub session_ttl_hours: i64,
#[serde(default = "default_gamma_admin_groups")]
pub gamma_admin_groups: Vec<String>,
#[serde(default)]
pub gamma_api_client_id: String,
#[serde(default)]
pub gamma_api_key: String,
}

#[cfg(not(target_arch = "wasm32"))]
Expand Down Expand Up @@ -82,10 +76,6 @@ fn default_oidc_redirect_url() -> String {
fn default_session_ttl_hours() -> i64 {
12
}
#[cfg(not(target_arch = "wasm32"))]
fn default_gamma_admin_groups() -> Vec<String> {
Vec::new()
}

#[cfg(not(target_arch = "wasm32"))]
pub fn get() -> &'static Config {
Expand Down Expand Up @@ -130,16 +120,6 @@ impl Config {
session_ttl_hours: env_or_dotenv("SESSION_TTL_HOURS", &dotenv)
.and_then(|value| value.parse().ok())
.unwrap_or_else(default_session_ttl_hours),
gamma_admin_groups: env_or_dotenv("GAMMA_ADMIN_GROUPS", &dotenv)
.map(|s| {
s.split(',')
.map(|g| g.trim().to_string())
.filter(|g| !g.is_empty())
.collect()
})
.unwrap_or_else(default_gamma_admin_groups),
gamma_api_client_id: env_or_dotenv("GAMMA_API_CLIENT_ID", &dotenv).unwrap_or_default(),
gamma_api_key: env_or_dotenv("GAMMA_API_KEY", &dotenv).unwrap_or_default(),
};
assert!(
!config.oidc_client_id.trim().is_empty(),
Expand All @@ -157,18 +137,6 @@ impl Config {
config.session_ttl_hours > 0,
"SESSION_TTL_HOURS must be greater than zero"
);
assert!(
!config.gamma_admin_groups.is_empty(),
"Missing GAMMA_ADMIN_GROUPS configuration"
);
assert!(
!config.gamma_api_client_id.trim().is_empty(),
"Missing GAMMA_API_CLIENT_ID configuration"
);
assert!(
!config.gamma_api_key.trim().is_empty(),
"Missing GAMMA_API_KEY configuration"
);
assert!(
config.docker_cache_ttl_seconds > 0,
"DOCKER_CACHE_TTL_SECONDS must be greater than zero"
Expand Down
8 changes: 2 additions & 6 deletions src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -574,7 +574,6 @@ pub async fn create_auth_session(
subject: &str,
issuer: &str,
display_name: Option<&str>,
is_admin: bool,
ttl_hours: i64,
) -> Result<(), sqlx::Error> {
sqlx::query("DELETE FROM auth_sessions WHERE expires_at <= datetime('now')")
Expand All @@ -588,17 +587,15 @@ pub async fn create_auth_session(
subject,
issuer,
display_name,
is_admin,
expires_at
)
VALUES (?, ?, ?, ?, ?, datetime('now', ? || ' hours'))
VALUES (?, ?, ?, ?, datetime('now', ? || ' hours'))
"#,
)
.bind(sha256_hex(session_token.as_bytes()))
.bind(subject)
.bind(issuer)
.bind(display_name)
.bind(is_admin)
.bind(ttl_hours)
.execute(pool)
.await?;
Expand All @@ -614,7 +611,7 @@ pub async fn get_auth_session_by_token(
let token_hash = sha256_hex(session_token.as_bytes());
let row = sqlx::query(
r#"
SELECT id, subject, issuer, display_name, is_admin
SELECT id, subject, issuer, display_name
FROM auth_sessions
WHERE session_token_hash = ? AND expires_at > datetime('now')
"#,
Expand All @@ -634,7 +631,6 @@ pub async fn get_auth_session_by_token(

Ok(row.map(|row| AuthSession {
display_name: row.get("display_name"),
is_admin: row.get("is_admin"),
}))
}

Expand Down
1 change: 0 additions & 1 deletion src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,4 @@ pub struct ManualServiceRecord {
pub struct AuthStatus {
pub authenticated: bool,
pub display_name: Option<String>,
pub is_admin: bool,
}
Loading