-
Notifications
You must be signed in to change notification settings - Fork 1k
Fixes #39512 - Add audit logs for user login/logoff/failed attempts #11097
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
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -111,10 +111,17 @@ def taxed_and_untaxed | |
| or(arel_table[:auditable_type].in(untaxable.map(&:to_s))). | ||
| or(arel_table.grouping(arel_taxed_only_by_organization)). | ||
| or(arel_table.grouping(arel_taxed_only_by_location)) | ||
| statement = statement.or(arel_table.grouping(arel_global_failed_login)) if User.current.admin? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I assume this is to deny a User that can read Audits but not the List of Users the ability to implicitly find out which User-Accounts exist/are disabled. |
||
|
|
||
| taxonomy_join_scope.where(statement) | ||
| end | ||
|
|
||
| def arel_global_failed_login | ||
| arel_table[:auditable_type].eq('User'). | ||
| and(arel_table[:auditable_id].eq(nil)). | ||
| and(arel_table[:action].eq('failed_login')) | ||
| end | ||
|
|
||
| def arel_taxed_only_by_location | ||
| arel_table[:auditable_type].in(location_taxable.map(&:to_s)). | ||
| and(loc_join_arel[:taxonomy_id].in(user_taxonomy_ids(Location))) | ||
|
|
@@ -153,6 +160,25 @@ def has_taxonomix?(model) | |
| end | ||
|
|
||
| module ClassMethods | ||
| def manual_event!(action:, auditable_type:, attribute:, value:, auditable_id: nil, auditable_name: nil, | ||
| actor: User.current, remote_address: nil, request_uuid: nil) | ||
| audit_attributes = { | ||
| :auditable_type => auditable_type, | ||
| :auditable_id => auditable_id, | ||
| :auditable_name => auditable_name, | ||
| :action => action, | ||
| :audited_changes => { attribute.to_s => value }, | ||
| :remote_address => remote_address, | ||
| :request_uuid => request_uuid, | ||
| } | ||
|
|
||
| if actor | ||
| User.as(actor) { create!(audit_attributes) } | ||
| else | ||
| create_without_actor!(audit_attributes) | ||
| end | ||
| end | ||
|
|
||
| def main_objects | ||
| main_classes = audited_classes.reject { |cl| cl.audited_options.key?(:associated_with) } | ||
| main_classes.concat(non_abstract_parents(main_classes)) | ||
|
|
@@ -166,6 +192,20 @@ def non_abstract_parents(classes_list) | |
| parents_list = classes_list.map(&:superclass).uniq | ||
| parents_list.select { |cl| cl != ActiveRecord::Base && !cl.abstract_class? && cl.table_exists? }.compact | ||
| end | ||
|
|
||
| private | ||
|
|
||
| # Audit callbacks copy User.current into the audit row. For actor: nil we | ||
| # intentionally want no actor, not an anonymous admin or a previously set | ||
| # current user. Clear User.current only for this create and restore it after | ||
| # so the caller's request/thread context is unchanged. | ||
| def create_without_actor!(audit_attributes) | ||
| previous_user = User.current | ||
| User.current = nil | ||
| create!(audit_attributes) | ||
| ensure | ||
| User.current = previous_user | ||
| end | ||
|
Comment on lines
+203
to
+208
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Got curious and now I know (finally) that this has no side-effects on other Threads, because |
||
| end | ||
|
|
||
| private | ||
|
|
@@ -189,8 +229,9 @@ def log_audit | |
| audited_fields[:audit_field] = change | ||
| log_line = change | ||
| end | ||
| audit_target = auditable_id.nil? ? auditable_name : auditable_id | ||
| Foreman::Logging.with_fields(audited_fields) do | ||
| audit_logger.info "#{auditable_type} (#{auditable_id}) #{action} event on #{attribute} #{log_line}" | ||
| audit_logger.info "#{auditable_type} (#{audit_target}) #{action} event on #{attribute} #{log_line}" | ||
| end | ||
| end | ||
| telemetry_increment_counter(:audit_records_logged, audited_changes.count, type: auditable_type) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want to say that the ludit-Logging should be part of
log_bruteforce(), assuming we want to audit any Brute-Force attempt. Downside: we cannot uselog_authentication_event()inForeman::Controller::BruteforceProtection.Also it might not be tied to User's ID in other occurrences of
log_bruteforce().There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
log_bruteforce is a method with a single line - which logs the login attempt. So, I dont see the requirement to move the log_authentication_event() to this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The idea is to add the audit-log also into
log_bruteforce(), so all invocations of that method also create an audit-log. For instance thelog_bruteforce()method is also used here: https://github.com/ATIX-AG/foreman/blob/dc651706cd492de2ead0b1db57adf34f17419811/app/controllers/api/base_controller.rb#L203Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this would then log user access attempts to the API. actually, the current implementation of this PR is not about API.