Skip to content

Commit

Permalink
Add Organizational event logging feature
Browse files Browse the repository at this point in the history
This PR adds event/audit logging support for organizations.
By default this feature is disabled, since it does log a lot and adds
extra database transactions.

All events are touched except a few, since we do not support those
features (yet), like SSO for example.

This feature is tested with multiple clients and all database types.

Fixes #229
  • Loading branch information
BlackDex authored and dani-garcia committed Dec 1, 2022
1 parent 5a13efe commit b186813
Show file tree
Hide file tree
Showing 31 changed files with 1,887 additions and 240 deletions.
27 changes: 24 additions & 3 deletions .env.template
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# shellcheck disable=SC2034,SC2148
## Vaultwarden Configuration File
## Uncomment any of the following lines to change the defaults
##
## Be aware that most of these settings will be overridden if they were changed
## in the admin interface. Those overrides are stored within DATA_FOLDER/config.json .
##
## By default, vaultwarden expects for this file to be named ".env" and located
## By default, Vaultwarden expects for this file to be named ".env" and located
## in the current working directory. If this is not the case, the environment
## variable ENV_FILE can be set to the location of this file prior to starting
## vaultwarden.
## Vaultwarden.

## Main data folder
# DATA_FOLDER=data
Expand Down Expand Up @@ -80,11 +81,27 @@
## This setting applies globally to all users.
# EMERGENCY_ACCESS_ALLOWED=true

## Controls whether event logging is enabled for organizations
## This setting applies to organizations.
## Default this is disabled. Also check the EVENT_CLEANUP_SCHEDULE and EVENTS_DAYS_RETAIN settings.
# ORG_EVENTS_ENABLED=false

## Number of days to retain events stored in the database.
## If unset (the default), events are kept indefently and also disables the scheduled job!
# EVENTS_DAYS_RETAIN=

## Job scheduler settings
##
## Job schedules use a cron-like syntax (as parsed by https://crates.io/crates/cron),
## and are always in terms of UTC time (regardless of your local time zone settings).
##
## The schedule format is a bit different from crontab as crontab does not contains seconds.
## You can test the the format here: https://crontab.guru, but remove the first digit!
## SEC MIN HOUR DAY OF MONTH MONTH DAY OF WEEK
## "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri"
## "0 30 * * * * "
## "0 30 1 * * * "
##
## How often (in ms) the job scheduler thread checks for jobs that need running.
## Set to 0 to globally disable scheduled jobs.
# JOB_POLL_INTERVAL_MS=30000
Expand All @@ -108,6 +125,10 @@
## Cron schedule of the job that grants emergency access requests that have met the required wait time.
## Defaults to hourly (5 minutes after the hour). Set blank to disable this job.
# EMERGENCY_REQUEST_TIMEOUT_SCHEDULE="0 5 * * * *"
##
## Cron schedule of the job that cleans old events from the event table.
## Defaults to daily. Set blank to disable this job. Also without EVENTS_DAYS_RETAIN set, this job will not start.
# EVENT_CLEANUP_SCHEDULE="0 10 0 * * *"

## Enable extended logging, which shows timestamps and targets in the logs
# EXTENDED_LOGGING=true
Expand All @@ -133,7 +154,7 @@
## Enable WAL for the DB
## Set to false to avoid enabling WAL during startup.
## Note that if the DB already has WAL enabled, you will also need to disable WAL in the DB,
## this setting only prevents vaultwarden from automatically enabling it on start.
## this setting only prevents Vaultwarden from automatically enabling it on start.
## Please read project wiki page about this setting first before changing the value as it can
## cause performance degradation or might render the service unable to start.
# ENABLE_DB_WAL=true
Expand Down
1 change: 1 addition & 0 deletions migrations/mysql/2022-10-18-170602_add_events/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE event;
19 changes: 19 additions & 0 deletions migrations/mysql/2022-10-18-170602_add_events/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CREATE TABLE event (
uuid CHAR(36) NOT NULL PRIMARY KEY,
event_type INTEGER NOT NULL,
user_uuid CHAR(36),
org_uuid CHAR(36),
cipher_uuid CHAR(36),
collection_uuid CHAR(36),
group_uuid CHAR(36),
org_user_uuid CHAR(36),
act_user_uuid CHAR(36),
device_type INTEGER,
ip_address TEXT,
event_date DATETIME NOT NULL,
policy_uuid CHAR(36),
provider_uuid CHAR(36),
provider_user_uuid CHAR(36),
provider_org_uuid CHAR(36),
UNIQUE (uuid)
);
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE event;
19 changes: 19 additions & 0 deletions migrations/postgresql/2022-10-18-170602_add_events/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CREATE TABLE event (
uuid CHAR(36) NOT NULL PRIMARY KEY,
event_type INTEGER NOT NULL,
user_uuid CHAR(36),
org_uuid CHAR(36),
cipher_uuid CHAR(36),
collection_uuid CHAR(36),
group_uuid CHAR(36),
org_user_uuid CHAR(36),
act_user_uuid CHAR(36),
device_type INTEGER,
ip_address TEXT,
event_date TIMESTAMP NOT NULL,
policy_uuid CHAR(36),
provider_uuid CHAR(36),
provider_user_uuid CHAR(36),
provider_org_uuid CHAR(36),
UNIQUE (uuid)
);
1 change: 1 addition & 0 deletions migrations/sqlite/2022-10-18-170602_add_events/down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DROP TABLE event;
19 changes: 19 additions & 0 deletions migrations/sqlite/2022-10-18-170602_add_events/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
CREATE TABLE event (
uuid TEXT NOT NULL PRIMARY KEY,
event_type INTEGER NOT NULL,
user_uuid TEXT,
org_uuid TEXT,
cipher_uuid TEXT,
collection_uuid TEXT,
group_uuid TEXT,
org_user_uuid TEXT,
act_user_uuid TEXT,
device_type INTEGER,
ip_address TEXT,
event_date DATETIME NOT NULL,
policy_uuid TEXT,
provider_uuid TEXT,
provider_user_uuid TEXT,
provider_org_uuid TEXT,
UNIQUE (uuid)
);
44 changes: 40 additions & 4 deletions src/api/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use rocket::{
};

