diff --git a/app.py b/app.py index 3655936a3..f00cc23db 100644 --- a/app.py +++ b/app.py @@ -19,7 +19,7 @@ from util.exceptionlog import Sentry from util.queuemetrics import QueueMetrics from util.names import urn_generator from util.oauth import GoogleOAuthConfig, GithubOAuthConfig -from util.configutil import import_yaml, generate_secret_key +from util.config.configutil import import_yaml, generate_secret_key from data.billing import Billing from data.buildlogs import BuildLogs from data.archivedlogs import LogArchive @@ -124,9 +124,9 @@ queue_metrics = QueueMetrics(app) authentication = UserAuthentication(app) userevents = UserEventsBuilderModule(app) -github_login = GithubOAuthConfig(app, 'GITHUB_LOGIN_CONFIG') -github_trigger = GithubOAuthConfig(app, 'GITHUB_TRIGGER_CONFIG') -google_login = GoogleOAuthConfig(app, 'GOOGLE_LOGIN_CONFIG') +github_login = GithubOAuthConfig(app.config, 'GITHUB_LOGIN_CONFIG') +github_trigger = GithubOAuthConfig(app.config, 'GITHUB_TRIGGER_CONFIG') +google_login = GoogleOAuthConfig(app.config, 'GOOGLE_LOGIN_CONFIG') oauth_apps = [github_login, github_trigger, google_login] tf = app.config['DB_TRANSACTION_FACTORY'] diff --git a/endpoints/api/suconfig.py b/endpoints/api/suconfig.py index 4bd6ca8b2..cc5910ae6 100644 --- a/endpoints/api/suconfig.py +++ b/endpoints/api/suconfig.py @@ -9,18 +9,17 @@ from endpoints.api import (ApiResource, nickname, resource, internal_only, show_ from endpoints.common import common_login from app import app, OVERRIDE_CONFIG_YAML_FILENAME, OVERRIDE_CONFIG_DIRECTORY from data import model -from data.database import User, validate_database_url from auth.permissions import SuperUserPermission from auth.auth_context import get_authenticated_user -from util.configutil import (import_yaml, export_yaml, add_enterprise_config_defaults, - set_config_value) +from data.database import User +from util.config.configutil import (import_yaml, export_yaml, add_enterprise_config_defaults, + set_config_value) +from util.config.validator import validate_service_for_config, SSL_FILENAMES import features logger = logging.getLogger(__name__) -CONFIG_FILE_WHITELIST = ['ssl.key', 'ssl.cert'] - def database_is_valid(): try: User.select().limit(1) @@ -131,7 +130,7 @@ class SuperUserConfigFile(ApiResource): @nickname('scConfigFileExists') def get(self, filename): """ Returns whether the configuration file with the given name exists. """ - if not filename in CONFIG_FILE_WHITELIST: + if not filename in SSL_FILENAMES: abort(404) if SuperUserPermission().can(): @@ -260,19 +259,6 @@ class SuperUserConfigValidate(ApiResource): # this is also safe since this method does not access any information not given in the request. if not os.path.exists(OVERRIDE_CONFIG_YAML_FILENAME) or SuperUserPermission().can(): config = request.get_json()['config'] - if service == 'database': - try: - validate_database_url(config['DB_URI']) - return { - 'status': True - } - except Exception as ex: - logger.exception('Could not validate database') - return { - 'status': False, - 'reason': str(ex) - } - - return {} + return validate_service_for_config(service, config) abort(403) \ No newline at end of file diff --git a/static/css/core-ui.css b/static/css/core-ui.css index bfe58608c..570492b4b 100644 --- a/static/css/core-ui.css +++ b/static/css/core-ui.css @@ -231,6 +231,31 @@ width: 400px; } +.config-contact-field { + margin-bottom: 4px; +} + +.config-contact-field .dropdown button { + width: 100px; + text-align: left; +} + +.config-contact-field .dropdown button .caret { + float: right; + margin-top: 9px; +} + +.config-contact-field .dropdown button i.fa { + margin-right: 6px; + width: 14px; + text-align: center; + display: inline-block; +} + +.config-contact-field .form-control { + width: 350px; +} + .config-list-field-element .empty { color: #ccc; margin-bottom: 10px; @@ -338,4 +363,64 @@ border-right: none; } +.co-floating-bottom-bar { + height: 50px; +} +.co-floating-bottom-bar.floating { + position: fixed; + bottom: 0px; +} + +.config-setup-tool .cor-floating-bottom-bar { + text-align: right; +} + +.config-setup-tool .cor-floating-bottom-bar button i.fa { + margin-right: 6px; +} + +.config-setup-tool .service-verification { + padding: 20px; + background: #343434; + color: white; + margin-bottom: -14px; +} + +.config-setup-tool .service-verification-row { + margin-bottom: 6px; +} + +.config-setup-tool .service-verification-row .service-title { + font-variant: small-caps; + font-size: 145%; + vertical-align: middle; +} + +#validateAndSaveModal .fa-warning { + font-size: 22px; + margin-right: 10px; + vertical-align: middle; + color: rgb(255, 186, 53); +} + +#validateAndSaveModal .fa-check-circle { + font-size: 22px; + margin-right: 10px; + vertical-align: middle; + color: rgb(53, 186, 53); +} + +.config-setup-tool .service-verification-error { + white-space: pre; + margin-top: 10px; + margin-left: 36px; + margin-bottom: 20px; + max-height: 250px; + overflow: auto; + border: 1px solid #797979; + background: black; + padding: 6px; + font-family: Consolas, "Lucida Console", Monaco, monospace; + font-size: 12px; +} diff --git a/static/css/quay.css b/static/css/quay.css index e3aced015..9d032549a 100644 --- a/static/css/quay.css +++ b/static/css/quay.css @@ -4907,12 +4907,12 @@ i.slack-icon { font-size: 18px; } -.initial-setup-modal .valid-database .verified { +.verified { font-size: 16px; margin-bottom: 16px; } -.initial-setup-modal .valid-database .verified i.fa { +.verified i.fa { font-size: 26px; margin-right: 10px; vertical-align: middle; @@ -4923,8 +4923,4 @@ i.slack-icon { border: 1px solid #eee; vertical-align: middle; padding: 4px; -} - -.config-contact-field .form-control { - width: 350px; } \ No newline at end of file diff --git a/static/directives/config/config-contact-field.html b/static/directives/config/config-contact-field.html index f1b98a4c1..ce79a405c 100644 --- a/static/directives/config/config-contact-field.html +++ b/static/directives/config/config-contact-field.html @@ -5,10 +5,10 @@ - - Build Support: - -
- - -
-
- If enabled, users can submit Dockerfiles to be built and pushed by the Enterprise Registry. -
- -
- Note: Build workers are required for this feature. - See Adding Build Workers for instructions on how to setup build workers. -
- - @@ -155,9 +138,6 @@ -
- -
@@ -186,122 +166,27 @@ - - - Storage Path: + + + {{ field.title }}: - - - - - - Access Key: - - - - - - Secret Key: - - - - - - Bucket Name: - - - - - - - - Access Key: - - - - - - Secret Key: - - - - - - Bucket Name: - - - - - - - - Hostname: - - - - - - Is Secure: - -
- - + binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]" + placeholder="{{ field.placeholder }}" + ng-if="field.kind == 'text'"> +
+ +
- - - - Access Key: - - -
- See - RADOS Documentation - for more information +
+ See Documentation for more information
- - Secret Key: - - - - - - Bucket Name: - - - -
-
- -
-
@@ -375,7 +260,8 @@ Password: - @@ -384,9 +270,6 @@ -
- -
@@ -446,10 +329,6 @@ - -
- -
@@ -513,11 +392,6 @@ - - -
- -
@@ -562,17 +436,34 @@ - - -
- -
+ +
+
+ Dockerfile Build Support +
+
+
+ If enabled, users can submit Dockerfiles to be built and pushed by the Enterprise Registry. +
+ +
+ + +
+ +
+ Note: Build workers are required for this feature. + See Adding Build Workers for instructions on how to setup build workers. +
+
+
+ -
+
Github (Enterprise) Build Triggers
@@ -631,12 +522,90 @@ - - -
- -
+ + +
+ +
+ + + + \ No newline at end of file diff --git a/static/directives/cor-floating-bottom-bar.html b/static/directives/cor-floating-bottom-bar.html new file mode 100644 index 000000000..2e5337fd2 --- /dev/null +++ b/static/directives/cor-floating-bottom-bar.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/static/js/core-config-setup.js b/static/js/core-config-setup.js index 1aff16ea9..d0d2bd8e6 100644 --- a/static/js/core-config-setup.js +++ b/static/js/core-config-setup.js @@ -10,8 +10,137 @@ angular.module("core-config-setup", ['angularFileUpload']) 'isActive': '=isActive' }, controller: function($rootScope, $scope, $element, $timeout, ApiService) { + $scope.SERVICES = [ + {'id': 'redis', 'title': 'Redis'}, + + {'id': 'registry-storage', 'title': 'Registry Storage'}, + + {'id': 'ssl', 'title': 'SSL certificate and key', 'condition': function(config) { + return config.PREFERRED_URL_SCHEME == 'https'; + }}, + + {'id': 'ldap', 'title': 'LDAP Authentication', 'condition': function(config) { + return config.AUTHENTICATION_TYPE == 'LDAP'; + }}, + + {'id': 'mail', 'title': 'E-mail Support', 'condition': function(config) { + return config.FEATURE_MAILING; + }}, + + {'id': 'github-login', 'title': 'Github (Enterprise) Authentication', 'condition': function(config) { + return config.FEATURE_GITHUB_LOGIN; + }} + ]; + + $scope.STORAGE_CONFIG_FIELDS = { + 'LocalStorage': [ + {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/some/directory', 'kind': 'text'} + ], + + 'S3Storage': [ + {'name': 's3_access_key', 'title': 'AWS Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text'}, + {'name': 's3_secret_key', 'title': 'AWS Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'}, + {'name': 's3_bucket', 'title': 'S3 Bucket', 'placeholder': 'my-cool-bucket', 'kind': 'text'}, + {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'} + ], + + 'GoogleCloudStorage': [ + {'name': 'access_key', 'title': 'Cloud Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text'}, + {'name': 'secret_key', 'title': 'Cloud Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'}, + {'name': 'bucket_name', 'title': 'GCS Bucket', 'placeholder': 'my-cool-bucket', 'kind': 'text'}, + {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'} + ], + + 'RadosGWStorage': [ + {'name': 'hostname', 'title': 'Rados Server Hostname', 'placeholder': 'my.rados.hostname', 'kind': 'text'}, + {'name': 'is_secure', 'title': 'Is Secure', 'placeholder': 'Require SSL', 'kind': 'bool'}, + {'name': 'access_key', 'title': 'Access Key', 'placeholder': 'accesskeyhere', 'kind': 'text', 'help_url': 'http://ceph.com/docs/master/radosgw/admin/'}, + {'name': 'secret_key', 'title': 'Secret Key', 'placeholder': 'secretkeyhere', 'kind': 'text'}, + {'name': 'bucket_name', 'title': 'Bucket Name', 'placeholder': 'my-cool-bucket', 'kind': 'text'}, + {'name': 'storage_path', 'title': 'Storage Directory', 'placeholder': '/path/inside/bucket', 'kind': 'text'} + ] + }; + $scope.config = null; - $scope.mapped = {}; + $scope.mapped = { + '$hasChanges': false + }; + + $scope.validating = null; + $scope.savingConfiguration = false; + + $scope.getServices = function(config) { + var services = []; + if (!config) { return services; } + + for (var i = 0; i < $scope.SERVICES.length; ++i) { + var service = $scope.SERVICES[i]; + if (!service.condition || service.condition(config)) { + services.push({ + 'service': service, + 'status': 'validating' + }); + } + } + + return services; + }; + + $scope.validationStatus = function(serviceInfos) { + if (!serviceInfos) { return 'validating'; } + + var hasError = false; + for (var i = 0; i < serviceInfos.length; ++i) { + if (serviceInfos[i].status == 'validating') { + return 'validating'; + } + if (serviceInfos[i].status == 'error') { + hasError = true; + } + } + + return hasError ? 'failed' : 'success'; + }; + + $scope.validateService = function(serviceInfo) { + var params = { + 'service': serviceInfo.service.id + }; + + ApiService.scValidateConfig({'config': $scope.config}, params).then(function(resp) { + serviceInfo.status = resp.status ? 'success' : 'error'; + serviceInfo.errorMessage = $.trim(resp.reason || ''); + }, ApiService.errorDisplay('Could not validate configuration. Please report this error.')); + }; + + $scope.validateAndSave = function() { + $scope.savingConfiguration = false; + $scope.validating = $scope.getServices($scope.config); + + $('#validateAndSaveModal').modal({ + keyboard: false, + backdrop: 'static' + }); + + for (var i = 0; i < $scope.validating.length; ++i) { + var serviceInfo = $scope.validating[i]; + $scope.validateService(serviceInfo); + } + }; + + $scope.saveConfiguration = function() { + $scope.savingConfiguration = true; + + var data = { + 'config': $scope.config, + 'hostname': window.location.host + }; + + ApiService.scUpdateConfig(data).then(function(resp) { + $scope.savingConfiguration = false; + $scope.mapped.$hasChanges = false + }, ApiService.errorDisplay('Could not save configuration. Please report this error.')); + }; var githubSelector = function(key) { return function(value) { @@ -36,8 +165,8 @@ angular.module("core-config-setup", ['angularFileUpload']) var current = config; for (var i = 0; i < parts.length; ++i) { var part = parts[i]; - if (!config[part]) { return null; } - current = config[part]; + if (!current[part]) { return null; } + current = current[part]; } return current; }; @@ -86,7 +215,36 @@ angular.module("core-config-setup", ['angularFileUpload']) $scope.$watch('mapped.redis.port', redisSetter('port')); $scope.$watch('mapped.redis.password', redisSetter('password')); + // Add a watch to remove any fields not allowed by the current storage configuration. + // We have to do this otherwise extra fields (which are not allowed) can end up in the + // configuration. + $scope.$watch('config.DISTRIBUTED_STORAGE_CONFIG.local[0]', function(value) { + // Remove any fields not associated with the current kind. + if (!value || !$scope.STORAGE_CONFIG_FIELDS[value] + || !$scope.config.DISTRIBUTED_STORAGE_CONFIG + || !$scope.config.DISTRIBUTED_STORAGE_CONFIG.local + || !$scope.config.DISTRIBUTED_STORAGE_CONFIG.local[1]) { return; } + + var allowedFields = $scope.STORAGE_CONFIG_FIELDS[value]; + var configObject = $scope.config.DISTRIBUTED_STORAGE_CONFIG.local[1]; + + for (var fieldName in configObject) { + if (!configObject.hasOwnProperty(fieldName)) { + continue; + } + + var isValidField = $.grep(allowedFields, function(field) { + return field.name == fieldName; + }).length > 0; + + if (!isValidField) { + delete configObject[fieldName]; + } + } + }); + $scope.$watch('config', function(value) { + $scope.mapped['$hasChanges'] = true; }, true); $scope.$watch('isActive', function(value) { @@ -95,6 +253,7 @@ angular.module("core-config-setup", ['angularFileUpload']) ApiService.scGetConfig().then(function(resp) { $scope.config = resp['config']; initializeMappedLogic($scope.config); + $scope.mapped['$hasChanges'] = false; }); }); } @@ -376,9 +535,7 @@ angular.module("core-config-setup", ['angularFileUpload']) 'binding': '=binding' }, controller: function($scope, $element) { - $scope.$watch('items', function(items) { - if (!items) { return; } - + var padItems = function(items) { // Remove the last item if both it and the second to last items are empty. if (items.length > 1 && !items[items.length - 2].value && !items[items.length - 1].value) { items.splice(items.length - 1, 1); @@ -386,14 +543,45 @@ angular.module("core-config-setup", ['angularFileUpload']) } // If the last item is non-empty, add a new item. - if (items[items.length - 1].value) { + if (items.length == 0 || items[items.length - 1].value) { items.push({'value': ''}); + return; } + }; + + $scope.itemHash = null; + $scope.$watch('items', function(items) { + if (!items) { return; } + padItems(items); + + var itemHash = ''; + var binding = []; + for (var i = 0; i < items.length; ++i) { + var item = items[i]; + if (item.value && (URI(item.value).host() || URI(item.value).path())) { + binding.push(item.value); + itemHash += item.value; + } + } + + $scope.itemHash = itemHash; + $scope.binding = binding; }, true); $scope.$watch('binding', function(binding) { - $scope.items = []; - $scope.items.push({'value': ''}); + if (!binding) { return; } + + var current = binding; + var items = []; + var itemHash = ''; + for (var i = 0; i < current.length; ++i) { + items.push({'value': current[i]}) + itemHash += current[i]; + } + + if ($scope.itemHash != itemHash) { + $scope.items = items; + } }); } }; @@ -416,6 +604,7 @@ angular.module("core-config-setup", ['angularFileUpload']) $scope.value = null; var updateBinding = function() { + if ($scope.value == null) { return; } var value = $scope.value || ''; switch ($scope.kind) { diff --git a/static/js/core-ui.js b/static/js/core-ui.js index 2cd547f4d..9a42fd8dc 100644 --- a/static/js/core-ui.js +++ b/static/js/core-ui.js @@ -175,6 +175,49 @@ angular.module("core-ui", []) return directiveDefinitionObject; }) + .directive('corFloatingBottomBar', function() { + var directiveDefinitionObject = { + priority: 3, + templateUrl: '/static/directives/cor-floating-bottom-bar.html', + replace: true, + transclude: true, + restrict: 'C', + scope: {}, + controller: function($rootScope, $scope, $element, $timeout, $interval) { + var handler = function() { + $element.removeClass('floating'); + $element.css('width', $element[0].parentNode.clientWidth + 'px'); + + var windowHeight = $(window).height(); + var rect = $element[0].getBoundingClientRect(); + if (rect.bottom > windowHeight) { + $element.addClass('floating'); + } + }; + + $(window).on("scroll", handler); + $(window).on("resize", handler); + + var previousHeight = $element[0].parentNode.clientHeight; + var stop = $interval(function() { + var currentHeight = $element[0].parentNode.clientWidth; + if (previousHeight != currentHeight) { + currentHeight = previousHeight; + handler(); + } + }, 100); + + $scope.$on('$destroy', function() { + $(window).off("resize", handler); + $(window).off("scroll", handler); + $internval.stop(stop); + }); + } + }; + return directiveDefinitionObject; + + }) + .directive('corTab', function() { var directiveDefinitionObject = { priority: 4, diff --git a/storage/__init__.py b/storage/__init__.py index 4d1134d4b..7893343c2 100644 --- a/storage/__init__.py +++ b/storage/__init__.py @@ -11,6 +11,14 @@ STORAGE_DRIVER_CLASSES = { 'RadosGWStorage': RadosGWStorage, } +def get_storage_driver(storage_params): + """ Returns a storage driver class for the given storage configuration + (a pair of string name and a dict of parameters). """ + driver = storage_params[0] + parameters = storage_params[1] + driver_class = STORAGE_DRIVER_CLASSES.get(driver, FakeStorage) + return driver_class(**parameters) + class Storage(object): def __init__(self, app=None): @@ -23,12 +31,7 @@ class Storage(object): def init_app(self, app): storages = {} for location, storage_params in app.config.get('DISTRIBUTED_STORAGE_CONFIG').items(): - driver = storage_params[0] - parameters = storage_params[1] - - driver_class = STORAGE_DRIVER_CLASSES.get(driver, FakeStorage) - storage = driver_class(**parameters) - storages[location] = storage + storages[location] = get_storage_driver(storage_params) preference = app.config.get('DISTRIBUTED_STORAGE_PREFERENCE', None) if not preference: diff --git a/util/config/__init__.py b/util/config/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/util/configutil.py b/util/config/configutil.py similarity index 100% rename from util/configutil.py rename to util/config/configutil.py diff --git a/util/config/validator.py b/util/config/validator.py new file mode 100644 index 000000000..16211c254 --- /dev/null +++ b/util/config/validator.py @@ -0,0 +1,122 @@ +import redis +import os +import json +import ldap + +from data.users import LDAPConnection +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, OVERRIDE_CONFIG_DIRECTORY +from auth.auth_context import get_authenticated_user +from util.oauth import GoogleOAuthConfig, GithubOAuthConfig + +SSL_FILENAMES = ['ssl.cert', 'ssl.key'] + +def validate_service_for_config(service, config): + """ Attempts to validate the configuration for the given service. """ + if not service in _VALIDATORS: + return { + 'status': False + } + + try: + _VALIDATORS[service](config) + return { + 'status': True + } + except Exception as ex: + return { + 'status': False, + 'reason': str(ex) + } + +def _validate_database(config): + """ Validates connecting to the database. """ + validate_database_url(config['DB_URI']) + +def _validate_redis(config): + """ Validates connecting to redis. """ + redis_config = config['BUILDLOGS_REDIS'] + client = redis.StrictRedis(socket_connect_timeout=5, **redis_config) + client.ping() + +def _validate_registry_storage(config): + """ Validates registry storage. """ + parameters = config.get('DISTRIBUTED_STORAGE_CONFIG', {}).get('local', ['LocalStorage', {}]) + try: + driver = get_storage_driver(parameters) + except TypeError: + raise Exception('Missing required storage configuration parameter(s)') + + # Put and remove a temporary file. + driver.put_content('_verify', 'testing 123') + driver.remove('_verify') + +def _validate_mailing(config): + """ Validates sending email. """ + test_app = Flask("mail-test-app") + test_app.config.update(config) + test_app.config.update({ + 'MAIL_FAIL_SILENTLY': False, + 'TESTING': False + }) + + test_mail = Mail(test_app) + test_msg = Message("Test e-mail from %s" % app.config['REGISTRY_TITLE']) + test_msg.add_recipient(get_authenticated_user().email) + test_mail.send(test_msg) + +def _validate_github_login(config): + """ Validates the OAuth credentials and API endpoint for Github Login. """ + client = app.config['HTTPCLIENT'] + oauth = GithubOAuthConfig(config, 'GITHUB_LOGIN_CONFIG') + endpoint = oauth.authorize_endpoint() + # TODO: this + + +def _validate_ssl(config): + """ Validates the SSL configuration (if enabled). """ + if config.get('PREFERRED_URL_SCHEME', 'http') != 'https': + return + + for filename in SSL_FILENAMES: + if not os.path.exists(os.path.join(OVERRIDE_CONFIG_DIRECTORY, filename)): + raise Exception('Missing required SSL file: %s' % filename) + + +def _validate_ldap(config): + """ Validates the LDAP connection. """ + if config.get('AUTHENTICATION_TYPE', 'Database') != 'LDAP': + return + + # Note: raises ldap.INVALID_CREDENTIALS on failure + admin_dn = config.get('LDAP_ADMIN_DN') + admin_passwd = config.get('LDAP_ADMIN_PASSWD') + + if not admin_dn: + raise Exception('Missing Admin DN for LDAP configuration') + + if not admin_passwd: + raise Exception('Missing Admin Password for LDAP configuration') + + ldap_uri = config.get('LDAP_URI', 'ldap://localhost') + + try: + with LDAPConnection(ldap_uri, admin_dn, admin_passwd): + pass + except ldap.LDAPError as ex: + values = ex.args[0] if ex.args else {} + raise Exception(values.get('desc', 'Unknown error')) + + +_VALIDATORS = { + 'database': _validate_database, + 'redis': _validate_redis, + 'registry-storage': _validate_registry_storage, + 'mail': _validate_mailing, + 'github-login': _validate_github_login, + 'ssl': _validate_ssl, + 'ldap': _validate_ldap, +} \ No newline at end of file diff --git a/util/oauth.py b/util/oauth.py index e0d38d395..65af5cd13 100644 --- a/util/oauth.py +++ b/util/oauth.py @@ -1,9 +1,9 @@ import urlparse class OAuthConfig(object): - def __init__(self, app, key_name): + def __init__(self, config, key_name): self.key_name = key_name - self.config = app.config.get(key_name) or {} + self.config = config.get(key_name) or {} def service_name(self): raise NotImplementedError @@ -23,6 +23,9 @@ class OAuthConfig(object): def client_secret(self): return self.config.get('CLIENT_SECRET') + def basic_scope(self): + raise NotImplementedError + def _get_url(self, endpoint, *args): for arg in args: endpoint = urlparse.urljoin(endpoint, arg) @@ -31,8 +34,8 @@ class OAuthConfig(object): class GithubOAuthConfig(OAuthConfig): - def __init__(self, app, key_name): - super(GithubOAuthConfig, self).__init__(app, key_name) + def __init__(self, config, key_name): + super(GithubOAuthConfig, self).__init__(config, key_name) def service_name(self): return 'GitHub' @@ -43,6 +46,9 @@ class GithubOAuthConfig(OAuthConfig): endpoint = endpoint + '/' return endpoint + def basic_scope(self): + return 'user:email' + def authorize_endpoint(self): return self._get_url(self._endpoint(), '/login/oauth/authorize') + '?' @@ -73,12 +79,15 @@ class GithubOAuthConfig(OAuthConfig): class GoogleOAuthConfig(OAuthConfig): - def __init__(self, app, key_name): - super(GoogleOAuthConfig, self).__init__(app, key_name) + def __init__(self, config, key_name): + super(GoogleOAuthConfig, self).__init__(config, key_name) def service_name(self): return 'Google' + def basic_scope(self): + return 'openid email' + def authorize_endpoint(self): return 'https://accounts.google.com/o/oauth2/auth?response_type=code&'