From e4b659f1071036805569afb9f564d9b961d36a48 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 25 Mar 2015 18:43:12 -0400 Subject: [PATCH] Add support for encrypted client tokens via basic auth (for the docker CLI) and a feature flag to disable normal passwords --- auth/auth.py | 3 +- config.py | 4 ++ data/users.py | 26 ++++++++++ endpoints/api/user.py | 51 ++++++++++++++++++- endpoints/index.py | 4 +- .../directives/config/config-setup-tool.html | 30 ++++++++++- static/js/pages/user-admin.js | 15 ++++++ static/js/services/ui-service.js | 42 +++++++++++++++ static/partials/user-admin.html | 32 ++++++++++++ test/test_api_security.py | 23 ++++++++- 10 files changed, 222 insertions(+), 8 deletions(-) diff --git a/auth/auth.py b/auth/auth.py index 79e07e3be..30e2f68db 100644 --- a/auth/auth.py +++ b/auth/auth.py @@ -114,7 +114,8 @@ def _process_basic_auth(auth): logger.debug('Invalid robot or password for robot: %s' % credentials[0]) else: - authenticated = authentication.verify_user(credentials[0], credentials[1]) + (authenticated, error_message) = authentication.verify_user(credentials[0], credentials[1], + basic_auth=True) if authenticated: logger.debug('Successfully validated user: %s' % authenticated.username) diff --git a/config.py b/config.py index 339ffca34..2d50138af 100644 --- a/config.py +++ b/config.py @@ -165,6 +165,10 @@ class DefaultConfig(object): # Feature Flag: Whether users can be renamed FEATURE_USER_RENAME = False + # Feature Flag: Whether non-encrypted passwords (as opposed to encrypted tokens) can be used for + # basic auth. + FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH = False + BUILD_MANAGER = ('enterprise', {}) DISTRIBUTED_STORAGE_CONFIG = { diff --git a/data/users.py b/data/users.py index 9e01e4d45..c7a6128db 100644 --- a/data/users.py +++ b/data/users.py @@ -1,6 +1,7 @@ import ldap import logging +from flask.sessions import SecureCookieSessionInterface, BadSignature from util.validation import generate_valid_usernames from data import model @@ -138,5 +139,30 @@ class UserAuthentication(object): app.extensions['authentication'] = users return users + def verify_user(self, username_or_email, password, basic_auth=False): + # First try to decode the password as a signed token. + if basic_auth: + from app import app + import features + + ser = SecureCookieSessionInterface().get_signing_serializer(app) + + try: + token_data = ser.loads(password) + password = token_data.get('password', password) + except BadSignature: + # This is a normal password. + if features.REQUIRE_ENCRYPTED_BASIC_AUTH: + msg = ('Client login with passwords is disabled. Please generate a client token ' + + 'and use it in place of your password.') + return (None, msg) + + result = self.state.verify_user(username_or_email, password) + if result: + return (result, '') + else: + return (result, 'Invalid password') + + def __getattr__(self, name): return getattr(self.state, name, None) diff --git a/endpoints/api/user.py b/endpoints/api/user.py index 6c3cafd63..d10807940 100644 --- a/endpoints/api/user.py +++ b/endpoints/api/user.py @@ -1,9 +1,11 @@ import logging import json +from random import SystemRandom from flask import request from flask.ext.login import logout_user from flask.ext.principal import identity_changed, AnonymousIdentity +from flask.sessions import SecureCookieSessionInterface from peewee import IntegrityError from app import app, billing as stripe, authentication, avatar @@ -335,13 +337,58 @@ class PrivateRepositories(ApiResource): } +@resource('/v1/user/clientkey') +@internal_only +class ClientKey(ApiResource): + """ Operations for returning an encrypted key which can be used in place of a password + for the Docker client. """ + schemas = { + 'GenerateClientKey': { + 'id': 'GenerateClientKey', + 'type': 'object', + 'required': [ + 'password', + ], + 'properties': { + 'password': { + 'type': 'string', + 'description': 'The user\'s password', + }, + } + } + } + + @require_user_admin + @nickname('generateUserClientKey') + @validate_json_request('GenerateClientKey') + def post(self): + """ Return's the user's private client key. """ + username = get_authenticated_user().username + password = request.get_json()['password'] + + (result, error_message) = authentication.verify_user(username, password) + if not result: + raise request_error(message=error_message) + + ser = SecureCookieSessionInterface().get_signing_serializer(app) + data_to_sign = { + 'password': password, + 'nonce': SystemRandom().randint(0, 10000000000) + } + + encrypted = ser.dumps(data_to_sign) + return { + 'key': encrypted + } + + def conduct_signin(username_or_email, password): needs_email_verification = False invalid_credentials = False verified = None try: - verified = authentication.verify_user(username_or_email, password) + (verified, error_message) = authentication.verify_user(username_or_email, password) except model.TooManyUsersException as ex: raise license_error(exception=ex) @@ -407,7 +454,7 @@ class ConvertToOrganization(ApiResource): # Ensure that the sign in credentials work. admin_password = convert_data['adminPassword'] - admin_user = authentication.verify_user(admin_username, admin_password) + (admin_user, error_message) = authentication.verify_user(admin_username, admin_password) if not admin_user: raise request_error(reason='invaliduser', message='The admin user credentials are not valid') diff --git a/endpoints/index.py b/endpoints/index.py index 2df427601..e42696777 100644 --- a/endpoints/index.py +++ b/endpoints/index.py @@ -109,7 +109,7 @@ def create_user(): issue='robot-login-failure') if authentication.user_exists(username): - verified = authentication.verify_user(username, password) + (verified, error_message) = authentication.verify_user(username, password, basic_auth=True) if verified: # Mark that the user was logged in. event = userevents.get_event(username) @@ -121,7 +121,7 @@ def create_user(): event = userevents.get_event(username) event.publish_event_data('docker-cli', {'action': 'loginfailure'}) - abort(400, 'Invalid password.', issue='login-failure') + abort(400, error_message, issue='login-failure') elif not features.USER_CREATION: abort(400, 'User creation is disabled. Please speak to your administrator.') diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html index 6c774dcf7..9be7ddd37 100644 --- a/static/directives/config/config-setup-tool.html +++ b/static/directives/config/config-setup-tool.html @@ -34,7 +34,7 @@ - User Creation: + User Creation:
@@ -46,6 +46,23 @@
+ + Encrypted Client Tokens: + +
+ + +
+
+ If enabled, users will not be able to login from the Docker command + line with a non-encrypted password and must generate an encrypted + token to use. +
+
+ This feature is highly recommended for setups with LDAP authentication, as Docker currently stores passwords in plaintext on user's machines. +
+ + @@ -293,6 +310,16 @@

