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.
+