Skip to content

Commit

Permalink
Merge pull request #83 from sebadob/audit-part-4
Browse files Browse the repository at this point in the history
Audit part 4
  • Loading branch information
sebadob committed Oct 19, 2023
2 parents 7b95acc + cdee2b1 commit 34d8888
Show file tree
Hide file tree
Showing 25 changed files with 789 additions and 232 deletions.
161 changes: 84 additions & 77 deletions Cargo.lock

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions dev_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ start with simple polling every few seconds first because of the HA_MODE problem
- if postgres does not work out nicely, think about using a NATS deployment for this task
- switch the UI component to the SSE stream
- add some way of configuring an email (or webhook, slack, ... ?) which gets messages depending on configured event level


### other features (some may come with v0.18 depending on amount of work)

- impl ApiKeyEntity in enc keys migrations
- add a way of detecting brute force or DoS attempts from certain IPs
- add an 'ip blacklist' feature
- add 'alg' in well-known jwks
- create an optional config to auto-blacklist IPs that have been detected doing brute force or DoS
think about the bigger picture here, maybe do this in 2 stages, like short block after 5 bad logins, 24h block after 10, ...
- admin ui component to show blacklisted IPs
Expand Down
17 changes: 17 additions & 0 deletions migrations/postgres/11_api_keys.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
create table api_keys
(
name varchar not null
constraint api_keys_pk
primary key,
secret bytea not null,
created bigint not null,
expires bigint,
enc_key_id varchar not null,
access bytea not null
);

create index api_keys_enc_key_id_index
on api_keys (enc_key_id);

create index api_keys_expires_index
on api_keys (expires);
17 changes: 17 additions & 0 deletions migrations/sqlite/11_api_keys.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
create table api_keys
(
name varchar not null
constraint api_keys_pk
primary key,
secret blob not null,
created bigint not null,
expires bigint,
enc_key_id varchar not null,
access blob not null
);

create index api_keys_enc_key_id_index
on api_keys (enc_key_id);

create index api_keys_expires_index
on api_keys (expires);
1 change: 1 addition & 0 deletions rauthy-common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ time = { workspace = true }
tracing = { workspace = true }
tokio = { workspace = true }
utoipa = { workspace = true }
validator = { workspace = true }

[dev-dependencies]
pretty_assertions = "1"
Expand Down
3 changes: 3 additions & 0 deletions rauthy-common/src/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub const RAUTHY_VERSION: &str = env!("CARGO_PKG_VERSION");

pub const HEADER_HTML: (&str, &str) = ("content-type", "text/html;charset=utf-8");
pub const APPLICATION_JSON: &str = "application/json";
pub const TOKEN_API_KEY: &str = "API-Key";
pub const TOKEN_BEARER: &str = "Bearer";
pub const COOKIE_SESSION: &str = "rauthy-session";
pub const COOKIE_MFA: &str = "rauthy-mfa";
Expand All @@ -19,6 +20,8 @@ pub const PWD_CSRF_HEADER: &str = "pwd-csrf-token";

pub const ARGON2ID_M_COST_MIN: u32 = 32768;
pub const ARGON2ID_T_COST_MIN: u32 = 1;
pub const API_KEY_LENGTH: usize = 48;
pub const EVENTS_LATEST_LIMIT: u16 = 100;

pub const CACHE_NAME_12HR: &str = "12hr";
pub const CACHE_NAME_AUTH_CODES: &str = "auth-codes";
Expand Down
9 changes: 9 additions & 0 deletions rauthy-common/src/error_response.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,3 +218,12 @@ impl From<FromUtf8Error> for ErrorResponse {
ErrorResponse::new(ErrorResponseType::Internal, value.to_string())
}
}