use crate::{
api::{ApiResult, EmptyResult, JsonResult, NumberOrString},
api::{core::log_event, ApiResult, EmptyResult, JsonResult, NumberOrString},
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
config::ConfigBuilder,
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
Expand Down Expand Up @@ -81,6 +81,8 @@ const DT_FMT: &str = "%Y-%m-%d %H:%M:%S %Z";

const BASE_TEMPLATE: &str = "admin/base";

const ACTING_ADMIN_USER: &str = "vaultwarden-admin-00000-000000000000";

fn admin_path() -> String {
format!("{}{}", CONFIG.domain_path(), ADMIN_PATH)
}
Expand Down Expand Up @@ -361,9 +363,27 @@ async fn get_user_json(uuid: String, _token: AdminToken, mut conn: DbConn) -> Js
}

#[post("/users/<uuid>/delete")]
async fn delete_user(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
async fn delete_user(uuid: String, _token: AdminToken, mut conn: DbConn, ip: ClientIp) -> EmptyResult {
let user = get_user_or_404(&uuid, &mut conn).await?;
user.delete(&mut conn).await

// Get the user_org records before deleting the actual user
let user_orgs = UserOrganization::find_any_state_by_user(&uuid, &mut conn).await;
let res = user.delete(&mut conn).await;

for user_org in user_orgs {
log_event(
EventType::OrganizationUserRemoved as i32,
&user_org.uuid,
user_org.org_uuid,
String::from(ACTING_ADMIN_USER),
14, // Use UnknownBrowser type
&ip.ip,
&mut conn,
)
.await;
}

res
}

#[post("/users/<uuid>/deauth")]
Expand Down Expand Up @@ -409,7 +429,12 @@ struct UserOrgTypeData {
}

#[post("/users/org_type", data = "<data>")]
async fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, mut conn: DbConn) -> EmptyResult {
async fn update_user_org_type(
data: Json<UserOrgTypeData>,
_token: AdminToken,
mut conn: DbConn,
ip: ClientIp,
) -> EmptyResult {
let data: UserOrgTypeData = data.into_inner();

let mut user_to_edit =
Expand Down Expand Up @@ -444,6 +469,17 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, m
}
}

log_event(
EventType::OrganizationUserUpdated as i32,
&user_to_edit.uuid,
data.org_uuid,
String::from(ACTING_ADMIN_USER),
14, // Use UnknownBrowser type
&ip.ip,
&mut conn,
)
.await;

user_to_edit.atype = new_type;
user_to_edit.save(&mut conn).await
}
Expand Down
25 changes: 20 additions & 5 deletions src/api/core/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ use rocket::serde::json::Json;
use serde_json::Value;

use crate::{
api::{EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType},
auth::{decode_delete, decode_invite, decode_verify_email, Headers},
api::{
core::log_user_event, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType,
},
auth::{decode_delete, decode_invite, decode_verify_email, ClientIp, Headers},
crypto,
db::{models::*, DbConn},
mail, CONFIG,
Expand Down Expand Up @@ -268,7 +270,12 @@ struct ChangePassData {
}

#[post("/accounts/password", data = "<data>")]
async fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
async fn post_password(
data: JsonUpcase<ChangePassData>,
headers: Headers,
mut conn: DbConn,
ip: ClientIp,
) -> EmptyResult {
let data: ChangePassData = data.into_inner().data;
let mut user = headers.user;

Expand All @@ -279,6 +286,8 @@ async fn post_password(data: JsonUpcase<ChangePassData>, headers: Headers, mut c
user.password_hint = clean_password_hint(&data.MasterPasswordHint);
enforce_password_hint_setting(&user.password_hint)?;

log_user_event(EventType::UserChangedPassword as i32, &user.uuid, headers.device.atype, &ip.ip, &mut conn).await;

user.set_password(
&data.NewMasterPasswordHash,
Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]),
Expand Down Expand Up @@ -334,7 +343,13 @@ struct KeyData {
}

#[post("/accounts/key", data = "<data>")]
async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
async fn post_rotatekey(
data: JsonUpcase<KeyData>,
headers: Headers,
mut conn: DbConn,
ip: ClientIp,
nt: Notify<'_>,
) -> EmptyResult {
let data: KeyData = data.into_inner().data;

if !headers.user.check_valid_password(&data.MasterPasswordHash) {
Expand Down Expand Up @@ -373,7 +388,7 @@ async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: D

// Prevent triggering cipher updates via WebSockets by settings UpdateType::None
// The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues.
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &mut conn, &nt, UpdateType::None)
update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &mut conn, &ip, &nt, UpdateType::None)
.await?
}

Expand Down
Loading

0 comments on commit b186813

Please sign in to comment.