2026-05-29 11:24:58 +02:00

190 lines
6.0 KiB
Clojure

;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC Sucursal en España SL
(ns app.rpc.commands.audit
"Audit Log related RPC methods"
(:require
[app.common.data :as d]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http :as-alias http]
[app.loggers.audit :as audit]
[app.loggers.database :as loggers.db]
[app.loggers.mattermost :as loggers.mm]
[app.rpc :as-alias rpc]
[app.rpc.climit :as-alias climit]
[app.rpc.doc :as-alias doc]
[app.rpc.helpers :as rph]
[app.util.inet :as inet]
[app.util.services :as sv]
[clojure.set :as set]))
(def ^:private event-columns
[:id
:name
:source
:type
:tracked-at
:created-at
:profile-id
:ip-addr
:props
:context])
(defn- event->row [event]
[(:id event)
(:name event)
(:source event)
(:type event)
(:tracked-at event)
(:created-at event)
(:profile-id event)
(db/inet (:ip-addr event))
(db/tjson (:props event))
(db/tjson (d/without-nils (:context event)))])
(defn- adjust-timestamp
[{:keys [tracked-at created-at] :as event}]
(let [margin (inst-ms (ct/diff tracked-at created-at))]
(if (or (neg? margin)
(> margin 3600000))
;; If event is in future or lags more than 1 hour, we reasign
;; tracked-at to the server creation date
(-> event
(assoc :tracked-at created-at)
(update :context assoc :original-tracked-at tracked-at))
event)))
(defn- exception-event?
[{:keys [type name] :as ev}]
(and (= "action" type)
(or (= "unhandled-exception" name)
(= "exception-page" name))))
(def ^:private xf:map-event-row
(comp
(map adjust-timestamp)
(map event->row)))
(defn- prepare-events
[{:keys [::rpc/request-at ::rpc/profile-id events] :as params}]
(let [request (-> params meta ::http/request)
ip-addr (inet/parse-request request)
xform (comp
(map (fn [event]
{:id (uuid/next)
:type (:type event)
:name (:name event)
:props (:props event)
:context (:context event)
:profile-id profile-id
:ip-addr ip-addr
:source "frontend"
:tracked-at (:timestamp event)
:created-at request-at}))
(map (fn [item]
(with-meta item {::audit/event true}))))]
(sequence xform events)))
(def ^:private xf:map-telemetry-event-row
(comp
(map adjust-timestamp)
(map (fn [event]
(-> event
(assoc :id (uuid/next))
(update :created-at ct/truncate :days)
(update :tracked-at ct/truncate :days)
(audit/filter-telemetry-props)
(audit/filter-telemetry-context)
(assoc :ip-addr "0.0.0.0")
(assoc :source "telemetry:frontend"))))
(map event->row)))
(defn- handle-events
[{:keys [::db/pool] :as cfg} params]
(let [events (prepare-events params)]
;; Look for error reports and save them on internal reports table
(when-let [events (->> events
(sequence (filter exception-event?))
(not-empty))]
(run! (partial loggers.db/emit cfg) events)
(run! (partial loggers.mm/emit cfg) events))
(when (contains? cf/flags :audit-log)
;; Process and save full audit events when audit-log flag is active
(when-let [rows (-> (sequence xf:map-event-row events)
(not-empty))]
(db/insert-many! pool :audit-log event-columns rows)))
(when (contains? cf/flags :telemetry)
;; Store anonymized frontend events so the telemetry task can ship them
;; in batches. Runs independently from the audit-log insert above so
;; both modes can be active simultaneously.
(when-let [rows (-> (sequence xf:map-telemetry-event-row events)
(not-empty))]
(db/insert-many! pool :audit-log event-columns rows)))))
(def ^:private valid-event-types
#{"action" "identify" "trigger"})
(def ^:private schema:frontend-event
[:map {:title "Event"}
[:name
[:and {:gen/elements ["update-file", "get-profile"]}
[:string {:max 250}]
[:re #"[\d\w-]{1,50}"]]]
[:type
[:and {:gen/elements valid-event-types}
[:string {:max 250}]
[::sm/one-of {:format "string"} valid-event-types]]]
[:props
[:map-of :keyword ::sm/any]]
[:timestamp ::ct/inst]
[:context {:optional true}
[:map-of :keyword ::sm/any]]])
(def ^:private schema:push-audit-events
[:map {:title "push-audit-events"}
[:events [:vector schema:frontend-event]]])
(sv/defmethod ::push-audit-events
{::climit/id :submit-audit-events/by-profile
::climit/key-fn ::rpc/profile-id
::sm/params schema:push-audit-events
::audit/skip true
::doc/skip true
::doc/added "1.17"}
[{:keys [::db/pool] :as cfg} params]
(let [telemetry? (contains? cf/flags :telemetry)
audit-log? (contains? cf/flags :audit-log)
enabled? (and (not (db/read-only? pool))
(or audit-log? telemetry?))]
(when enabled?
(try
(handle-events cfg params)
(catch Throwable cause
(l/error :hint "unexpected error on persisting audit events from frontend"
:cause cause))))
(rph/wrap nil)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GET-ENABLED-FLAGS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(sv/defmethod ::get-enabled-flags
{::audit/skip true
::doc/skip true
::doc/added "1.20"}
[_cfg _params]
(set/intersection cf/flags #{:audit-log :telemetry}))