Skip to content
Open
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
3 changes: 3 additions & 0 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ link_duration: 1h
# How long will a global session be valid for
session_duration: 1mon

# Maximum allowed API key duration (0 disables expiration limit)
api_key_max_expiration: 365d

# How often to run expired secrets cleanup
secrets_cleanup_interval: 24h

Expand Down
81 changes: 81 additions & 0 deletions hurl/api_key.hurl
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Login and create an API key
GET http://localhost:8080/login
HTTP 200
[Asserts]
xpath "//form[@method='post']" count == 1

POST http://localhost:8080/login
[Form]
email: valid@example.com
HTTP 200

GET http://localhost:8081/.link.txt
HTTP 200
[Captures]
link: body

GET {{link}}
HTTP 302
Location: /
[Captures]
session: cookie "session_id"

POST http://localhost:8080/api_key
[Cookies]
session_id: {{session}}
[Form]
service: example
expiration: 60
HTTP 200
[Captures]
created_key: xpath "string(//code[@id='api-key'])"

# Use created key
GET http://localhost:8080/auth-url/status
X-Original-URL: http://localhost:8081/
X-Api-Key: {{created_key}}
HTTP 200

# Rotate key
POST http://localhost:8080/api_key/rotate
[Cookies]
session_id: {{session}}
[Form]
key: {{created_key}}
HTTP 200
[Captures]
rotated_key: xpath "string(//code[@id='api-key'])"

# Old key should fail
GET http://localhost:8080/auth-url/status
X-Original-URL: http://localhost:8081/
X-Api-Key: {{created_key}}
HTTP 401

# New key should succeed
GET http://localhost:8080/auth-url/status
X-Original-URL: http://localhost:8081/
X-Api-Key: {{rotated_key}}
HTTP 200

# Delete new key
POST http://localhost:8080/api_key/delete
[Cookies]
session_id: {{session}}
[Form]
key: {{rotated_key}}
HTTP 302
Location: /

# Deleted key should fail
GET http://localhost:8080/auth-url/status
X-Original-URL: http://localhost:8081/
X-Api-Key: {{rotated_key}}
HTTP 401

GET http://localhost:8080/
[Cookies]
session_id: {{session}}
HTTP 200
[Asserts]
body contains "API Keys"
45 changes: 33 additions & 12 deletions src/auth_url/handle_status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@ use actix_web::{get, web, HttpResponse};
use log::info;

use crate::error::Response;
use crate::secret::ProxySessionSecret;
use crate::secret::ProxyCodeSecret;
use crate::secret::{ApiKeySecret, ProxyCodeSecret, ProxySessionSecret};
use crate::{CONFIG, PROXY_SESSION_COOKIE};

