Skip to content

Commit ba752c8

Browse files
committed
Add token circuit breaker notification (issue #18886)
When a Token::Workflow fails to deliver to the SCM with authorization errors ('Unauthorized request' or 'Request forbidden'), the token gets disabled. This change adds: - Event::TokenDisabled: New event triggered when token is disabled - Notification system: Sends notifications to token executor and members - Default subscriptions: Enabled by default for instant_email and web channels - Tests: Added comprehensive test coverage for the new functionality The notification helps users immediately understand why their token was disabled and take corrective action. Fixes #18886
1 parent 3f69ef7 commit ba752c8

File tree

6 files changed

+162
-2
lines changed

6 files changed

+162
-2
lines changed

src/api/app/models/event/base.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def notification_events
2222
Event::CommentForRequest, Event::CommentForReport,
2323
Event::RelationshipCreate, Event::RelationshipDelete,
2424
Event::Report, Event::Decision, Event::AppealCreated,
25-
Event::WorkflowRunFail,
25+
Event::WorkflowRunFail, Event::TokenDisabled,
2626
Event::AddedUserToGroup, Event::RemovedUserFromGroup,
2727
Event::Assignment]
2828
end
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
module Event
2+
class TokenDisabled < Base
3+
self.description = 'SCM/CI Token disabled due to authorization failure'
4+
payload_keys :id, :token_id, :scm_vendor, :summary, :token_description
5+
6+
receiver_roles :token_executor, :token_member
7+
delegate :members, to: :token, prefix: true
8+
9+
self.notification_explanation = 'Receive notifications when an SCM/CI integration token is disabled due to authorization problems.'
10+
11+
# Example of subject:
12+
# GitHub workflow token disabled
13+
def subject
14+
vendor = payload['scm_vendor']&.capitalize
15+
"#{vendor} workflow token disabled"
16+
end
17+
18+
def token_executors
19+
[token&.executor].compact
20+
end
21+
22+
def parameters_for_notification
23+
super.merge(notifiable_type: 'Token::Workflow', notifiable_id: payload['token_id'], type: 'NotificationToken')
24+
end
25+
26+
def event_object
27+
Token.find(payload['token_id'])
28+
end
29+
30+
private
31+
32+
def token
33+
Token.find_by(id: payload['token_id'], type: 'Token::Workflow')
34+
end
35+
end
36+
end
37+
38+
# == Schema Information
39+
#
40+
# Table name: events
41+
#
42+
# id :bigint not null, primary key
43+
# eventtype :string(255) not null, indexed
44+
# mails_sent :boolean default(FALSE), indexed
45+
# payload :text(16777215)
46+
# undone_jobs :integer default(0)
47+
# created_at :datetime indexed
48+
# updated_at :datetime
49+
#
50+
# Indexes
51+
#
52+
# index_events_on_created_at (created_at)
53+
# index_events_on_eventtype (eventtype)
54+
# index_events_on_mails_sent (mails_sent)
55+
#

src/api/app/models/workflow_run.rb

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,16 @@ def update_as_failed(message)
8181
# "Failed to report back to GitHub: Unauthorized request. Please check your credentials again."
8282
# "Failed to report back to GitHub: Request is forbidden."
8383

84-
token.update(enabled: false) if message.include?('Unauthorized request') || /Request (is )?forbidden/.match?(message)
84+
return unless message.include?('Unauthorized request') || /Request (is )?forbidden/.match?(message)
85+
86+
token.update(enabled: false)
87+
# Create event notification for token being disabled
88+
Event::TokenDisabled.create(
89+
token_id: token.id,
90+
scm_vendor: scm_vendor,
91+
summary: message,
92+
token_description: token.description
93+
)
8594
end
8695

8796
# Stores debug info to help figure out what went wrong when trying to save a Status in the SCM.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
# frozen_string_literal: true
2+
3+
class CreateDefaultTokenDisabledSubscriptions < ActiveRecord::Migration[7.2]
4+
def up; end
5+
6+
def down
7+
raise ActiveRecord::IrreversibleMigration
8+
end
9+
end
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
RSpec.describe Event::TokenDisabled do
2+
describe '#token_executors' do
3+
subject { event.token_executors }
4+
5+
let(:token) { create(:workflow_token) }
6+
let(:event) do
7+
Event::TokenDisabled.create(
8+
token_id: token.id,
9+
scm_vendor: 'github',
10+
summary: 'Failed to report back to GitHub: Unauthorized request.',
11+
token_description: 'My workflow token'
12+
)
13+
end
14+
15+
it { expect(subject).to contain_exactly(token.executor) }
16+
17+
context 'when the token does not exist' do
18+
before do
19+
event
20+
token.destroy
21+
end
22+
23+
it { expect(subject).to be_empty }
24+
end
25+
end
26+
27+
describe '#subject' do
28+
subject { event.subject }
29+
30+
let(:token) { create(:workflow_token) }
31+
32+
context 'with GitHub vendor' do
33+
let(:event) do
34+
Event::TokenDisabled.create(
35+
token_id: token.id,
36+
scm_vendor: 'github',
37+
summary: 'Failed to report back to GitHub: Unauthorized request.',
38+
token_description: 'My workflow token'
39+
)
40+
end
41+
42+
it { expect(subject).to eq('Github workflow token disabled') }
43+
end
44+
45+
context 'with GitLab vendor' do
46+
let(:event) do
47+
Event::TokenDisabled.create(
48+
token_id: token.id,
49+
scm_vendor: 'gitlab',
50+
summary: 'Failed to report back to GitLab: Request forbidden.',
51+
token_description: 'My workflow token'
52+
)
53+
end
54+
55+
it { expect(subject).to eq('Gitlab workflow token disabled') }
56+
end
57+
end
58+
end

src/api/spec/models/workflow_run_spec.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,35 @@
9898
it 'disables the token of the token workflow' do
9999
expect { subject }.to change { workflow_run.token.reload.enabled }.from(true).to(false)
100100
end
101+
102+
it 'creates a TokenDisabled event' do
103+
expect { subject }.to change(Event::TokenDisabled, :count).by(1)
104+
end
105+
106+
it 'stores token_id in the TokenDisabled event payload' do
107+
subject
108+
event = Event::TokenDisabled.last
109+
expect(event.payload['token_id']).to eq(workflow_run.token.id)
110+
end
111+
112+
it 'stores scm_vendor and summary in the TokenDisabled event payload' do
113+
subject
114+
event = Event::TokenDisabled.last
115+
expect(event.payload['scm_vendor']).to eq(workflow_run.scm_vendor)
116+
expect(event.payload['summary']).to include('Request is forbidden')
117+
end
118+
end
119+
120+
context 'when the SCM responds with an unauthorized message' do
121+
subject { workflow_run.save_scm_report_failure('Failed to report back to GitLab: Unauthorized request. Please check your credentials again.', { api_endpoint: 'https://gitlab.com/api/v4' }) }
122+
123+
it 'disables the token of the token workflow' do
124+
expect { subject }.to change { workflow_run.token.reload.enabled }.from(true).to(false)
125+
end
126+
127+
it 'creates a TokenDisabled event' do
128+
expect { subject }.to change(Event::TokenDisabled, :count).by(1)
129+
end
101130
end
102131
end
103132

0 commit comments

Comments
 (0)