diff --git a/app/actions/providers/create_github_provider.rb b/app/actions/providers/create_github_provider.rb index 27b82b41..db5023db 100644 --- a/app/actions/providers/create_github_provider.rb +++ b/app/actions/providers/create_github_provider.rb @@ -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 diff --git a/app/actions/providers/create_gitlab_provider.rb b/app/actions/providers/create_gitlab_provider.rb index 3bce6750..bff9e8aa 100644 --- a/app/actions/providers/create_gitlab_provider.rb +++ b/app/actions/providers/create_gitlab_provider.rb @@ -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 diff --git a/app/models/provider.rb b/app/models/provider.rb index 4b650668..8cb7fa23 100644 --- a/app/models/provider.rb +++ b/app/models/provider.rb @@ -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! diff --git a/app/services/git/client.rb b/app/services/git/client.rb index 3a64382e..e5b6d4b6 100644 --- a/app/services/git/client.rb +++ b/app/services/git/client.rb @@ -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 diff --git a/app/services/git/github/client.rb b/app/services/git/github/client.rb index 9ba540b8..60e5892e 100644 --- a/app/services/git/github/client.rb +++ b/app/services/git/github/client.rb @@ -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 diff --git a/app/services/git/gitlab/client.rb b/app/services/git/gitlab/client.rb index a682c3ab..f239daed 100644 --- a/app/services/git/gitlab/client.rb +++ b/app/services/git/gitlab/client.rb @@ -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 diff --git a/app/views/providers/_form.html.erb b/app/views/providers/_form.html.erb index af6d7176..c385666e 100644 --- a/app/views/providers/_form.html.erb +++ b/app/views/providers/_form.html.erb @@ -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 %> +
+ + <%= form.text_field :registry_url, class: "input input-bordered", placeholder: params[:provider_type] == Provider::GITHUB_PROVIDER ? "https://github.example.com" : "https://gitlab.example.com" %> + +
+ <% end %> <% if params[:provider_type] == Provider::CUSTOM_REGISTRY_PROVIDER || form.object.provider == Provider::CUSTOM_REGISTRY_PROVIDER %>
diff --git a/app/views/providers/new.html.erb b/app/views/providers/new.html.erb index 692f787d..1bb1c067 100644 --- a/app/views/providers/new.html.erb +++ b/app/views/providers/new.html.erb @@ -22,6 +22,9 @@ <% end %> permissions.
+
+ For GitHub Enterprise, enter your server's host URL below. +
<% 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 " %>
@@ -31,6 +34,9 @@ <% end %> permissions.
+
+ For GitLab Enterprise/Self-Managed, enter your server's host URL below. +
<% end %>
diff --git a/spec/actions/providers/create_gitlab_provider_spec.rb b/spec/actions/providers/create_gitlab_provider_spec.rb index a9d7ea0e..d65cf781 100644 --- a/spec/actions/providers/create_gitlab_provider_spec.rb +++ b/spec/actions/providers/create_gitlab_provider_spec.rb @@ -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, diff --git a/spec/services/git/gitlab/client_spec.rb b/spec/services/git/gitlab/client_spec.rb index ad9fd412..c1718175 100644 --- a/spec/services/git/gitlab/client_spec.rb +++ b/spec/services/git/gitlab/client_spec.rb @@ -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