Skip to content

Commit

Permalink
Merge pull request #2693 from cyberark/audit-list-memberships
Browse files Browse the repository at this point in the history
Add: audit event to GET /roles/:account/:kind/*identifier?memberships
  • Loading branch information
yoavgeva authored Jan 10, 2023
2 parents 1a84703 + 8e6a0f4 commit 5c99194
Show file tree
Hide file tree
Showing 5 changed files with 240 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
[cyberark/conjur#2691](https://github.com/cyberark/conjur/pull/2691)
- Show resource request (`GET /resources/:account/:kind/*identifier`) now produce audit events.
[cyberark/conjur#2695](https://github.com/cyberark/conjur/pull/2695)
- List memberships request (`GET /roles/:account/:kind/*identifier?memberships`) now produce audit events.
[cyberark/conjur#2693](https://github.com/cyberark/conjur/pull/2693)

## [1.19.0] - 2022-11-29

Expand Down
38 changes: 37 additions & 1 deletion app/controllers/roles_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@ def all_memberships
#
def direct_memberships
memberships = filtered_roles(role.direct_memberships_dataset(filter_params), membership_filter)
render_dataset(memberships)
render_result = render_dataset(memberships)
audit_memberships_success(membership_filter)
return render_result
rescue => e
audit_memberships_failure(membership_filter, e)
raise e
end

# Find all members of this role.
Expand Down Expand Up @@ -260,4 +265,35 @@ def audit_list_failure(err)
)
end

def audit_memberships_success(filter)
additional_params = %i[account count search kind filter]
options = params.permit(*additional_params).to_h.symbolize_keys
options[:filter] = filter if filter
options[:role] = role_id
Audit.logger.log(
Audit::Event::Memberships.new(
user_id: current_user.role_id,
client_ip: request.ip,
subject: options,
success: true
)
)
end

def audit_memberships_failure(filter, err)
additional_params = %i[account count search kind filter]
options = params.permit(*additional_params).to_h.symbolize_keys
options[:filter] = filter if filter
options[:role] = role_id
Audit.logger.log(
Audit::Event::Memberships.new(
user_id: current_user.role_id,
client_ip: request.ip,
subject: options,
success: false,
error_message: err.message
)
)
end

end
89 changes: 89 additions & 0 deletions app/models/audit/event/memberships.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
module Audit
module Event
class Memberships
def initialize(
user_id:,
client_ip:,
success:,
subject:,
error_message: nil
)
@user_id = user_id
@client_ip = client_ip
@subject = subject
@success = success
@error_message = error_message

# Implements `==` for audit events
@comparable_evt = ComparableEvent.new(self)
end

# NOTE: We want this class to be responsible for providing `progname`.
# At the same time, `progname` is currently always "conjur" and this is
# unlikely to change. Moving `progname` into the constructor now
# feels like premature optimization, so we ignore reek here.
# :reek:UtilityFunction
def progname
Event.progname
end

def severity
attempted_action.severity
end

def to_s
message
end

# action_sd means "action structured data"
def action_sd
attempted_action.action_sd
end

def message
attempted_action.message(
success_msg: "#{@user_id} successfully listed memberships with parameters: #{@subject}",
failure_msg: "#{@user_id} failed to list memberships with parameters: #{@subject}",
error_msg: @error_message
)
end

def message_id
'membership'
end

def structured_data
{
SDID::AUTH => { user: @user_id },
SDID::SUBJECT => @subject,
SDID::CLIENT => { ip: @client_ip }
}.merge(
attempted_action.action_sd
)
end

def facility
# Security or authorization messages which should be kept private. See:
# https://github.com/ruby/ruby/blob/master/ext/syslog/syslog.c#L109
# Note: Changed this to from LOG_AUTH to LOG_AUTHPRIV because the former
# is deprecated.
Syslog::LOG_AUTHPRIV
end

def ==(other)
@comparable_evt == other
end

private

def attempted_action
@attempted_action ||= AttemptedAction.new(
success: @success,
operation: 'list'
)
end
end

end

end
34 changes: 33 additions & 1 deletion cucumber/api/features/memberships.feature
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ Feature: Obtain the memberships of a role
And I create a new user "carol"
And I grant user "carol" to user "bob"
And I grant user "bob" to user "alice"
Given I save my place in the audit log file for remote
When I successfully GET "/roles/cucumber/user/alice?memberships"
Then the JSON should be:
"""
Expand All @@ -73,27 +74,48 @@ Feature: Obtain the memberships of a role
}
]
"""
And there is an audit record matching:
"""
<86>1 * * conjur * membership
[auth@43868 user="cucumber:user:admin"]
[subject@43868 account="cucumber" kind="user" role="cucumber:user:alice"]
[client@43868 ip="\d+\.\d+\.\d+\.\d+"]
[action@43868 result="success" operation="list"]
cucumber:user:admin successfully listed memberships with parameters: {:account=>"cucumber", :kind=>"user", :role=>"cucumber:user:alice"}
"""


@smoke
Scenario: Direct memberships can be counted
Given I create a new user "bob"
And I create a new user "carol"
And I grant user "carol" to user "bob"
And I grant user "bob" to user "alice"
When I successfully GET "/roles/cucumber/user/alice?memberships&count"
Given I save my place in the audit log file for remote
When I successfully GET "/roles/cucumber/user/alice?memberships&count=true"
Then the JSON should be:
"""
{
"count": 1
}
"""
And there is an audit record matching:
"""
<86>1 * * conjur * membership
[auth@43868 user="cucumber:user:admin"]
[subject@43868 account="cucumber" count="true" kind="user" role="cucumber:user:alice"]
[client@43868 ip="\d+\.\d+\.\d+\.\d+"]
[action@43868 result="success" operation="list"]
cucumber:user:admin successfully listed memberships with parameters: {:account=>"cucumber", :count=>"true", :kind=>"user", :role=>"cucumber:user:alice"}
"""

@smoke
Scenario: Direct memberships can be searched
Given I create a new user "bob"
And I create a new user "carol"
And I grant user "alice" to user "bob"
And I grant user "carol" to user "bob"
Given I save my place in the audit log file for remote
When I successfully GET "/roles/cucumber/user/bob?memberships&search=alice"
Then the JSON should be:
"""
Expand All @@ -106,6 +128,15 @@ Feature: Obtain the memberships of a role
}
]
"""
And there is an audit record matching:
"""
<86>1 * * conjur * membership
[auth@43868 user="cucumber:user:admin"]
[subject@43868 account="cucumber" search="alice" kind="user" role="cucumber:user:bob"]
[client@43868 ip="\d+\.\d+\.\d+\.\d+"]
[action@43868 result="success" operation="list"]
cucumber:user:admin successfully listed memberships with parameters: {:account=>"cucumber", :search=>"alice", :kind=>"user", :role=>"cucumber:user:bob"}
"""

@smoke
Scenario: The role memberships list can be filtered.
Expand Down Expand Up @@ -134,3 +165,4 @@ Feature: Obtain the memberships of a role
"cucumber:user:charles"
]
"""

79 changes: 79 additions & 0 deletions spec/models/audit/event/memberships_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
require 'spec_helper'

describe Audit::Event::Memberships do
let(:user_id) { 'rspec:user:my_user' }
let(:client_ip) { 'my-client-ip' }
let(:list_param) { { "limit"=> "1000" } }
let(:success) { true }
let(:error_message) { nil }


subject do
Audit::Event::Memberships.new(
user_id: user_id,
client_ip: client_ip,
subject: list_param,
success: success,
error_message: error_message
)
end

context 'when successful' do
it 'produces the expected message' do
expect(subject.message).to eq(
'rspec:user:my_user successfully listed memberships with parameters: {"limit"=>"1000"}'
)
end

it 'uses the INFO log level' do
expect(subject.severity).to eq(Syslog::LOG_INFO)
end

it 'renders to string correctly' do
expect(subject.to_s).to eq(
'rspec:user:my_user successfully listed memberships with parameters: {"limit"=>"1000"}'
)
end

it 'contains the user field' do
expect(subject.structured_data).to match(hash_including({
Audit::SDID::AUTH => { user: user_id }
}))
end

it 'contains the ip field' do
expect(subject.structured_data).to match(hash_including({
Audit::SDID::CLIENT => { ip: client_ip }
}))
end

it 'produces the expected action_sd' do
expect(subject.action_sd).to eq({ "action@43868": { operation: "list", result: "success" } })
end

it_behaves_like 'structured data includes client IP address'
end

context 'when a failure occurs' do
let(:success) { false }

it 'produces the expected message' do
expect(subject.message).to eq(
'rspec:user:my_user failed to list memberships with parameters: {"limit"=>"1000"}'
)
end

it 'uses the WARNING log level' do
expect(subject.severity).to eq(Syslog::LOG_WARNING)
end

it 'produces the expected action_sd' do
expect(subject.action_sd).to eq({ "action@43868": { operation: "list", result: "failure" } })
end

it_behaves_like 'structured data includes client IP address'
end



end

0 comments on commit 5c99194

Please sign in to comment.