Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-10-26 03:11:53 +00:00
parent 9a02cb2918
commit 40512a72df
11 changed files with 470 additions and 1 deletions

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
module ActivityPub
ThirdPartyError = Class.new(StandardError)
end

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 | |

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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