+
+ It is highly recommended to require encrypted client tokens. LDAP passwords used in the Docker client will be stored in plain-text! + Enable this requirement now. +
+ +
+ Note: The "Require Encrypted Client Tokens" feature is currently enabled which will + prevent LDAP passwords from being saved as plain-text by the Docker client. +
+ @@ -305,7 +332,6 @@
Authentication:
- diff --git a/static/js/pages/user-admin.js b/static/js/pages/user-admin.js index 6af34d264..43a56388a 100644 --- a/static/js/pages/user-admin.js +++ b/static/js/pages/user-admin.js @@ -196,6 +196,21 @@ }); }; + $scope.generateClientToken = function() { + var generateToken = function(password) { + var data = { + 'password': password + }; + + ApiService.generateUserClientKey(data).then(function(resp) { + $scope.generatedClientToken = resp['key']; + $('#clientTokenModal').modal({}); + }, ApiService.errorDisplay('Could not generate token')); + }; + + UIService.showPasswordDialog('Enter your password to generate a client token:', generateToken); + }; + $scope.detachExternalLogin = function(kind) { var params = { 'servicename': kind diff --git a/static/js/services/ui-service.js b/static/js/services/ui-service.js index 2e857e8fa..4b8464e9e 100644 --- a/static/js/services/ui-service.js +++ b/static/js/services/ui-service.js @@ -92,5 +92,47 @@ angular.module('quay').factory('UIService', [function() { return new CheckStateController(items, opt_checked); }; + uiService.showPasswordDialog = function(message, callback, opt_canceledCallback) { + var success = function() { + var password = $('#passDialogBox').val(); + $('#passDialogBox').val(''); + callback(password); + }; + + var canceled = function() { + $('#passDialogBox').val(''); + opt_canceledCallback && opt_canceledCallback(); + }; + + var box = bootbox.dialog({ + "message": message + + '' + + '' + + '', + "title": 'Please Verify', + "buttons": { + "verify": { + "label": "Verify", + "className": "btn-success", + "callback": success + }, + "close": { + "label": "Cancel", + "className": "btn-default", + "callback": canceled + } + } + }); + + box.bind('shown.bs.modal', function(){ + box.find("input").focus(); + box.find("form").submit(function() { + if (!$('#passDialogBox').val()) { return; } + box.modal('hide'); + success(); + }); + }); + }; + return uiService; }]); diff --git a/static/partials/user-admin.html b/static/partials/user-admin.html index 2e3249764..4d803689d 100644 --- a/static/partials/user-admin.html +++ b/static/partials/user-admin.html @@ -33,6 +33,7 @@
  • Account E-mail
  • Robot Accounts
  • Change Password
  • +
  • Client Token
  • External Logins
  • Authorized Applications
  • @@ -99,6 +100,16 @@ + +
    +
    Click the "Generate" button below to generate a client token that can be used in place of your password for the Docker + command line.
    + + +
    +
    @@ -370,6 +381,27 @@
    + + + + +
  • LDAP URI: