From 6efae726627500a3f1ebe3dd7ccd313fa171dc7f Mon Sep 17 00:00:00 2001 From: Ami Mahloof Date: Tue, 16 Apr 2024 16:59:46 -0400 Subject: [PATCH 1/2] Management: Audit create event. --- Gemfile | 1 + Gemfile.lock | 6 ++ README.md | 19 ++++- examples/ruby/management/audit_app.rb | 39 +++++++-- lib/descope/api/v1/management/audit.rb | 25 ++++++ 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 | 80 +++++++++++++++++++ 9 files changed, 198 insertions(+), 11 deletions(-) diff --git a/Gemfile b/Gemfile index 90c5708..dcd3eb0 100644 --- a/Gemfile +++ b/Gemfile @@ -5,6 +5,7 @@ gemspec group :development do gem 'rubocop', '1.63.0', require: false + gem 'rubocop-rails', '2.24.1', require: false end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 0fd00f7..3023cef 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.0) + 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..c4c92f5 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( + action: "pencil.created", + type: "info", # either: info/warn/error + actor_id: "UXXX", # for example a user ID + tenant_id: "tenant-id", # required + data: { + pencil_id: "PXXX", + pencil_name: "Pencil Name" + } +) +``` + ### 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..03dfb84 100644 --- a/lib/descope/api/v1/management/audit.rb +++ b/lib/descope/api/v1/management/audit.rb @@ -58,6 +58,31 @@ 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 + + # validate data + raise Descope::AuthException, 'data must be provided (Hash)' unless data.is_a?(Hash) + raise Descope::AuthException, 'data must not be empty' if data.empty? + + # validate tenant_id + 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..4b39cf5 100644 --- a/spec/lib.descope/api/v1/management/audit_spec.rb +++ b/spec/lib.descope/api/v1/management/audit_spec.rb @@ -75,4 +75,84 @@ 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 (Hash)') + end + + it 'should raise an error if data is an empty hash' do + expect do + @instance.audit_create_event( + action: 'get', + type: 'info', + data: {}, + user_id: 'user_id', + actor_id: 'actor_id', + tenant_id: 'tenant_id' + ) + end.to raise_error(Descope::AuthException, 'data must not be empty') + 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 From 949f323a2b3e7e71f0ae3460110fb660beae63aa Mon Sep 17 00:00:00 2001 From: Ami Mahloof Date: Wed, 17 Apr 2024 08:14:13 -0400 Subject: [PATCH 2/2] fixes for mandatory fields --- README.md | 8 +++---- lib/descope/api/v1/management/audit.rb | 9 ++++---- .../api/v1/management/audit_spec.rb | 22 ++++++++++++++----- 3 files changed, 25 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index c4c92f5..39ca440 100644 --- a/README.md +++ b/README.md @@ -902,14 +902,14 @@ You can also create audit event with data ```ruby descope_client.audit_create_event( - action: "pencil.created", - type: "info", # either: info/warn/error - actor_id: "UXXX", # for example a user ID + 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 ) ``` diff --git a/lib/descope/api/v1/management/audit.rb b/lib/descope/api/v1/management/audit.rb index 03dfb84..49bdf38 100644 --- a/lib/descope/api/v1/management/audit.rb +++ b/lib/descope/api/v1/management/audit.rb @@ -64,11 +64,10 @@ def audit_create_event(action: nil, type: nil, data: nil, user_id: nil, actor_id raise Descope::AuthException, 'type must be either info, warn or error' end - # validate data - raise Descope::AuthException, 'data must be provided (Hash)' unless data.is_a?(Hash) - raise Descope::AuthException, 'data must not be empty' if data.empty? - - # validate tenant_id + # 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 = { diff --git a/spec/lib.descope/api/v1/management/audit_spec.rb b/spec/lib.descope/api/v1/management/audit_spec.rb index 4b39cf5..0396448 100644 --- a/spec/lib.descope/api/v1/management/audit_spec.rb +++ b/spec/lib.descope/api/v1/management/audit_spec.rb @@ -80,6 +80,7 @@ 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( @@ -103,20 +104,31 @@ actor_id: 'actor_id', tenant_id: 'tenant_id' ) - end.to raise_error(Descope::AuthException, 'data must be provided (Hash)') + end.to raise_error(Descope::AuthException, 'data must be provided as a key, value Hash') end - it 'should raise an error if data is an empty hash' do + it 'should raise an error if action is not provided' do expect do @instance.audit_create_event( - action: 'get', type: 'info', - data: {}, + data: { key: 'value' }, user_id: 'user_id', actor_id: 'actor_id', tenant_id: 'tenant_id' ) - end.to raise_error(Descope::AuthException, 'data must not be empty') + 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