support for github / gitlab enterprise

This commit is contained in:
Chris
2025-12-10 17:17:37 -08:00
parent 15aad3ef26
commit beaa7e5048
10 changed files with 111 additions and 42 deletions

View File

@@ -6,7 +6,11 @@ class Providers::CreateGithubProvider
promises :provider
executed do |context|
client = Octokit::Client.new(access_token: context.provider.access_token)
client_options = { access_token: context.provider.access_token }
if context.provider.enterprise?
client_options[:api_endpoint] = "#{context.provider.api_base_url}/api/v3/"
end
client = Octokit::Client.new(client_options)
username = client.user[:login]
context.provider.auth = {
info: {
@@ -14,16 +18,23 @@ class Providers::CreateGithubProvider
}
}.to_json
if (client.scopes & EXPECTED_SCOPES).sort != EXPECTED_SCOPES.sort
message = "Invalid scopes. Please check that your personal access token has the following scopes: #{EXPECTED_SCOPES.join(", ")}"
context.fail_and_return!(message)
context.provider.errors.add(:access_token, message)
next
# Skip scope validation for enterprise (some GHE instances don't expose scopes)
unless context.provider.enterprise?
if (client.scopes & EXPECTED_SCOPES).sort != EXPECTED_SCOPES.sort
message = "Invalid scopes. Please check that your personal access token has the following scopes: #{EXPECTED_SCOPES.join(", ")}"
context.fail_and_return!(message)
context.provider.errors.add(:access_token, message)
next
end
end
context.provider.save!
rescue Octokit::Unauthorized
message = "Invalid access token"
context.provider.errors.add(:access_token, message)
context.fail_and_return!(message)
rescue Faraday::ConnectionFailed => e
message = "Could not connect to GitHub server: #{e.message}"
context.provider.errors.add(:registry_url, message)
context.fail_and_return!(message)
end
end

View File

@@ -1,14 +1,16 @@
class Providers::CreateGitlabProvider
EXPECTED_SCOPES = %w[ api read_repository read_registry write_registry ]
GITLAB_PAT_API_URL = "https://gitlab.com/api/v4/personal_access_tokens/self"
GITLAB_USER_API_URL = "https://gitlab.com/api/v4/user"
extend LightService::Action
expects :provider
promises :provider
executed do |context|
response = HTTParty.get(GITLAB_PAT_API_URL,
base_url = context.provider.api_base_url
pat_api_url = "#{base_url}/api/v4/personal_access_tokens/self"
user_api_url = "#{base_url}/api/v4/user"
response = HTTParty.get(pat_api_url,
headers: {
"Authorization" => "Bearer #{context.provider.access_token}"
},
@@ -20,15 +22,18 @@ class Providers::CreateGitlabProvider
next
end
if (response["scopes"] & EXPECTED_SCOPES).sort != EXPECTED_SCOPES.sort
message = "Invalid scopes. Please check that your personal access token has the following scopes: #{EXPECTED_SCOPES.join(", ")}"
context.provider.errors.add(:access_token, message)
context.fail_and_return!(message)
next
# Skip scope validation for enterprise (some instances may have different scope requirements)
unless context.provider.enterprise?
if (response["scopes"] & EXPECTED_SCOPES).sort != EXPECTED_SCOPES.sort
message = "Invalid scopes. Please check that your personal access token has the following scopes: #{EXPECTED_SCOPES.join(", ")}"
context.provider.errors.add(:access_token, message)
context.fail_and_return!(message)
next
end
end
# Get username data
response = HTTParty.get(GITLAB_USER_API_URL,
response = HTTParty.get(user_api_url,
headers: {
"Authorization" => "Bearer #{context.provider.access_token}"
},
@@ -43,5 +48,9 @@ class Providers::CreateGitlabProvider
context.provider.auth = body
context.provider.save!
rescue Errno::ECONNREFUSED, SocketError => e
message = "Could not connect to GitLab server: #{e.message}"
context.provider.errors.add(:registry_url, message)
context.fail_and_return!(message)
end
end

View File

@@ -28,8 +28,10 @@
class Provider < ApplicationRecord
attr_accessor :username_param
GITHUB_PROVIDER = "github"
GITHUB_API_BASE = "https://api.github.com"
CUSTOM_REGISTRY_PROVIDER = "container_registry"
GITLAB_PROVIDER = "gitlab"
GITLAB_API_BASE = "https://gitlab.com"
GIT_TYPE = "git"
REGISTRY_TYPE = "registry"
PROVIDER_TYPES = {
@@ -83,6 +85,20 @@ class Provider < ApplicationRecord
provider == GITLAB_PROVIDER
end
def enterprise?
(github? || gitlab?) && registry_url.present?
end
def api_base_url
if registry_url.present?
registry_url.chomp("/")
elsif github?
GITHUB_API_BASE
elsif gitlab?
GITLAB_API_BASE
end
end
def twitter_refresh_token!(token); end
def used!

View File

@@ -1,9 +1,9 @@
class Git::Client
def self.from_provider(provider:, repository_url:)
if provider.github?
Git::Github::Client.new(access_token: provider.access_token, repository_url:)
Git::Github::Client.new(access_token: provider.access_token, repository_url:, api_base_url: provider.api_base_url)
elsif provider.gitlab?
Git::Gitlab::Client.new(access_token: provider.access_token, repository_url:)
Git::Gitlab::Client.new(access_token: provider.access_token, repository_url:, api_base_url: provider.api_base_url)
else
raise "Unsupported Git provider: #{provider}"
end

View File

@@ -4,9 +4,11 @@ class Git::Github::Client < Git::Client
attr_accessor :client, :repository_url
def self.from_project(project)
provider = project.project_credential_provider.provider
new(
access_token: project.project_credential_provider.access_token,
repository_url: project.repository_url
access_token: provider.access_token,
repository_url: project.repository_url,
api_base_url: provider.api_base_url
)
end
@@ -26,8 +28,12 @@ class Git::Github::Client < Git::Client
end
end
def initialize(access_token:, repository_url:)
@client = Octokit::Client.new(access_token:)
def initialize(access_token:, repository_url:, api_base_url: nil)
client_options = { access_token: }
if api_base_url && api_base_url != "https://api.github.com"
client_options[:api_endpoint] = "#{api_base_url}/api/v3/"
end
@client = Octokit::Client.new(client_options)
@repository_url = repository_url
end

View File

@@ -1,19 +1,25 @@
class Git::Gitlab::Client < Git::Client
GITLAB_API_BASE = "https://gitlab.com/api/v4"
GITLAB_WEBHOOK_SECRET = ENV["GITLAB_WEBHOOK_SECRET"]
attr_accessor :access_token, :repository_url
attr_accessor :access_token, :repository_url, :api_base_url
def self.from_project(project)
raise "Project is not a GitLab project" unless project.project_credential_provider.provider.gitlab?
provider = project.project_credential_provider.provider
raise "Project is not a GitLab project" unless provider.gitlab?
new(
access_token: project.project_credential_provider.access_token,
repository_url: project.repository_url
access_token: provider.access_token,
repository_url: project.repository_url,
api_base_url: provider.api_base_url
)
end
def initialize(access_token:, repository_url:)
def initialize(access_token:, repository_url:, api_base_url: nil)
@access_token = access_token
@repository_url = repository_url
@api_base_url = api_base_url || "https://gitlab.com"
end
def gitlab_api_base
"#{@api_base_url}/api/v4"
end
def repository_exists?
@@ -22,7 +28,7 @@ class Git::Gitlab::Client < Git::Client
def commits(branch)
response = HTTParty.get(
"#{GITLAB_API_BASE}/projects/#{encoded_url}/repository/commits?ref=#{branch}",
"#{gitlab_api_base}/projects/#{encoded_url}/repository/commits?ref=#{branch}",
headers: { "Authorization" => "Bearer #{access_token}" }
)
unless response.success?
@@ -53,7 +59,7 @@ class Git::Gitlab::Client < Git::Client
return
end
response = HTTParty.post(
"#{GITLAB_API_BASE}/projects/#{encoded_url}/hooks",
"#{gitlab_api_base}/projects/#{encoded_url}/hooks",
headers: { "Authorization" => "Bearer #{access_token}", "Content-Type" => "application/json" },
body: {
url: Rails.application.routes.url_helpers.inbound_webhooks_gitlab_index_url,
@@ -71,7 +77,7 @@ class Git::Gitlab::Client < Git::Client
def webhooks
response = HTTParty.get(
"#{GITLAB_API_BASE}/projects/#{encoded_url}/hooks",
"#{gitlab_api_base}/projects/#{encoded_url}/hooks",
headers: { "Authorization" => "Bearer #{access_token}" },
format: :json
)
@@ -84,7 +90,7 @@ class Git::Gitlab::Client < Git::Client
def repository
@repository ||= begin
project_response = HTTParty.get(
"#{GITLAB_API_BASE}/projects/#{encoded_url}",
"#{gitlab_api_base}/projects/#{encoded_url}",
headers: { "Authorization" => "Bearer #{access_token}" }
)
end
@@ -105,7 +111,7 @@ class Git::Gitlab::Client < Git::Client
def remove_webhook!
if webhook_exists?
HTTParty.delete(
"#{GITLAB_API_BASE}/projects/#{encoded_url}/hooks/#{webhook['id']}",
"#{gitlab_api_base}/projects/#{encoded_url}/hooks/#{webhook['id']}",
headers: { "Authorization" => "Bearer #{access_token}" }
)
end
@@ -113,7 +119,7 @@ class Git::Gitlab::Client < Git::Client
def pull_requests
HTTParty.get(
"#{GITLAB_API_BASE}/projects/#{encoded_url}/merge_requests",
"#{gitlab_api_base}/projects/#{encoded_url}/merge_requests",
headers: { "Authorization" => "Bearer #{access_token}" }
).map do |row|
Git::Common::PullRequest.new(
@@ -131,7 +137,7 @@ class Git::Gitlab::Client < Git::Client
def pull_request_status(pr_number)
response = HTTParty.get(
"#{GITLAB_API_BASE}/projects/#{encoded_url}/merge_requests/#{pr_number}",
"#{gitlab_api_base}/projects/#{encoded_url}/merge_requests/#{pr_number}",
headers: { "Authorization" => "Bearer #{access_token}" }
)
return 'not_found' unless response.success?
@@ -141,7 +147,7 @@ class Git::Gitlab::Client < Git::Client
def get_file(file_path, branch)
response = HTTParty.get(
"#{GITLAB_API_BASE}/projects/#{encoded_url}/repository/files/#{URI.encode_www_form_component(file_path)}/raw?ref=#{branch}",
"#{gitlab_api_base}/projects/#{encoded_url}/repository/files/#{URI.encode_www_form_component(file_path)}/raw?ref=#{branch}",
headers: { "Authorization" => "Bearer #{access_token}" }
)
response.success? ? Git::Common::File.new(file_path, response.body, branch) : nil

View File

@@ -1,6 +1,17 @@
<%= form_with model: provider do |form| %>
<%= render "shared/error_messages", resource: form.object %>
<%= form.hidden_field :provider, value: params[:provider_type] %>
<% if params[:provider_type] == Provider::GITHUB_PROVIDER || params[:provider_type] == Provider::GITLAB_PROVIDER %>
<div class="form-control mt-1 w-full max-w-sm">
<label class="label">
<span class="label-text">Host URL <span class="text-gray-400">(optional, for Enterprise)</span></span>
</label>
<%= form.text_field :registry_url, class: "input input-bordered", placeholder: params[:provider_type] == Provider::GITHUB_PROVIDER ? "https://github.example.com" : "https://gitlab.example.com" %>
<label class="label">
<span class="label-text-alt text-gray-400">Leave blank for <%= params[:provider_type] == Provider::GITHUB_PROVIDER ? "github.com" : "gitlab.com" %></span>
</label>
</div>
<% end %>
<% if params[:provider_type] == Provider::CUSTOM_REGISTRY_PROVIDER || form.object.provider == Provider::CUSTOM_REGISTRY_PROVIDER %>
<div data-controller="registry-selector">
<div class="form-control mt-1 w-full max-w-sm">

View File

@@ -22,6 +22,9 @@
<% end %>
permissions.
</div>
<div class="mt-2 text-sm text-gray-500">
For GitHub Enterprise, enter your server's host URL below.
</div>
<% elsif params[:provider_type] == Provider::GITLAB_PROVIDER %>
<%= link_to "Find your Gitlab token →", "https://gitlab.com/-/user_settings/personal_access_tokens", target: "_blank", class: "text-sm text-gray-500 " %>
<div class="mt-2">
@@ -31,6 +34,9 @@
<% end %>
permissions.
</div>
<div class="mt-2 text-sm text-gray-500">
For GitLab Enterprise/Self-Managed, enter your server's host URL below.
</div>
<% end %>
</div>
</div>

View File

@@ -12,16 +12,19 @@ RSpec.describe Providers::CreateGitlabProvider do
JSON.parse(File.read(Rails.root.join('spec/resources/integrations/gitlab/user.json')))
end
let(:gitlab_pat_api_url) { "#{provider.api_base_url}/api/v4/personal_access_tokens/self" }
let(:gitlab_user_api_url) { "#{provider.api_base_url}/api/v4/user" }
describe '.execute' do
context 'when the access token is valid and has correct scopes' do
before do
stub_request(:get, Providers::CreateGitlabProvider::GITLAB_PAT_API_URL)
stub_request(:get, gitlab_pat_api_url)
.to_return(
status: 200,
body: personal_access_tokens_data.to_json,
headers: { 'Content-Type' => 'application/json' }
)
stub_request(:get, Providers::CreateGitlabProvider::GITLAB_USER_API_URL)
stub_request(:get, gitlab_user_api_url)
.to_return(
status: 200,
body: user_data.to_json,
@@ -38,7 +41,7 @@ RSpec.describe Providers::CreateGitlabProvider do
context 'when the access token is invalid' do
before do
stub_request(:get, Providers::CreateGitlabProvider::GITLAB_PAT_API_URL)
stub_request(:get, gitlab_pat_api_url)
.to_return(
status: 401,
body: { error: "Unauthorized" }.to_json,
@@ -60,7 +63,7 @@ RSpec.describe Providers::CreateGitlabProvider do
before do
error_response = personal_access_tokens_data.deep_dup
error_response["scopes"] = []
stub_request(:get, Providers::CreateGitlabProvider::GITLAB_PAT_API_URL)
stub_request(:get, gitlab_pat_api_url)
.to_return(
status: 200,
body: error_response.to_json,

View File

@@ -4,6 +4,7 @@ RSpec.describe Git::Gitlab::Client do
let(:access_token) { 'test_token' }
let(:repository_url) { 'czhu12/echo' }
let(:client) { described_class.new(access_token:, repository_url:) }
let(:gitlab_api_base) { "#{Provider::GITLAB_API_BASE}/api/v4" }
describe '#register_webhook!' do
let(:webhook_url) { 'http://localhost:3000/inbound_webhooks/gitlab' }
@@ -32,7 +33,7 @@ RSpec.describe Git::Gitlab::Client do
it 'creates a new webhook' do
expect(HTTParty).to receive(:post).with(
"#{described_class::GITLAB_API_BASE}/projects/#{client.encoded_url}/hooks",
"#{gitlab_api_base}/projects/#{client.encoded_url}/hooks",
headers: { "Authorization" => "Bearer #{access_token}", "Content-Type" => "application/json" },
body: {
url: webhook_url,
@@ -66,7 +67,7 @@ RSpec.describe Git::Gitlab::Client do
it 'deletes the webhook' do
expect(HTTParty).to receive(:delete).with(
"#{described_class::GITLAB_API_BASE}/projects/#{client.encoded_url}/hooks/#{webhook['id']}",
"#{gitlab_api_base}/projects/#{client.encoded_url}/hooks/#{webhook['id']}",
headers: { "Authorization" => "Bearer #{access_token}" }
)
@@ -96,7 +97,7 @@ RSpec.describe Git::Gitlab::Client do
context 'when file exists' do
before do
allow(HTTParty).to receive(:get).with(
"#{described_class::GITLAB_API_BASE}/projects/#{client.encoded_url}/repository/files/#{URI.encode_www_form_component(file_path)}/raw?ref=#{branch}",
"#{gitlab_api_base}/projects/#{client.encoded_url}/repository/files/#{URI.encode_www_form_component(file_path)}/raw?ref=#{branch}",
headers: { "Authorization" => "Bearer #{access_token}" }
).and_return(success_response)
end