/// This endpoint is used to check weather a user is logged in from a proxy
Expand All @@ -23,33 +22,55 @@ async fn status(
db: web::Data<crate::Database>,
proxy_code_opt: Option<ProxyCodeSecret>,
proxy_session_opt: Option<ProxySessionSecret>,
api_key_opt: Option<ApiKeySecret>,
) -> Response {
let mut response_builder = HttpResponse::Ok();
let mut response = response_builder.content_type("text/plain");

if let Some(api_key) = api_key_opt {
let config = CONFIG.read().await;
return Ok(response
.insert_header((
config.auth_url_email_header.as_str(),
api_key.user().email.clone(),
))
.insert_header((
config.auth_url_user_header.as_str(),
api_key.user().username.clone(),
))
.insert_header((
config.auth_url_name_header.as_str(),
api_key.user().name.clone(),
))
.insert_header((
config.auth_url_realms_header.as_str(),
api_key.user().realms.join(","),
))
.finish());
}

let proxy_session = if let Some(proxy_session) = proxy_session_opt {
proxy_session
} else if let Some(proxy_code) = proxy_code_opt {
info!("Proxied login for {}", &proxy_code.user().email);
let proxy_session = proxy_code
.exchange_sibling(&db)
.await?;
let proxy_session = proxy_code.exchange_sibling(&db).await?;

response = response.cookie(
Cookie::build(PROXY_SESSION_COOKIE, proxy_session.code().to_str_that_i_wont_print())
.path("/")
.http_only(true)
.finish(),
Cookie::build(
PROXY_SESSION_COOKIE,
proxy_session.code().to_str_that_i_wont_print(),
)
.path("/")
.http_only(true)
.finish(),
);

proxy_session
} else {
let mut remove_cookie = Cookie::new(PROXY_SESSION_COOKIE, "");
remove_cookie.make_removal();

return Ok(HttpResponse::Unauthorized()
.cookie(remove_cookie)
.finish());
return Ok(HttpResponse::Unauthorized().cookie(remove_cookie).finish());
};

let config = CONFIG.read().await;
Expand Down
83 changes: 45 additions & 38 deletions src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,16 @@ pub struct ConfigFile {

#[serde(deserialize_with = "duration_str::deserialize_duration_chrono")]
pub link_duration: Duration,
#[serde(deserialize_with = "duration_str::deserialize_duration_chrono")]
pub session_duration: Duration,
#[serde(deserialize_with = "duration_str::deserialize_duration_chrono")]
pub session_duration: Duration,

/// Interval for periodic cleanup of expired secrets
#[serde(deserialize_with = "duration_str::deserialize_duration_chrono")]
pub secrets_cleanup_interval: Duration,
/// Maximum allowed API key duration. 0 means no limit
#[serde(deserialize_with = "duration_str::deserialize_duration_chrono")]
pub api_key_max_expiration: Duration,

/// Interval for periodic cleanup of expired secrets
#[serde(deserialize_with = "duration_str::deserialize_duration_chrono")]
pub secrets_cleanup_interval: Duration,

pub title: String,
pub static_path: String,
Expand Down Expand Up @@ -79,8 +83,8 @@ pub struct ConfigFile {
}

impl Default for ConfigFile {
fn default() -> Self {
Self {
fn default() -> Self {
Self {
database_url: std::env::var("DATABASE_URL").unwrap_or("database.db".to_string()),

listen_host : std::env::var("LISTEN_HOST").unwrap_or("127.0.0.1".to_string()),
Expand All @@ -91,6 +95,8 @@ impl Default for ConfigFile {
link_duration : Duration::try_hours(12).unwrap(),
session_duration: Duration::try_days(30).unwrap(),

api_key_max_expiration: Duration::try_days(365).unwrap(),

secrets_cleanup_interval: Duration::try_hours(24).unwrap(),

title: "MagicEntry".to_string(),
Expand Down Expand Up @@ -128,7 +134,7 @@ impl Default for ConfigFile {

services: Services(vec![]),
}
}
}
}

impl ConfigFile {
Expand Down Expand Up @@ -163,16 +169,13 @@ impl ConfigFile {
let mut config = CONFIG.write().await;
log::info!("Reloading config from {}", CONFIG_FILE.as_str());

let mut new_config = serde_yaml::from_str::<ConfigFile>(
&std::fs::read_to_string(CONFIG_FILE.as_str())?
)?;
let mut new_config =
serde_yaml::from_str::<ConfigFile>(&std::fs::read_to_string(CONFIG_FILE.as_str())?)?;

if let Some(users_file) = &new_config.users_file {
new_config.users.extend(
serde_yaml::from_str::<Vec<User>>(
&std::fs::read_to_string(users_file)?
)?
);
new_config.users.extend(serde_yaml::from_str::<Vec<User>>(
&std::fs::read_to_string(users_file)?,
)?);
}

if new_config.users_file != config.users_file {
Expand All @@ -191,25 +194,27 @@ impl ConfigFile {
.with_poll_interval(std::time::Duration::from_secs(2))
.with_follow_symlinks(true);

let mut watcher = notify::PollWatcher::new(move |_| {
log::info!("Config file changed, reloading");
futures::executor::block_on(async {
if let Err(e) = ConfigFile::reload().await {
log::error!("Failed to reload config file: {}", e);
}
})
}, watcher_config)
.expect("Failed to create watcher for the config file");
let mut watcher = notify::PollWatcher::new(
move |_| {
log::info!("Config file changed, reloading");
futures::executor::block_on(async {
if let Err(e) = ConfigFile::reload().await {
log::error!("Failed to reload config file: {}", e);
}
})
},
watcher_config,
)
.expect("Failed to create watcher for the config file");

watcher
.watch(Path::new(CONFIG_FILE.as_str()), notify::RecursiveMode::NonRecursive)
.watch(
Path::new(CONFIG_FILE.as_str()),
notify::RecursiveMode::NonRecursive,
)
.expect("Failed to watch config file for changes");

if let Some(users_file) = CONFIG
.try_read()
.ok()
.and_then(|c| c.users_file.clone())
{
if let Some(users_file) = CONFIG.try_read().ok().and_then(|c| c.users_file.clone()) {
watcher
.watch(Path::new(&users_file), notify::RecursiveMode::NonRecursive)
.expect("Failed to watch users file for changes");
Expand All @@ -235,9 +240,7 @@ impl ConfigFile {
let data = std::fs::read_to_string(&self.saml_key_pem_path)?;
Ok(data
.lines()
.filter(|line| {
!line.contains("BEGIN PRIVATE KEY") && !line.contains("END PRIVATE KEY")
})
.filter(|line| !line.contains("BEGIN PRIVATE KEY") && !line.contains("END PRIVATE KEY"))
.collect::<String>()
.replace("\n", ""))
}
Expand All @@ -256,19 +259,23 @@ pub struct ConfigKV {

impl ConfigKV {
/// Set the provided key to the provided value - overwrites any previous values
pub async fn set(key: ConfigKeys, value: Option<String>, db: &Database) -> crate::error::Result<()> {
pub async fn set(
key: ConfigKeys,
value: Option<String>,
db: &Database,
) -> crate::error::Result<()> {
let key_str = serde_json::to_string(&key)?;
let value_str = value.unwrap_or_default();

let row = ConfigKVRow {
key: key_str,
value: value_str,
updated_at: None,
};

row.save(db).await
}

/// Get a config value by key
pub async fn get(key: &ConfigKeys, db: &Database) -> crate::error::Result<Option<String>> {
let key_str = serde_json::to_string(key)?;
Expand Down
Loading
Loading