Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
9a02cb2918
commit
40512a72df
|
|
@ -0,0 +1,55 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActivityPub
|
||||||
|
class AcceptFollowService
|
||||||
|
MissingInboxURLError = Class.new(StandardError)
|
||||||
|
|
||||||
|
attr_reader :subscription, :actor
|
||||||
|
|
||||||
|
def initialize(subscription, actor)
|
||||||
|
@subscription = subscription
|
||||||
|
@actor = actor
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
return if subscription.accepted?
|
||||||
|
raise MissingInboxURLError unless subscription.subscriber_inbox_url.present?
|
||||||
|
|
||||||
|
upload_accept_activity
|
||||||
|
subscription.accepted!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def upload_accept_activity
|
||||||
|
body = Gitlab::Json::LimitedEncoder.encode(payload, limit: 1.megabyte)
|
||||||
|
|
||||||
|
begin
|
||||||
|
Gitlab::HTTP.post(subscription.subscriber_inbox_url, body: body, headers: headers)
|
||||||
|
rescue StandardError => e
|
||||||
|
raise ThirdPartyError, e.message
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def payload
|
||||||
|
follow = subscription.payload.dup
|
||||||
|
follow.delete('@context')
|
||||||
|
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: "#{actor}#follow/#{subscription.id}/accept",
|
||||||
|
type: 'Accept',
|
||||||
|
actor: actor,
|
||||||
|
object: follow
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def headers
|
||||||
|
{
|
||||||
|
'User-Agent' => "GitLab/#{Gitlab::VERSION}",
|
||||||
|
'Content-Type' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||||
|
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActivityPub
|
||||||
|
class InboxResolverService
|
||||||
|
attr_reader :subscription
|
||||||
|
|
||||||
|
def initialize(subscription)
|
||||||
|
@subscription = subscription
|
||||||
|
end
|
||||||
|
|
||||||
|
def execute
|
||||||
|
profile = subscriber_profile
|
||||||
|
unless profile.has_key?('inbox') && profile['inbox'].is_a?(String)
|
||||||
|
raise ThirdPartyError, 'Inbox parameter absent or invalid'
|
||||||
|
end
|
||||||
|
|
||||||
|
subscription.subscriber_inbox_url = profile['inbox']
|
||||||
|
subscription.shared_inbox_url = profile.dig('entrypoints', 'sharedInbox')
|
||||||
|
subscription.save!
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def subscriber_profile
|
||||||
|
raw_data = download_subscriber_profile
|
||||||
|
|
||||||
|
begin
|
||||||
|
profile = Gitlab::Json.parse(raw_data)
|
||||||
|
rescue JSON::ParserError => e
|
||||||
|
raise ThirdPartyError, e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
profile
|
||||||
|
end
|
||||||
|
|
||||||
|
def download_subscriber_profile
|
||||||
|
begin
|
||||||
|
response = Gitlab::HTTP.get(subscription.subscriber_url,
|
||||||
|
headers: {
|
||||||
|
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
rescue StandardError => e
|
||||||
|
raise ThirdPartyError, e.message
|
||||||
|
end
|
||||||
|
|
||||||
|
response.body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActivityPub
|
||||||
|
ThirdPartyError = Class.new(StandardError)
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
module ActivityPub
|
||||||
|
module Projects
|
||||||
|
class ReleasesSubscriptionWorker
|
||||||
|
include ApplicationWorker
|
||||||
|
include Gitlab::Routing.url_helpers
|
||||||
|
|
||||||
|
idempotent!
|
||||||
|
worker_has_external_dependencies!
|
||||||
|
feature_category :release_orchestration
|
||||||
|
data_consistency :delayed
|
||||||
|
queue_namespace :activity_pub
|
||||||
|
|
||||||
|
sidekiq_retries_exhausted do |msg, _ex|
|
||||||
|
subscription_id = msg['args'].second
|
||||||
|
subscription = ActivityPub::ReleasesSubscription.find_by_id(subscription_id)
|
||||||
|
subscription&.destroy
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform(subscription_id)
|
||||||
|
subscription = ActivityPub::ReleasesSubscription.find_by_id(subscription_id)
|
||||||
|
return if subscription.nil?
|
||||||
|
|
||||||
|
unless subscription.project.public?
|
||||||
|
subscription.destroy
|
||||||
|
return
|
||||||
|
end
|
||||||
|
|
||||||
|
InboxResolverService.new(subscription).execute if needs_resolving?(subscription)
|
||||||
|
AcceptFollowService.new(subscription, project_releases_url(subscription.project)).execute
|
||||||
|
end
|
||||||
|
|
||||||
|
def needs_resolving?(subscription)
|
||||||
|
subscription.subscriber_inbox_url.blank? || subscription.shared_inbox_url.blank?
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -3,6 +3,15 @@
|
||||||
#
|
#
|
||||||
# Do not edit it manually!
|
# Do not edit it manually!
|
||||||
---
|
---
|
||||||
|
- :name: activity_pub:activity_pub_projects_releases_subscription
|
||||||
|
:worker_name: ActivityPub::Projects::ReleasesSubscriptionWorker
|
||||||
|
:feature_category: :release_orchestration
|
||||||
|
:has_external_dependencies: true
|
||||||
|
:urgency: :low
|
||||||
|
:resource_boundary: :unknown
|
||||||
|
:weight: 1
|
||||||
|
:idempotent: true
|
||||||
|
:tags: []
|
||||||
- :name: authorized_project_update:authorized_project_update_project_recalculate
|
- :name: authorized_project_update:authorized_project_update_project_recalculate
|
||||||
:worker_name: AuthorizedProjectUpdate::ProjectRecalculateWorker
|
:worker_name: AuthorizedProjectUpdate::ProjectRecalculateWorker
|
||||||
:feature_category: :system_access
|
:feature_category: :system_access
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@
|
||||||
:queues:
|
:queues:
|
||||||
- - abuse_new_abuse_report
|
- - abuse_new_abuse_report
|
||||||
- 1
|
- 1
|
||||||
|
- - activity_pub
|
||||||
|
- 1
|
||||||
- - adjourned_project_deletion
|
- - adjourned_project_deletion
|
||||||
- 1
|
- 1
|
||||||
- - admin_emails
|
- - admin_emails
|
||||||
|
|
|
||||||
|
|
@ -933,7 +933,7 @@ types the variables can control for:
|
||||||
| `CI_COMMIT_BRANCH` | Yes | | | Yes |
|
| `CI_COMMIT_BRANCH` | Yes | | | Yes |
|
||||||
| `CI_COMMIT_TAG` | | Yes | | Yes, if the scheduled pipeline is configured to run on a tag. |
|
| `CI_COMMIT_TAG` | | Yes | | Yes, if the scheduled pipeline is configured to run on a tag. |
|
||||||
| `CI_PIPELINE_SOURCE = push` | Yes | Yes | | |
|
| `CI_PIPELINE_SOURCE = push` | Yes | Yes | | |
|
||||||
| `CI_PIPELINE_SOURCE = scheduled` | | | | Yes |
|
| `CI_PIPELINE_SOURCE = schedule` | | | | Yes |
|
||||||
| `CI_PIPELINE_SOURCE = merge_request_event` | | | Yes | |
|
| `CI_PIPELINE_SOURCE = merge_request_event` | | | Yes | |
|
||||||
| `CI_MERGE_REQUEST_IID` | | | Yes | |
|
| `CI_MERGE_REQUEST_IID` | | | Yes | |
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19575,6 +19575,11 @@ msgstr ""
|
||||||
msgid "Exceptions"
|
msgid "Exceptions"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Excluding 1 project with no DORA metrics"
|
||||||
|
msgid_plural "Excluding %d projects with no DORA metrics"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
msgid "Excluding USB security keys, you should include the browser name together with the device name."
|
msgid "Excluding USB security keys, you should include the browser name together with the device name."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::AcceptFollowService, feature_category: :integrations do
|
||||||
|
let_it_be(:project) { create(:project, :public) }
|
||||||
|
let_it_be_with_reload(:existing_subscription) do
|
||||||
|
create(:activity_pub_releases_subscription, :inbox, project: project)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:service) { described_class.new(existing_subscription, 'http://localhost/my-project/releases') }
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
context 'when third party server complies' do
|
||||||
|
before do
|
||||||
|
allow(Gitlab::HTTP).to receive(:post).and_return(true)
|
||||||
|
service.execute
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sends an Accept activity' do
|
||||||
|
expect(Gitlab::HTTP).to have_received(:post)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'updates subscription state to accepted' do
|
||||||
|
expect(existing_subscription.reload.status).to eq 'accepted'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when there is an error with third party server' do
|
||||||
|
before do
|
||||||
|
allow(Gitlab::HTTP).to receive(:post).and_raise(Errno::ECONNREFUSED)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises a ThirdPartyError' do
|
||||||
|
expect { service.execute }.to raise_error(ActivityPub::ThirdPartyError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not update subscription state to accepted' do
|
||||||
|
begin
|
||||||
|
service.execute
|
||||||
|
rescue StandardError
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(existing_subscription.reload.status).to eq 'requested'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when subscription is already accepted' do
|
||||||
|
before do
|
||||||
|
allow(Gitlab::HTTP).to receive(:post).and_return(true)
|
||||||
|
allow(existing_subscription).to receive(:accepted!).and_return(true)
|
||||||
|
existing_subscription.status = :accepted
|
||||||
|
service.execute
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not send an Accept activity' do
|
||||||
|
expect(Gitlab::HTTP).not_to have_received(:post)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not update subscription state' do
|
||||||
|
expect(existing_subscription).not_to have_received(:accepted!)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when inbox has not been resolved' do
|
||||||
|
before do
|
||||||
|
allow(Gitlab::HTTP).to receive(:post).and_return(true)
|
||||||
|
allow(existing_subscription).to receive(:accepted!).and_return(true)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'raises an error' do
|
||||||
|
existing_subscription.subscriber_inbox_url = nil
|
||||||
|
expect { service.execute }.to raise_error(ActivityPub::AcceptFollowService::MissingInboxURLError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::InboxResolverService, feature_category: :integrations do
|
||||||
|
let_it_be(:project) { create(:project, :public) }
|
||||||
|
let_it_be_with_reload(:existing_subscription) { create(:activity_pub_releases_subscription, project: project) }
|
||||||
|
let(:service) { described_class.new(existing_subscription) }
|
||||||
|
|
||||||
|
shared_examples 'third party error' do
|
||||||
|
it 'raises a ThirdPartyError' do
|
||||||
|
expect { service.execute }.to raise_error(ActivityPub::ThirdPartyError)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not update the subscription record' do
|
||||||
|
begin
|
||||||
|
service.execute
|
||||||
|
rescue StandardError
|
||||||
|
end
|
||||||
|
|
||||||
|
expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).not_to eq 'https://example.com/user/inbox'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#execute' do
|
||||||
|
context 'with successful HTTP request' do
|
||||||
|
before do
|
||||||
|
allow(Gitlab::HTTP).to receive(:get) { response }
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:response) { instance_double(HTTParty::Response, body: body) }
|
||||||
|
|
||||||
|
context 'with a JSON response' do
|
||||||
|
let(:body) do
|
||||||
|
{
|
||||||
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||||
|
id: 'https://example.com/user',
|
||||||
|
type: 'Person',
|
||||||
|
**inbox,
|
||||||
|
**entrypoints,
|
||||||
|
outbox: 'https://example.com/user/outbox'
|
||||||
|
}.to_json
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:entrypoints) { {} }
|
||||||
|
|
||||||
|
context 'with valid response' do
|
||||||
|
let(:inbox) { { inbox: 'https://example.com/user/inbox' } }
|
||||||
|
|
||||||
|
context 'without a shared inbox' do
|
||||||
|
it 'updates only the inbox in the subscription record' do
|
||||||
|
service.execute
|
||||||
|
|
||||||
|
expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).to eq 'https://example.com/user/inbox'
|
||||||
|
expect(ActivityPub::ReleasesSubscription.last.shared_inbox_url).to be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a shared inbox' do
|
||||||
|
let(:entrypoints) { { entrypoints: { sharedInbox: 'https://example.com/shared-inbox' } } }
|
||||||
|
|
||||||
|
it 'updates both the inbox and shared inbox in the subscription record' do
|
||||||
|
service.execute
|
||||||
|
|
||||||
|
expect(ActivityPub::ReleasesSubscription.last.subscriber_inbox_url).to eq 'https://example.com/user/inbox'
|
||||||
|
expect(ActivityPub::ReleasesSubscription.last.shared_inbox_url).to eq 'https://example.com/shared-inbox'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'without inbox attribute' do
|
||||||
|
let(:inbox) { {} }
|
||||||
|
|
||||||
|
it_behaves_like 'third party error'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with a non string inbox attribute' do
|
||||||
|
let(:inbox) { { inbox: 27.13 } }
|
||||||
|
|
||||||
|
it_behaves_like 'third party error'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with non JSON response' do
|
||||||
|
let(:body) { '<div>woops</div>' }
|
||||||
|
|
||||||
|
it_behaves_like 'third party error'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'with http error' do
|
||||||
|
before do
|
||||||
|
allow(Gitlab::HTTP).to receive(:get).and_raise(Errno::ECONNREFUSED)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'third party error'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
RSpec.describe ActivityPub::Projects::ReleasesSubscriptionWorker, feature_category: :release_orchestration do
|
||||||
|
describe '#perform' do
|
||||||
|
let(:worker) { described_class.new }
|
||||||
|
let(:project) { build_stubbed :project, :public }
|
||||||
|
let(:subscription) { build_stubbed :activity_pub_releases_subscription, project: project }
|
||||||
|
let(:inbox_resolver_service) { instance_double('ActivityPub::InboxResolverService', execute: true) }
|
||||||
|
let(:accept_follow_service) { instance_double('ActivityPub::AcceptFollowService', execute: true) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(ActivityPub::ReleasesSubscription).to receive(:find_by_id) { subscription }
|
||||||
|
allow(subscription).to receive(:destroy).and_return(true)
|
||||||
|
allow(ActivityPub::InboxResolverService).to receive(:new) { inbox_resolver_service }
|
||||||
|
allow(ActivityPub::AcceptFollowService).to receive(:new) { accept_follow_service }
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the project is public' do
|
||||||
|
before do
|
||||||
|
worker.perform(subscription.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when inbox url has not been resolved yet' do
|
||||||
|
it 'calls the service to resolve the inbox url' do
|
||||||
|
expect(inbox_resolver_service).to have_received(:execute)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls the service to send out the Accept activity' do
|
||||||
|
expect(accept_follow_service).to have_received(:execute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when inbox url has been resolved' do
|
||||||
|
context 'when shared inbox url has not been resolved' do
|
||||||
|
let(:subscription) { build_stubbed :activity_pub_releases_subscription, :inbox, project: project }
|
||||||
|
|
||||||
|
it 'calls the service to resolve the inbox url' do
|
||||||
|
expect(inbox_resolver_service).to have_received(:execute)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls the service to send out the Accept activity' do
|
||||||
|
expect(accept_follow_service).to have_received(:execute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when shared inbox url has been resolved' do
|
||||||
|
let(:subscription) do
|
||||||
|
build_stubbed :activity_pub_releases_subscription, :inbox, :shared_inbox, project: project
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not call the service to resolve the inbox url' do
|
||||||
|
expect(inbox_resolver_service).not_to have_received(:execute)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls the service to send out the Accept activity' do
|
||||||
|
expect(accept_follow_service).to have_received(:execute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'failed job' do
|
||||||
|
it 'does not resolve inbox url' do
|
||||||
|
expect(inbox_resolver_service).not_to have_received(:execute)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'does not send out Accept activity' do
|
||||||
|
expect(accept_follow_service).not_to have_received(:execute)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the subscription does not exist' do
|
||||||
|
before do
|
||||||
|
allow(ActivityPub::ReleasesSubscription).to receive(:find_by_id).and_return(nil)
|
||||||
|
worker.perform(subscription.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it_behaves_like 'failed job'
|
||||||
|
end
|
||||||
|
|
||||||
|
shared_examples 'non public project' do
|
||||||
|
it_behaves_like 'failed job'
|
||||||
|
|
||||||
|
it 'deletes the subscription' do
|
||||||
|
expect(subscription).to have_received(:destroy)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project has changed to internal' do
|
||||||
|
before do
|
||||||
|
worker.perform(subscription.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:project) { build_stubbed :project, :internal }
|
||||||
|
|
||||||
|
it_behaves_like 'non public project'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when project has changed to private' do
|
||||||
|
before do
|
||||||
|
worker.perform(subscription.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
let(:project) { build_stubbed :project, :private }
|
||||||
|
|
||||||
|
it_behaves_like 'non public project'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#sidekiq_retries_exhausted' do
|
||||||
|
let(:project) { build_stubbed :project, :public }
|
||||||
|
let(:subscription) { build_stubbed :activity_pub_releases_subscription, project: project }
|
||||||
|
let(:job) { { 'args' => [project.id, subscription.id], 'error_message' => 'Error' } }
|
||||||
|
|
||||||
|
before do
|
||||||
|
allow(Project).to receive(:find) { project }
|
||||||
|
allow(ActivityPub::ReleasesSubscription).to receive(:find_by_id) { subscription }
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'delete the subscription' do
|
||||||
|
expect(subscription).to receive(:destroy)
|
||||||
|
|
||||||
|
described_class.sidekiq_retries_exhausted_block.call(job, StandardError.new)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Reference in New Issue