Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Organizational event logging feature #2868

Merged
merged 1 commit into from
Dec 1, 2022
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
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