diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 04ac2abf2..22e302ca5 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -411,7 +411,7 @@ + pattern="{{ GITHOST_REGEX }}">
The GitHub Enterprise endpoint. Must start with http:// or https://. @@ -524,7 +524,6 @@
-
@@ -565,7 +564,7 @@ + pattern="{{ GITHOST_REGEX }}">
The GitHub Enterprise endpoint. Must start with http:// or https://. @@ -589,6 +588,115 @@
+ + +
+
+ BitBucket Build Triggers +
+
+
+

+ If enabled, users can setup BitBucket triggers to invoke Registry builds. +

+

+ Note: A registered BitBucket OAuth application is required. + View instructions on how to + + Create an OAuth Application in BitBucket + +

+
+ +
+ + +
+ + + + + + + + + + +
OAuth Consumer Key: + + +
OAuth Consumer Secret: + + +
+
+
+ + +
+
+ GitLab Build Triggers +
+
+
+

+ If enabled, users can setup GitLab triggers to invoke Registry builds. +

+

+ Note: A registered GitLab OAuth application is required. + View instructions on how to + + Create an OAuth Application in GitLab + +

+
+ +
+ + +
+ + + + + + + + + + + + + + + + + + +
GitLab: + +
GitLab Endpoint: + + +
+ The GitLab Enterprise endpoint. Must start with http:// or https://. +
+
OAuth Client ID: + + +
OAuth Client Secret: + + +
+
+
+ diff --git a/static/js/core-config-setup.js b/static/js/core-config-setup.js index 569169201..033bbfbb6 100644 --- a/static/js/core-config-setup.js +++ b/static/js/core-config-setup.js @@ -12,7 +12,7 @@ angular.module("core-config-setup", ['angularFileUpload']) }, controller: function($rootScope, $scope, $element, $timeout, ApiService) { $scope.HOSTNAME_REGEX = '^[a-zA-Z-0-9\.]+(:[0-9]+)?$'; - $scope.GITHUB_REGEX = '^https?://([a-zA-Z0-9]+\.?\/?)+$'; + $scope.GITHOST_REGEX = '^https?://([a-zA-Z0-9]+\.?\/?)+$'; $scope.SERVICES = [ {'id': 'redis', 'title': 'Redis'}, @@ -39,8 +39,16 @@ angular.module("core-config-setup", ['angularFileUpload']) return config.FEATURE_GOOGLE_LOGIN; }}, - {'id': 'github-trigger', 'title': 'Github (Enterprise) Build Triggers', 'condition': function(config) { + {'id': 'github-trigger', 'title': 'GitHub (Enterprise) Build Triggers', 'condition': function(config) { return config.FEATURE_GITHUB_BUILD; + }}, + + {'id': 'bitbucket-trigger', 'title': 'BitBucket Build Triggers', 'condition': function(config) { + return config.FEATURE_BITBUCKET_BUILD; + }}, + + {'id': 'gitlab-trigger', 'title': 'GitLab Build Triggers', 'condition': function(config) { + return config.FEATURE_GITLAB_BUILD; }} ]; @@ -184,6 +192,24 @@ angular.module("core-config-setup", ['angularFileUpload']) }, ApiService.errorDisplay('Could not save configuration. Please report this error.')); }; + var gitlabSelector = function(key) { + return function(value) { + if (!value || !$scope.config) { return; } + + if (!$scope.config[key]) { + $scope.config[key] = {}; + } + + if (value == 'enterprise') { + if ($scope.config[key]['GITLAB_ENDPOINT'] == 'https://gitlab.com/') { + $scope.config[key]['GITLAB_ENDPOINT'] = ''; + } + } else if (value == 'hosted') { + $scope.config[key]['GITLAB_ENDPOINT'] = 'https://gitlab.com/'; + } + }; + }; + var githubSelector = function(key) { return function(value) { if (!value || !$scope.config) { return; } @@ -226,6 +252,9 @@ angular.module("core-config-setup", ['angularFileUpload']) $scope.mapped['GITHUB_LOGIN_KIND'] = gle == 'https://github.com/' ? 'hosted' : 'enterprise'; $scope.mapped['GITHUB_TRIGGER_KIND'] = gte == 'https://github.com/' ? 'hosted' : 'enterprise'; + var glabe = getKey(config, 'GITLAB_TRIGGER_KIND.GITHUB_ENDPOINT'); + $scope.mapped['GITLAB_TRIGGER_KIND'] = glabe == 'https://gitlab.com/' ? 'hosted' : 'enterprise'; + $scope.mapped['redis'] = {}; $scope.mapped['redis']['host'] = getKey(config, 'BUILDLOGS_REDIS.host') || getKey(config, 'USER_EVENTS_REDIS.host'); $scope.mapped['redis']['port'] = getKey(config, 'BUILDLOGS_REDIS.port') || getKey(config, 'USER_EVENTS_REDIS.port'); @@ -258,6 +287,7 @@ angular.module("core-config-setup", ['angularFileUpload']) // Add mapped logic. $scope.$watch('mapped.GITHUB_LOGIN_KIND', githubSelector('GITHUB_LOGIN_CONFIG')); $scope.$watch('mapped.GITHUB_TRIGGER_KIND', githubSelector('GITHUB_TRIGGER_CONFIG')); + $scope.$watch('mapped.GITLAB_TRIGGER_KIND', gitlabSelector('GITLAB_TRIGGER_KIND')); $scope.$watch('mapped.redis.host', redisSetter('host')); $scope.$watch('mapped.redis.port', redisSetter('port')); diff --git a/util/config/validator.py b/util/config/validator.py index aff6faadb..ab1ee696d 100644 --- a/util/config/validator.py +++ b/util/config/validator.py @@ -12,9 +12,10 @@ from flask import Flask from flask.ext.mail import Mail, Message from data.database import validate_database_url, User from storage import get_storage_driver -from app import app, CONFIG_PROVIDER +from app import app, CONFIG_PROVIDER, get_app_url from auth.auth_context import get_authenticated_user -from util.oauth import GoogleOAuthConfig, GithubOAuthConfig +from util.oauth import GoogleOAuthConfig, GithubOAuthConfig, GitLabOAuthConfig +from bitbucket import BitBucket logger = logging.getLogger(__name__) @@ -99,6 +100,32 @@ def _validate_mailing(config): test_mail.send(test_msg) +def _validate_gitlab(config): + """ Validates the OAuth credentials and API endpoint for a GitLab service. """ + github_config = config.get('GITLAB_TRIGGER_CONFIG') + if not github_config: + raise Exception('Missing GitLab client id and client secret') + + endpoint = github_config.get('GITLAB_ENDPOINT') + if not endpoint: + raise Exception('Missing GitLab Endpoint') + + if endpoint.find('http://') != 0 and endpoint.find('https://') != 0: + raise Exception('GitLab Endpoint must start with http:// or https://') + + if not github_config.get('CLIENT_ID'): + raise Exception('Missing Client ID') + + if not github_config.get('CLIENT_SECRET'): + raise Exception('Missing Client Secret') + + client = app.config['HTTPCLIENT'] + oauth = GitLabOAuthConfig(config, 'GITLAB_TRIGGER_CONFIG') + result = oauth.validate_client_id_and_secret(client, app.config) + if not result: + raise Exception('Invalid client id or client secret') + + def _validate_github(config_key): return lambda config: _validate_github_with_key(config_key, config) @@ -107,11 +134,11 @@ def _validate_github_with_key(config_key, config): """ Validates the OAuth credentials and API endpoint for a Github service. """ github_config = config.get(config_key) if not github_config: - raise Exception('Missing Github client id and client secret') + raise Exception('Missing GitHub client id and client secret') endpoint = github_config.get('GITHUB_ENDPOINT') if not endpoint: - raise Exception('Missing Github Endpoint') + raise Exception('Missing GitHub Endpoint') if endpoint.find('http://') != 0 and endpoint.find('https://') != 0: raise Exception('Github Endpoint must start with http:// or https://') @@ -127,7 +154,7 @@ def _validate_github_with_key(config_key, config): client = app.config['HTTPCLIENT'] oauth = GithubOAuthConfig(config, config_key) - result = oauth.validate_client_id_and_secret(client) + result = oauth.validate_client_id_and_secret(client, app.config) if not result: raise Exception('Invalid client id or client secret') @@ -137,6 +164,28 @@ def _validate_github_with_key(config_key, config): raise Exception('Invalid organization: %s' % org_id) +def _validate_bitbucket(config): + """ Validates the config for BitBucket. """ + trigger_config = config.get('BITBUCKET_TRIGGER_CONFIG') + if not trigger_config: + raise Exception('Missing client ID and client secret') + + if not trigger_config.get('CONSUMER_KEY'): + raise Exception('Missing Consumer Key') + + if not trigger_config.get('CONSUMER_SECRET'): + raise Exception('Missing Consumer Secret') + + key = trigger_config['CONSUMER_KEY'] + secret = trigger_config['CONSUMER_SECRET'] + callback_url = '%s/oauth1/bitbucket/callback/trigger/' % (get_app_url()) + + bitbucket_client = BitBucket(key, secret, callback_url) + (result, _, _) = bitbucket_client.get_authorization_url() + if not result: + raise Exception('Invaid consumer key or secret') + + def _validate_google_login(config): """ Validates the Google Login client ID and secret. """ google_login_config = config.get('GOOGLE_LOGIN_CONFIG') @@ -151,7 +200,7 @@ def _validate_google_login(config): client = app.config['HTTPCLIENT'] oauth = GoogleOAuthConfig(config, 'GOOGLE_LOGIN_CONFIG') - result = oauth.validate_client_id_and_secret(client) + result = oauth.validate_client_id_and_secret(client, app.config) if not result: raise Exception('Invalid client id or client secret') @@ -261,6 +310,8 @@ _VALIDATORS = { 'mail': _validate_mailing, 'github-login': _validate_github('GITHUB_LOGIN_CONFIG'), 'github-trigger': _validate_github('GITHUB_TRIGGER_CONFIG'), + 'gitlab-trigger': _validate_gitlab, + 'bitbucket-trigger': _validate_bitbucket, 'google-login': _validate_google_login, 'ssl': _validate_ssl, 'ldap': _validate_ldap, diff --git a/util/oauth.py b/util/oauth.py index dfae97a2f..33c9330d1 100644 --- a/util/oauth.py +++ b/util/oauth.py @@ -15,7 +15,7 @@ class OAuthConfig(object): def user_endpoint(self): raise NotImplementedError - def validate_client_id_and_secret(self, http_client): + def validate_client_id_and_secret(self, http_client, app_config): raise NotImplementedError def client_id(self): @@ -30,6 +30,13 @@ class OAuthConfig(object): return endpoint + def get_redirect_uri(self, app_config, redirect_suffix=''): + return '%s://%s/oauth2/%s/callback%s' % (app_config['PREFERRED_URL_SCHEME'], + app_config['SERVER_HOSTNAME'], + self.service_name().lower(), + redirect_suffix) + + def exchange_code_for_token(self, app_config, http_client, code, form_encode=False, redirect_suffix=''): payload = { @@ -37,10 +44,7 @@ class OAuthConfig(object): 'client_secret': self.client_secret(), 'code': code, 'grant_type': 'authorization_code', - 'redirect_uri': '%s://%s/oauth2/%s/callback%s' % (app_config['PREFERRED_URL_SCHEME'], - app_config['SERVER_HOSTNAME'], - self.service_name().lower(), - redirect_suffix) + 'redirect_uri': self.get_redirect_uri(app_config, redirect_suffix) } headers = { @@ -114,7 +118,7 @@ class GithubOAuthConfig(OAuthConfig): api_endpoint = self._api_endpoint() return self._get_url(api_endpoint, 'user/orgs') - def validate_client_id_and_secret(self, http_client): + def validate_client_id_and_secret(self, http_client, app_config): # First: Verify that the github endpoint is actually Github by checking for the # X-GitHub-Request-Id here. api_endpoint = self._api_endpoint() @@ -176,7 +180,7 @@ class GoogleOAuthConfig(OAuthConfig): def user_endpoint(self): return 'https://www.googleapis.com/oauth2/v1/userinfo' - def validate_client_id_and_secret(self, http_client): + def validate_client_id_and_secret(self, http_client, app_config): # To verify the Google client ID and secret, we hit the # https://www.googleapis.com/oauth2/v3/token endpoint with an invalid request. If the client # ID or secret are invalid, we get returned a 403 Unauthorized. Otherwise, we get returned @@ -219,8 +223,24 @@ class GitLabOAuthConfig(OAuthConfig): def token_endpoint(self): return self._get_url(self._endpoint(), '/oauth/token') - def validate_client_id_and_secret(self, http_client): - pass + def validate_client_id_and_secret(self, http_client, app_config): + url = self.token_endpoint() + redirect_uri = self.get_redirect_uri(app_config, redirect_suffix='trigger') + data = { + 'code': 'fakecode', + 'client_id': self.client_id(), + 'client_secret': self.client_secret(), + 'grant_type': 'authorization_code', + 'redirect_uri': redirect_uri + } + + # We validate by checking the error code we receive from this call. + result = http_client.post(url, data=data, timeout=5) + value = result.json() + if not value: + return False + + return value.get('error', '') != 'invalid_client' def get_public_config(self): return {