impl From<validator::ValidationErrors> for ErrorResponse {
fn from(value: validator::ValidationErrors) -> Self {
ErrorResponse::new(
ErrorResponseType::BadRequest,
format!("Payload validation error: {:?}", value),
)
}
}
24 changes: 12 additions & 12 deletions rauthy-handlers/src/clients.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ pub async fn get_clients(
data: web::Data<AppState>,
principal: web::ReqData<Option<Principal>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;

let clients = Client::find_all(&data).await?;
Expand Down Expand Up @@ -69,7 +69,7 @@ pub async fn get_client_by_id(
data: web::Data<AppState>,
principal: web::ReqData<Option<Principal>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;

Client::find(&data, path.into_inner())
Expand Down Expand Up @@ -100,7 +100,7 @@ pub async fn get_client_secret(
path: web::Path<String>,
principal: web::ReqData<Option<Principal>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;

client::get_client_secret(path.into_inner(), &data)
Expand Down Expand Up @@ -134,7 +134,7 @@ pub async fn post_clients(
principal: web::ReqData<Option<Principal>>,
session_req: web::ReqData<Option<Session>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
Expand Down Expand Up @@ -172,7 +172,7 @@ pub async fn put_clients(
principal: web::ReqData<Option<Principal>>,
session_req: web::ReqData<Option<Session>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
Expand Down Expand Up @@ -204,7 +204,7 @@ pub async fn get_client_colors(
id: web::Path<String>,
principal: web::ReqData<Option<Principal>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;

ColorEntity::find(&data, id.as_str())
Expand Down Expand Up @@ -238,7 +238,7 @@ pub async fn put_client_colors(
session_req: web::ReqData<Option<Session>>,
req_data: actix_web_validator::Json<ColorsRequest>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
Expand Down Expand Up @@ -274,7 +274,7 @@ pub async fn delete_client_colors(
principal: web::ReqData<Option<Principal>>,
session_req: web::ReqData<Option<Session>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
Expand Down Expand Up @@ -333,7 +333,7 @@ pub async fn put_client_logo(
session_req: web::ReqData<Option<Session>>,
payload: actix_multipart::Multipart,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
Expand Down Expand Up @@ -368,7 +368,7 @@ pub async fn delete_client_logo(
principal: web::ReqData<Option<Principal>>,
session_req: web::ReqData<Option<Session>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
Expand Down Expand Up @@ -408,7 +408,7 @@ pub async fn put_generate_client_secret(
principal: web::ReqData<Option<Principal>>,
session_req: web::ReqData<Option<Session>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
Expand Down Expand Up @@ -444,7 +444,7 @@ pub async fn delete_client(
principal: web::ReqData<Option<Principal>>,
session_req: web::ReqData<Option<Session>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
Expand Down
118 changes: 100 additions & 18 deletions rauthy-handlers/src/events.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,112 @@
use actix_web::{get, web, Responder};
use actix_web_grants::proc_macro::has_roles;
use crate::real_ip_from_req;
use actix_web::{get, post, web, HttpRequest, HttpResponse, Responder};
use actix_web_grants::proc_macro::has_any_permission;
use actix_web_lab::sse;
use rauthy_common::constants::SSE_KEEP_ALIVE;
use rauthy_common::error_response::{ErrorResponse, ErrorResponseType};
use rauthy_models::app_state::AppState;
use rauthy_models::entity::api_keys::{AccessGroup, AccessRights, ApiKey};
use rauthy_models::entity::principal::Principal;
use rauthy_models::entity::sessions::Session;
use rauthy_models::events::event::Event;
use rauthy_models::events::listener::EventRouterMsg;
use rauthy_models::request::EventsListenParams;
use std::time::Duration;
use validator::Validate;

/// Listen to the Events SSE stream
#[utoipa::path(
get,
path = "/events",
tag = "events",
params(EventsListenParams),
responses(
(status = 200, description = "Ok"),
(status = 400, description = "BadRequest", body = ErrorResponse),
(status = 401, description = "Unauthorized", body = ErrorResponse),
(status = 403, description = "Forbidden", body = ErrorResponse),
(status = 404, description = "NotFound", body = ErrorResponse),
),
)]
#[get("/events")]
// #[has_roles("rauthy_admin")] // TODO ADD BACK IN AFTER LOCAL TESTING!!!
pub async fn sse_events(data: web::Data<AppState>) -> Result<impl Responder, ErrorResponse> {
let (tx, sse) = sse::channel(5);
#[has_any_permission("session-auth", "api-key")]
pub async fn sse_events(
data: web::Data<AppState>,
api_key: web::ReqData<Option<ApiKey>>,
principal: web::ReqData<Option<Principal>>,
req: HttpRequest,
params: web::Query<EventsListenParams>,
) -> Result<impl Responder, ErrorResponse> {
params.validate()?;

if let Err(err) = data
.tx_events_router
.send_async(EventRouterMsg::ClientReg {
ip: "".to_string(),
tx,
})
.await
{
Err(ErrorResponse::new(
ErrorResponseType::Internal,
format!("Cannot register SSE client: {:?}", err),
))
if let Some(api_key) = api_key.into_inner() {
api_key.has_access(AccessGroup::Events, AccessRights::Read)?;
} else {
Ok(sse.with_keep_alive(Duration::from_secs(*SSE_KEEP_ALIVE as u64)))
Principal::from_req(principal)?.validate_rauthy_admin()?;
}

let (tx, sse) = sse::channel(10);

match real_ip_from_req(&req) {
None => Err(ErrorResponse::new(
ErrorResponseType::NotFound,
"Cannot extract client IP from HttpRequest. This is an internal network error."
.to_string(),
)),
Some(ip) => {
let params = params.into_inner();
if let Err(err) = data
.tx_events_router
.send_async(EventRouterMsg::ClientReg {
ip,
tx,
latest: params.latest,
})
.await
{
Err(ErrorResponse::new(
ErrorResponseType::Internal,
format!("Cannot register SSE client: {:?}", err),
))
} else {
Ok(sse.with_keep_alive(Duration::from_secs(*SSE_KEEP_ALIVE as u64)))
}
}
}
}

/// Create a TEST Event
#[utoipa::path(
post,
path = "/events/test",
tag = "events",
responses(
(status = 200, description = "Ok"),
(status = 401, description = "Unauthorized", body = ErrorResponse),
(status = 403, description = "Forbidden", body = ErrorResponse),
),
)]
#[post("/events/test")]
#[has_any_permission("session-auth", "api-key")]
pub async fn post_event_test(
data: web::Data<AppState>,
api_key: web::ReqData<Option<ApiKey>>,
principal: web::ReqData<Option<Principal>>,
req: HttpRequest,
session_req: web::ReqData<Option<Session>>,
) -> Result<HttpResponse, ErrorResponse> {
if let Some(api_key) = api_key.into_inner() {
api_key.has_access(AccessGroup::Events, AccessRights::Create)?;
} else {
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
}
Principal::from_req(principal)?.validate_rauthy_admin()?;
}

Event::test(real_ip_from_req(&req))
.send(&data.tx_events)
.await?;

Ok(HttpResponse::Ok().finish())
}
14 changes: 7 additions & 7 deletions rauthy-handlers/src/generic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,7 @@ pub async fn get_auth_check_admin(
"Not authenticated".to_string(),
)),
Some(p) => {
let principal = Principal::get_from_req(p.into_inner())?;
let principal = Principal::from_req(p)?;
principal.validate_rauthy_admin()?;
Ok(HttpResponse::Ok().finish())
}
Expand Down Expand Up @@ -341,7 +341,7 @@ pub async fn get_enc_keys(
data: web::Data<AppState>,
principal: web::ReqData<Option<Principal>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;

let active = &data.enc_key_active;
Expand Down Expand Up @@ -377,7 +377,7 @@ pub async fn post_migrate_enc_key(
session_req: web::ReqData<Option<Session>>,
req_data: actix_web_validator::Json<EncKeyMigrateRequest>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
Expand Down Expand Up @@ -409,7 +409,7 @@ pub async fn get_login_time(
data: web::Data<AppState>,
principal: web::ReqData<Option<Principal>>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;

let login_time = cache_get!(
Expand Down Expand Up @@ -460,7 +460,7 @@ pub async fn post_password_hash_times(
session_req: web::ReqData<Option<Session>>,
req_data: actix_web_validator::Json<PasswordHashTimesRequest>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
Expand Down Expand Up @@ -516,7 +516,7 @@ pub async fn put_password_policy(
session_req: web::ReqData<Option<Session>>,
req_data: actix_web_validator::Json<PasswordPolicyRequest>,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
principal.validate_rauthy_admin()?;
if session_req.is_some() {
Session::extract_validate_csrf(session_req, &req)?;
Expand Down Expand Up @@ -574,7 +574,7 @@ pub async fn post_update_language(
principal: web::ReqData<Option<Principal>>,
req: HttpRequest,
) -> Result<HttpResponse, ErrorResponse> {
let principal = Principal::get_from_req(principal.into_inner())?;
let principal = Principal::from_req(principal)?;
let mut user = User::find(&data, principal.user_id).await?;
user.language = Language::try_from(&req).unwrap_or_default();
user.update_language(&data).await?;
Expand Down
Loading

0 comments on commit 34d8888

Please sign in to comment.