From b5af94ef92538a55d182620691b8155c09909b6c Mon Sep 17 00:00:00 2001 From: Ami Mahloof <130996527+ami-descope@users.noreply.github.com> Date: Wed, 17 Apr 2024 08:29:02 -0400 Subject: [PATCH] Management: Audit create event. (#68) --- Gemfile | 1 + Gemfile.lock | 6 ++ README.md | 19 +++- examples/ruby/management/audit_app.rb | 39 ++++++-- lib/descope/api/v1/management/audit.rb | 24 +++++ lib/descope/api/v1/management/common.rb | 1 + .../api/v1/management/audit_spec.rb | 36 ++++++++ spec/lib.descope/api/v1/auth_spec.rb | 2 +- .../api/v1/management/audit_spec.rb | 92 +++++++++++++++++++ 9 files changed, 209 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index 2f24c2d..8ba434f 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ gemspec group :development do gem 'rubocop', '1.63.2', require: false + gem 'rubocop-rails', '2.24.1', require: false end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index b77a01a..2dd7dfa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,6 +100,11 @@ GEM unicode-display_width (>= 2.4.0, < 3.0) rubocop-ast (1.31.2) parser (>= 3.3.0.4) + rubocop-rails (2.24.1) + activesupport (>= 4.2.0) + rack (>= 1.1) + rubocop (>= 1.33.0, < 2.0) + rubocop-ast (>= 1.31.1, < 2.0) ruby-progressbar (1.13.0) rubyzip (2.3.2) selenium-webdriver (4.19.0) @@ -135,6 +140,7 @@ DEPENDENCIES rotp (= 6.3.0) rspec (= 3.13.0) rubocop (= 1.63.2) + rubocop-rails (= 2.24.1) selenium-webdriver (= 4.19.0) simplecov (= 0.22.0) super_diff (= 0.11.0) diff --git a/README.md b/README.md index 7be3630..39ca440 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ These sections show how to use the SDK to perform permission and user management 8. [Manage Flows](#manage-flows-and-theme) 9. [Manage JWTs](#manage-jwts) 10. [Embedded links](#embedded-links) -11. [Search Audit](#search-audit) +11. [Audit](#audit) 12. [Manage ReBAC Authz](#manage-rebac-authz) 13. [Manage Project](#manage-project) @@ -871,7 +871,7 @@ This token can then be verified using the magic link 'verify' function, either d token = descope_client.generate_embedded_link(login_id: 'desmond@descope.com', custom_claims: {'key1':'value1'}) ``` -### Search Audit +### Audit You can perform an audit search for either specific values or full-text across the fields. Audit search is limited to the last 30 days. Below are some examples. For a full list of available search criteria options, see the function documentation. @@ -898,6 +898,21 @@ audits = descope_client.audit_search( audits = descope_client.audit_search(actions: ['LoginSucceed']) ``` +You can also create audit event with data + +```ruby +descope_client.audit_create_event( + actor_id: "UXXX", # required, for example a user ID + tenant_id: "tenant-id", # required + action: "pencil.created", # required + type: "info", # either: info/warn/error # required + data: { + pencil_id: "PXXX", + pencil_name: "Pencil Name" + } # optional +) +``` + ### Manage ReBAC Authz Descope supports full relation based access control (ReBAC) using a [Google Zanzibar](https://research.google/pubs/pub48190/) like schema and operations. diff --git a/examples/ruby/management/audit_app.rb b/examples/ruby/management/audit_app.rb index f88d56e..b42b425 100644 --- a/examples/ruby/management/audit_app.rb +++ b/examples/ruby/management/audit_app.rb @@ -14,13 +14,36 @@ @client = Descope::Client.new({ project_id: @project_id, management_key: @management_key }) begin - @logger.info('Going to search audit') - text = nil - text = ARGV[0] if ARGV.length > 1 - from_ts = nil - from_ts = DateTime.iso8601(ARGV[1]) if ARGV.length > 2 - res = @client.audit_search(text: text, from_ts: from_ts) - @logger.info("Audit search result: #{res}") + @logger.info('Do you want to to create a new audit event? [y/n] ') + create_audit = gets.chomp + if create_audit == 'y' + @logger.info('Enter the action for the audit event: ') + action = gets.chomp + @logger.info('Enter the type for the audit event: [info/warn/error] ') + type = gets.chomp + @logger.info('Enter the actorId for the audit event: ') + actor_id = gets.chomp + @logger.info('Enter the tenantId for the audit event: ') + tenant_id = gets.chomp + res = @client.audit_create_event( + action: action, + type: type, + actor_id: actor_id, + tenant_id: tenant_id + ) + @logger.info("Audit event created successfully: #{res}") + end + + @logger.info('Do you want to search the audit trail? [y/n] ') + search_audit = gets.chomp + if search_audit == 'y' + @logger.info('Enter the text to search: ') + text = gets.chomp + @logger.info('Enter the from_ts in ISO8601 format (2024-01-01 15:00:00.000) to search: ') + from_ts = gets.chomp + res = @client.audit_search(text: text, from_ts: from_ts) + @logger.info("Audit search result: #{res}") + end rescue Descope::AuthException => e - @logger.error("Audit search failed #{e}") + @logger.error("Audit action failed #{e}") end diff --git a/lib/descope/api/v1/management/audit.rb b/lib/descope/api/v1/management/audit.rb index 0bf289a..49bdf38 100644 --- a/lib/descope/api/v1/management/audit.rb +++ b/lib/descope/api/v1/management/audit.rb @@ -58,6 +58,30 @@ def audit_search( { 'audits' => res['audits'].map { |audit| convert_audit_record(audit) } } end + def audit_create_event(action: nil, type: nil, data: nil, user_id: nil, actor_id: nil, tenant_id: nil) + # Create an audit event + unless %w[info warn error].include?(type) + raise Descope::AuthException, 'type must be either info, warn or error' + end + + # validation + raise Descope::AuthException, 'data must be provided as a key, value Hash' unless data.is_a?(Hash) + raise Descope::AuthException, 'action must be provided' if action.nil? + raise Descope::AuthException, 'actor_id must be provided' if actor_id.nil? + raise Descope::AuthException, 'tenant_id must be provided' if tenant_id.nil? + + request_params = { + action:, + tenantId: tenant_id, + type:, + actorId: actor_id, + data: + } + request_params[:userId] = user_id unless user_id.nil? + + post(AUDIT_CREATE_EVENT, request_params) + end + private def convert_audit_record(audit) diff --git a/lib/descope/api/v1/management/common.rb b/lib/descope/api/v1/management/common.rb index ceaab80..ff93532 100644 --- a/lib/descope/api/v1/management/common.rb +++ b/lib/descope/api/v1/management/common.rb @@ -100,6 +100,7 @@ module Common # Audit AUDIT_SEARCH = '/v1/mgmt/audit/search' + AUDIT_CREATE_EVENT = '/v1/mgmt/audit/event' # Authz ReBAC AUTHZ_SCHEMA_SAVE = '/v1/mgmt/authz/schema/save' diff --git a/spec/integration/lib.descope/api/v1/management/audit_spec.rb b/spec/integration/lib.descope/api/v1/management/audit_spec.rb index 9ad465e..9d265df 100644 --- a/spec/integration/lib.descope/api/v1/management/audit_spec.rb +++ b/spec/integration/lib.descope/api/v1/management/audit_spec.rb @@ -5,12 +5,48 @@ describe Descope::Api::V1::Management::Audit do before(:all) do @client = DescopeClient.new(Configuration.config) + @client.logger.info('Deleting all tenants for Ruby SDK...') + @client.search_all_tenants(names: ['Ruby-SDK-test'])['tenants'].each do |tenant| + @client.logger.info("Deleting tenant: #{tenant['name']}") + @client.delete_tenant(tenant['id']) + end + @client.logger.info('Cleanup completed. Starting tests...') end + after(:all) do + all_users = @client.search_all_users + all_users['users'].each do |user| + if user['middleName'] == 'Ruby SDK User' + puts "Deleting ruby spec test user #{user['loginIds'][0]}" + @client.delete_user(user['loginIds'][0]) + end + end + end it 'should search the audit trail for user operations' do res = @client.audit_search(actions: ['LoginSucceed']) expect(res).to be_a(Hash) expect(res['audits']).to be_a(Array) end + + it 'should create a new audit event' do + # Create tenants + @client.logger.info('creating Ruby-SDK-test tenant') + tenant_id = @client.create_tenant(name: 'Ruby-SDK-test')['id'] + + # Create a user (actor) + user = build(:user) + created_user = @client.create_user(**user)['user'] + + expect do + res = @client.audit_create_event( + action: 'pencil.created', + type: 'info', + tenant_id:, + actor_id: created_user['loginIds'][0], + data: { 'key' => 'value' } + ) + expect(res).to eq({}) + end.not_to raise_error + end end diff --git a/spec/lib.descope/api/v1/auth_spec.rb b/spec/lib.descope/api/v1/auth_spec.rb index 27d525a..f67afef 100644 --- a/spec/lib.descope/api/v1/auth_spec.rb +++ b/spec/lib.descope/api/v1/auth_spec.rb @@ -181,7 +181,7 @@ expect do exp_in_seconds = 20 - puts "Sleeping for #{exp_in_seconds} seconds to test token expiration. Please wait..." + puts "\nAuthSpec.validate_token::Sleeping for #{exp_in_seconds} seconds to test token expiration. Please wait...\n" sleep(exp_in_seconds) @instance.send(:validate_token, token) end.to raise_error( diff --git a/spec/lib.descope/api/v1/management/audit_spec.rb b/spec/lib.descope/api/v1/management/audit_spec.rb index 70f4194..0396448 100644 --- a/spec/lib.descope/api/v1/management/audit_spec.rb +++ b/spec/lib.descope/api/v1/management/audit_spec.rb @@ -75,4 +75,96 @@ expect(res['audits'][0]['projectId']).to eq('abc') end end + + context '.create_event' do + it 'should respond to .audit_create_event' do + expect(@instance).to respond_to :audit_create_event + end + + it 'should raise an error if type is not info, warn or error' do + expect do + @instance.audit_create_event( + action: 'get', + type: 'debug', + data: { key: 'value' }, + user_id: 'user_id', + actor_id: 'actor_id', + tenant_id: 'tenant_id' + ) + end.to raise_error(Descope::AuthException, 'type must be either info, warn or error') + end + + it 'should raise an error if data is not a hash' do + expect do + @instance.audit_create_event( + action: 'get', + type: 'info', + data: 'data', + user_id: 'user_id', + actor_id: 'actor_id', + tenant_id: 'tenant_id' + ) + end.to raise_error(Descope::AuthException, 'data must be provided as a key, value Hash') + end + + it 'should raise an error if action is not provided' do + expect do + @instance.audit_create_event( + type: 'info', + data: { key: 'value' }, + user_id: 'user_id', + actor_id: 'actor_id', + tenant_id: 'tenant_id' + ) + end.to raise_error(Descope::AuthException, 'action must be provided') + end + + it 'should raise an error if actor is not provided' do + expect do + @instance.audit_create_event( + action: 'get', + type: 'info', + data: { key: 'value' }, + user_id: 'user_id', + tenant_id: 'tenant_id' + ) + end.to raise_error(Descope::AuthException, 'actor_id must be provided') + end + + it 'should raise an error if tenant_id is not provided' do + expect do + @instance.audit_create_event( + action: 'get', + type: 'info', + data: { key: 'value' }, + user_id: 'user_id', + actor_id: 'actor_id' + ) + end.to raise_error(Descope::AuthException, 'tenant_id must be provided') + end + + it 'is expected to create an audit event' do + expect(@instance).to receive(:post).with( + '/v1/mgmt/audit/event', + { + action: 'get', + type: 'info', + actorId: 'actor_id', + data: { key: 'value' }, + tenantId: 'tenant_id', + userId: 'user_id' + } + ) + expect do + @instance.audit_create_event( + action: 'get', + type: 'info', + data: { key: 'value' }, + user_id: 'user_id', + actor_id: 'actor_id', + tenant_id: 'tenant_id' + ) + end.not_to raise_error + end + end end