Commit 4fe0e163 authored by Sanad Liaquat's avatar Sanad Liaquat Committed by Heinrich Lee Yu

Add logs for pat create and revoke services

Adds application and audit event logs.
This is requried for https://gitlab.com/gitlab-org/gitlab/-/issues/17176
to log an admin's creation of another user's PAT
parent 8188bc2d
......@@ -9,9 +9,13 @@ class Profiles::PersonalAccessTokensController < Profiles::ApplicationController
end
def create
@personal_access_token = finder.build(personal_access_token_params)
result = ::PersonalAccessTokens::CreateService.new(
current_user: current_user, target_user: current_user, params: personal_access_token_params
).execute
if @personal_access_token.save
@personal_access_token = result.payload[:personal_access_token]
if result.success?
PersonalAccessToken.redis_store!(current_user.id, @personal_access_token.token)
redirect_to profile_personal_access_tokens_path, notice: _("Your new personal access token has been created.")
else
......
......@@ -27,6 +27,7 @@ class UserPolicy < BasePolicy
rule { default }.enable :read_user_profile
rule { (private_profile | blocked_user) & ~(user_is_self | admin) }.prevent :read_user_profile
rule { user_is_self | admin }.enable :disable_two_factor
rule { (user_is_self | admin) & ~blocked }.enable :create_user_personal_access_token
end
UserPolicy.prepend_if_ee('EE::UserPolicy')
......@@ -2,23 +2,30 @@
module PersonalAccessTokens
class CreateService < BaseService
def initialize(current_user, params = {})
def initialize(current_user:, target_user:, params: {})
@current_user = current_user
@target_user = target_user
@params = params.dup
@ip_address = @params.delete(:ip_address)
end
def execute
personal_access_token = current_user.personal_access_tokens.create(params.slice(*allowed_params))
return ServiceResponse.error(message: 'Not permitted to create') unless creation_permitted?
if personal_access_token.persisted?
ServiceResponse.success(payload: { personal_access_token: personal_access_token })
token = target_user.personal_access_tokens.create(params.slice(*allowed_params))
if token.persisted?
log_event(token)
ServiceResponse.success(payload: { personal_access_token: token })
else
ServiceResponse.error(message: personal_access_token.errors.full_messages.to_sentence)
ServiceResponse.error(message: token.errors.full_messages.to_sentence, payload: { personal_access_token: token })
end
end
private
attr_reader :target_user, :ip_address
def allowed_params
[
:name,
......@@ -27,5 +34,15 @@ module PersonalAccessTokens
:expires_at
]
end
def creation_permitted?
Ability.allowed?(current_user, :create_user_personal_access_token, target_user)
end
def log_event(token)
log_info("PAT CREATION: created_by: '#{current_user.username}', created_for: '#{token.user.username}', token_id: '#{token.id}'")
end
end
end
PersonalAccessTokens::CreateService.prepend_if_ee('EE::PersonalAccessTokens::CreateService')
......@@ -4,16 +4,17 @@ module PersonalAccessTokens
class RevokeService
attr_reader :token, :current_user, :group
def initialize(current_user = nil, params = { token: nil, group: nil })
def initialize(current_user = nil, token: nil, group: nil )
@current_user = current_user
@token = params[:token]
@group = params[:group]
@token = token
@group = group
end
def execute
return ServiceResponse.error(message: 'Not permitted to revoke') unless revocation_permitted?
if token.revoke!
log_event
ServiceResponse.success(message: success_message)
else
ServiceResponse.error(message: error_message)
......@@ -33,6 +34,10 @@ module PersonalAccessTokens
def revocation_permitted?
Ability.allowed?(current_user, :revoke_token, token)
end
def log_event
Gitlab::AppLogger.info("PAT REVOCATION: revoked_by: '#{current_user.username}', revoked_for: '#{token.user.username}', token_id: '#{token.id}'")
end
end
end
......
......@@ -83,7 +83,9 @@ module ResourceAccessTokens
end
def create_personal_access_token(user)
PersonalAccessTokens::CreateService.new(user, personal_access_token_params).execute
PersonalAccessTokens::CreateService.new(
current_user: user, target_user: user, params: personal_access_token_params
).execute
end
def personal_access_token_params
......
......@@ -127,6 +127,8 @@ recorded:
- User was blocked via Admin Area ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/251) in GitLab 12.8)
- User was blocked via API ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/25872) in GitLab 12.9)
- Failed second-factor authentication attempt ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/16826) in GitLab 13.5)
- A user's personal access token was successfully created or revoked ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276921) in GitLab 13.6)
- A failed attempt to create or revoke a user's personal access token ([introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/276921) in GitLab 13.6)
It's possible to filter particular actions by choosing an audit data type from
the filter dropdown box. You can further filter by specific group, project, or user
......
# frozen_string_literal: true
module EE
module PersonalAccessTokens
module CreateService
def execute
super.tap do |response|
log_audit_event(response.payload[:personal_access_token], response)
end
end
private
def log_audit_event(token, response)
audit_event_service(token, response).for_user(full_path: target_user.username, entity_id: target_user.id).security_event
end
def audit_event_service(token, response)
message = if response.success?
"Created personal access token with id #{token.id}"
else
"Attempted to create personal access token but failed with message: #{response.message}"
end
::AuditEventService.new(
current_user,
target_user,
action: :custom,
custom_message: message,
ip_address: ip_address
)
end
end
end
end
......@@ -6,9 +6,16 @@ module EE
include ::Gitlab::Allowable
extend ::Gitlab::Utils::Override
def execute
super.tap do |response|
log_audit_event(token, response)
end
end
private
override :revocation_permitted?
def revocation_permitted?
super || managed_user_revocation_allowed?
end
......@@ -20,6 +27,25 @@ module EE
token.user.managing_group == group &&
can?(current_user, :admin_group_credentials_inventory, group)
end
def log_audit_event(token, response)
audit_event_service(token, response).for_user(full_path: token.user.username, entity_id: token.user.id).security_event
end
def audit_event_service(token, response)
message = if response.success?
"Revoked personal access token with id #{token.id}"
else
"Attempted to revoke personal access token with id #{token.id} but failed with message: #{response.message}"
end
::AuditEventService.new(
current_user,
token.user,
action: :custom,
custom_message: message
)
end
end
end
end
---
title: Add audit logs for PAT create and revoke services
merge_request: 45976
author:
type: added
......@@ -112,6 +112,73 @@ RSpec.describe 'Admin::AuditLogs', :js do
it_behaves_like 'audit events date filter'
end
describe 'personal access token events' do
shared_examples 'personal access token audit event' do
it 'show personal access token event details' do
visit admin_audit_logs_path
expect(page).to have_content(message)
end
end
context 'create personal access token' do
let(:personal_access_token_params) { { name: 'Test token', impersonation: false, scopes: [:api], expires_at: Date.today + 1.month } }
let(:personal_access_token) do
PersonalAccessTokens::CreateService.new(
current_user: admin, target_user: user, params: personal_access_token_params
).execute.payload[:personal_access_token]
end
context 'when creation succeeds' do
before do
personal_access_token
end
let(:message) { "Created personal access token with id #{personal_access_token.id}" }
it_behaves_like 'personal access token audit event'
end
context 'when creation fails' do
before do
allow_any_instance_of(ServiceResponse).to receive(:success?).and_return(false)
allow_any_instance_of(ServiceResponse).to receive(:message).and_return('error')
personal_access_token
end
let(:message) { "Attempted to create personal access token but failed with message: error" }
it_behaves_like 'personal access token audit event'
end
end
context 'revoke personal access token' do
let(:personal_access_token) { create(:personal_access_token, user: user) }
context 'when revocation succeeds' do
before do
PersonalAccessTokens::RevokeService.new(admin, token: personal_access_token).execute
end
let(:message) { "Revoked personal access token with id #{personal_access_token.id}" }
it_behaves_like 'personal access token audit event'
end
context 'when revocation fails' do
let(:message) { "Attempted to revoke personal access token with id #{personal_access_token.id} but failed with message: error" }
before do
allow_any_instance_of(ServiceResponse).to receive(:success?).and_return(false)
allow_any_instance_of(ServiceResponse).to receive(:message).and_return('error')
PersonalAccessTokens::RevokeService.new(admin, token: personal_access_token).execute
end
it_behaves_like 'personal access token audit event'
end
end
end
describe 'impersonated events' do
it 'show impersonation details' do
visit admin_user_path(user)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PersonalAccessTokens::CreateService do
describe '#execute' do
subject { service.execute }
let(:user) { create(:user) }
let(:params) { { name: 'Test token', impersonation: true, scopes: [:api], expires_at: Date.today + 1.month } }
let(:service) { described_class.new(current_user: user, target_user: user, params: params) }
it 'creates audit logs' do
expect(::AuditEventService)
.to receive(:new)
.with(user, user, action: :custom, custom_message: /Created personal access token with id \d+/, ip_address: nil)
.and_call_original
subject
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe PersonalAccessTokens::RevokeService do
describe '#execute' do
subject { service.execute }
let(:user) { create(:user) }
let(:token) { create(:personal_access_token, user: user) }
let(:service) { described_class.new(user, token: token) }
it 'creates audit logs' do
expect(::AuditEventService)
.to receive(:new)
.with(user, user, action: :custom, custom_message: "Revoked personal access token with id #{token.id}")
.and_call_original
subject
end
end
end
......@@ -245,7 +245,7 @@ module API
end
result = ::PersonalAccessTokens::CreateService.new(
user, name: params[:name], scopes: params[:scopes], expires_at: expires_at
current_user: user, target_user: user, params: { name: params[:name], scopes: params[:scopes], expires_at: expires_at }
).execute
unless result.status == :success
......
......@@ -51,7 +51,7 @@ module API
delete ':id' do
service = ::PersonalAccessTokens::RevokeService.new(
current_user,
{ token: find_token(params[:id]) }
token: find_token(params[:id])
).execute
service.success? ? no_content! : bad_request!(nil)
......
......@@ -4,6 +4,7 @@ require 'spec_helper'
RSpec.describe 'Profile > Personal Access Tokens', :js do
let(:user) { create(:user) }
let(:pat_create_service) { double('PersonalAccessTokens::CreateService', execute: ServiceResponse.error(message: 'error', payload: { personal_access_token: PersonalAccessToken.new })) }
def active_personal_access_tokens
find(".table.active-tokens")
......@@ -18,7 +19,7 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
end
def disallow_personal_access_token_saves!
allow_any_instance_of(PersonalAccessToken).to receive(:save).and_return(false)
allow(PersonalAccessTokens::CreateService).to receive(:new).and_return(pat_create_service)
errors = ActiveModel::Errors.new(PersonalAccessToken.new).tap { |e| e.add(:name, "cannot be nil") }
allow_any_instance_of(PersonalAccessToken).to receive(:errors).and_return(errors)
......@@ -100,7 +101,10 @@ RSpec.describe 'Profile > Personal Access Tokens', :js do
context "when revocation fails" do
it "displays an error message" do
visit profile_personal_access_tokens_path
allow_any_instance_of(PersonalAccessTokens::RevokeService).to receive(:revocation_permitted?).and_return(false)
allow_next_instance_of(PersonalAccessTokens::RevokeService) do |instance|
allow(instance).to receive(:revocation_permitted?).and_return(false)
end
accept_confirm { click_on "Revoke" }
expect(active_personal_access_tokens).to have_text(personal_access_token.name)
......
......@@ -40,6 +40,46 @@ RSpec.describe UserPolicy do
end
end
describe "creating a different user's Personal Access Tokens" do
context 'when current_user is admin' do
let(:current_user) { create(:user, :admin) }
context 'when admin mode is enabled and current_user is not blocked', :enable_admin_mode do
it { is_expected.to be_allowed(:create_user_personal_access_token) }
end
context 'when admin mode is enabled and current_user is blocked', :enable_admin_mode do
let(:current_user) { create(:admin, :blocked) }
it { is_expected.not_to be_allowed(:create_user_personal_access_token) }
end
context 'when admin mode is disabled' do
it { is_expected.not_to be_allowed(:create_user_personal_access_token) }
end
end
context 'when current_user is not an admin' do
context 'creating their own personal access tokens' do
subject { described_class.new(current_user, current_user) }
context 'when current_user is not blocked' do
it { is_expected.to be_allowed(:create_user_personal_access_token) }
end
context 'when current_user is blocked' do
let(:current_user) { create(:user, :blocked) }
it { is_expected.not_to be_allowed(:create_user_personal_access_token) }
end
end
context "creating a different user's personal access tokens" do
it { is_expected.not_to be_allowed(:create_user_personal_access_token) }
end
end
end
shared_examples 'changing a user' do |ability|
context "when a regular user tries to destroy another regular user" do
it { is_expected.not_to be_allowed(ability) }
......
......@@ -3,21 +3,53 @@
require 'spec_helper'
RSpec.describe PersonalAccessTokens::CreateService do
shared_examples_for 'a successfully created token' do
it 'creates personal access token record' do
expect(subject.success?).to be true
expect(token.name).to eq(params[:name])
expect(token.impersonation).to eq(params[:impersonation])
expect(token.scopes).to eq(params[:scopes])
expect(token.expires_at).to eq(params[:expires_at])
expect(token.user).to eq(user)
end
it 'logs the event' do
expect(Gitlab::AppLogger).to receive(:info).with(/PAT CREATION: created_by: '#{current_user.username}', created_for: '#{user.username}', token_id: '\d+'/)
subject
end
end
shared_examples_for 'an unsuccessfully created token' do
it { expect(subject.success?).to be false }
it { expect(subject.message).to eq('Not permitted to create') }
it { expect(token).to be_nil }
end
describe '#execute' do
context 'with valid params' do
it 'creates personal access token record' do
user = create(:user)
params = { name: 'Test token', impersonation: true, scopes: [:api], expires_at: Date.today + 1.month }
response = described_class.new(user, params).execute
personal_access_token = response.payload[:personal_access_token]
expect(response.success?).to be true
expect(personal_access_token.name).to eq(params[:name])
expect(personal_access_token.impersonation).to eq(params[:impersonation])
expect(personal_access_token.scopes).to eq(params[:scopes])
expect(personal_access_token.expires_at).to eq(params[:expires_at])
expect(personal_access_token.user).to eq(user)
subject { service.execute }
let(:current_user) { create(:user) }
let(:user) { create(:user) }
let(:params) { { name: 'Test token', impersonation: false, scopes: [:api], expires_at: Date.today + 1.month } }
let(:service) { described_class.new(current_user: current_user, target_user: user, params: params) }
let(:token) { subject.payload[:personal_access_token] }
context 'when current_user is an administrator' do
let(:current_user) { create(:admin) }
it_behaves_like 'a successfully created token'
end
context 'when current_user is not an administrator' do
context 'target_user is not the same as current_user' do
it_behaves_like 'an unsuccessfully created token'
end
context 'target_user is same as current_user' do
let(:current_user) { user }
it_behaves_like 'a successfully created token'
end
end
end
......
......@@ -6,6 +6,11 @@ RSpec.describe PersonalAccessTokens::RevokeService do
shared_examples_for 'a successfully revoked token' do
it { expect(subject.success?).to be true }
it { expect(service.token.revoked?).to be true }
it 'logs the event' do
expect(Gitlab::AppLogger).to receive(:info).with(/PAT REVOCATION: revoked_by: '#{current_user.username}', revoked_for: '#{token.user.username}', token_id: '\d+'/)
subject
end
end
shared_examples_for 'an unsuccessfully revoked token' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment