Merge branch 'master' into orgview

This commit is contained in:
Joseph Schorr 2015-04-01 13:56:49 -04:00
commit 5cd500257d
52 changed files with 387 additions and 62 deletions

View file

@ -38,19 +38,13 @@ ADD . .
# Run grunt
RUN cd grunt && grunt
ADD conf/init/svlogd_config /svlogd_config
ADD conf/init/doupdatelimits.sh /etc/my_init.d/
ADD conf/init/preplogsdir.sh /etc/my_init.d/
ADD conf/init/copy_syslog_config.sh /etc/my_init.d/
ADD conf/init/runmigration.sh /etc/my_init.d/
ADD conf/init/gunicorn_web /etc/service/gunicorn_web
ADD conf/init/gunicorn_registry /etc/service/gunicorn_registry
ADD conf/init/gunicorn_verbs /etc/service/gunicorn_verbs
ADD conf/init/nginx /etc/service/nginx
ADD conf/init/diffsworker /etc/service/diffsworker
ADD conf/init/notificationworker /etc/service/notificationworker
ADD conf/init/buildlogsarchiver /etc/service/buildlogsarchiver
ADD conf/init/buildmanager /etc/service/buildmanager
ADD conf/init/service/ /etc/service/
RUN rm -rf /etc/service/syslog-forwarder
# Download any external libs.
RUN mkdir static/fonts static/ldn

1
app.py
View file

@ -39,7 +39,6 @@ OVERRIDE_CONFIG_YAML_FILENAME = 'conf/stack/config.yaml'
OVERRIDE_CONFIG_PY_FILENAME = 'conf/stack/config.py'
OVERRIDE_CONFIG_KEY = 'QUAY_OVERRIDE_CONFIG'
LICENSE_FILENAME = 'conf/stack/license.enc'
CONFIG_PROVIDER = FileConfigProvider(OVERRIDE_CONFIG_DIRECTORY, 'config.yaml', 'config.py')

View file

@ -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)

View file

@ -157,8 +157,12 @@ class EphemeralBuilderManager(BaseManager):
etcd_host = self._manager_config.get('ETCD_HOST', '127.0.0.1')
etcd_port = self._manager_config.get('ETCD_PORT', 2379)
etcd_auth = self._manager_config.get('ETCD_CERT_AND_KEY', None)
etcd_ca_cert = self._manager_config.get('ETCD_CA_CERT', None)
etcd_auth = self._manager_config.get('ETCD_CERT_AND_KEY', None)
if etcd_auth is not None:
etcd_auth = tuple(etcd_auth) # Convert YAML list to a tuple
etcd_protocol = 'http' if etcd_auth is None else 'https'
logger.debug('Connecting to etcd on %s:%s', etcd_host, etcd_port)

View file

@ -69,6 +69,7 @@ class BuilderExecutor(object):
manager_hostname=manager_hostname,
coreos_channel=coreos_channel,
worker_tag=self.executor_config['WORKER_TAG'],
logentries_token=self.executor_config.get('LOGENTRIES_TOKEN', None),
)

View file

@ -12,6 +12,9 @@ write_files:
REALM={{ realm }}
TOKEN={{ token }}
SERVER=wss://{{ manager_hostname }}
{% if logentries_token -%}
LOGENTRIES_TOKEN={{ logentries_token }}
{%- endif %}
coreos:
update:
@ -19,6 +22,17 @@ coreos:
group: {{ coreos_channel }}
units:
- name: systemd-journal-gatewayd.socket
command: start
enable: yes
content: |
[Unit]
Description=Journal Gateway Service Socket
[Socket]
ListenStream=/var/run/journald.sock
Service=systemd-journal-gatewayd.service
[Install]
WantedBy=sockets.target
{{ dockersystemd('quay-builder',
'quay.io/coreos/registry-build-worker',
quay_username,
@ -29,3 +43,10 @@ coreos:
flattened=True,
restart_policy='no'
) | indent(4) }}
{% if logentries_token -%}
{{ dockersystemd('builder-logs',
'quay.io/kelseyhightower/journal-2-logentries',
extra_args='--env-file /root/overrides.list -v /run/journald.sock:/run/journald.sock',
after_units=['quay-builder.service']
) | indent(4) }}
{%- endif %}

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="146" height="18"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient><rect rx="4" width="146" height="18" fill="#555"/><rect rx="4" x="92" width="54" height="18" fill="#dfb317"/><path fill="#dfb317" d="M92 0h4v18h-4z"/><rect rx="4" width="146" height="18" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="47" y="13" fill="#010101" fill-opacity=".3">Docker Image</text><text x="47" y="12">Docker Image</text><text x="118" y="13" fill="#010101" fill-opacity=".3">building</text><text x="118" y="12">building</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="117" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="117" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h63v20H0z"/><path fill="#dfb317" d="M63 0h54v20H63z"/><path fill="url(#b)" d="M0 0h117v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="32.5" y="15" fill="#010101" fill-opacity=".3">container</text><text x="32.5" y="14">container</text><text x="89" y="15" fill="#010101" fill-opacity=".3">building</text><text x="89" y="14">building</text></g></svg>

Before

Width:  |  Height:  |  Size: 836 B

After

Width:  |  Height:  |  Size: 748 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="164" height="18"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient><rect rx="4" width="164" height="18" fill="#555"/><rect rx="4" x="92" width="72" height="18" fill="#e05d44"/><path fill="#e05d44" d="M92 0h4v18h-4z"/><rect rx="4" width="164" height="18" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="47" y="13" fill="#010101" fill-opacity=".3">Docker Image</text><text x="47" y="12">Docker Image</text><text x="127" y="13" fill="#010101" fill-opacity=".3">build failed</text><text x="127" y="12">build failed</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="104" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="104" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h63v20H0z"/><path fill="#e05d44" d="M63 0h41v20H63z"/><path fill="url(#b)" d="M0 0h104v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="32.5" y="15" fill="#010101" fill-opacity=".3">container</text><text x="32.5" y="14">container</text><text x="82.5" y="15" fill="#010101" fill-opacity=".3">failed</text><text x="82.5" y="14">failed</text></g></svg>

Before

Width:  |  Height:  |  Size: 844 B

After

Width:  |  Height:  |  Size: 748 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="130" height="18"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient><rect rx="4" width="130" height="18" fill="#555"/><rect rx="4" x="92" width="38" height="18" fill="#9f9f9f"/><path fill="#9f9f9f" d="M92 0h4v18h-4z"/><rect rx="4" width="130" height="18" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="47" y="13" fill="#010101" fill-opacity=".3">Docker Image</text><text x="47" y="12">Docker Image</text><text x="110" y="13" fill="#010101" fill-opacity=".3">none</text><text x="110" y="12">none</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="101" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="101" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h63v20H0z"/><path fill="#9f9f9f" d="M63 0h38v20H63z"/><path fill="url(#b)" d="M0 0h101v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="32.5" y="15" fill="#010101" fill-opacity=".3">container</text><text x="32.5" y="14">container</text><text x="81" y="15" fill="#010101" fill-opacity=".3">none</text><text x="81" y="14">none</text></g></svg>

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 740 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="135" height="18"><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-opacity=".3"/><stop offset="1" stop-opacity=".5"/></linearGradient><rect rx="4" width="135" height="18" fill="#555"/><rect rx="4" x="92" width="43" height="18" fill="#4c1"/><path fill="#4c1" d="M92 0h4v18h-4z"/><rect rx="4" width="135" height="18" fill="url(#a)"/><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="47" y="13" fill="#010101" fill-opacity=".3">Docker Image</text><text x="47" y="12">Docker Image</text><text x="112.5" y="13" fill="#010101" fill-opacity=".3">ready</text><text x="112.5" y="12">ready</text></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="106" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="106" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h63v20H0z"/><path fill="#97CA00" d="M63 0h43v20H63z"/><path fill="url(#b)" d="M0 0h106v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="32.5" y="15" fill="#010101" fill-opacity=".3">container</text><text x="32.5" y="14">container</text><text x="83.5" y="15" fill="#010101" fill-opacity=".3">ready</text><text x="83.5" y="14">ready</text></g></svg>

Before

Width:  |  Height:  |  Size: 828 B

After

Width:  |  Height:  |  Size: 746 B

View file

@ -4,7 +4,7 @@ types_hash_max_size 2048;
include /usr/local/nginx/conf/mime.types.default;
default_type application/octet-stream;
access_log /var/log/nginx/nginx.access.log;
access_log /dev/stdout;
sendfile on;
gzip on;

View file

@ -1,2 +0,0 @@
#!/bin/sh
exec svlogd /var/log/buildlogsarchiver/

View file

@ -1,2 +0,0 @@
#!/bin/sh
exec svlogd /var/log/buildmanager/

View file

@ -0,0 +1,6 @@
#! /bin/sh
if [ -e /conf/stack/syslog-ng-extra.conf ]
then
cp /conf/stack/syslog-ng-extra.conf /etc/syslog-ng/conf.d/
fi

View file

@ -1,2 +0,0 @@
#!/bin/sh
exec svlogd /var/log/diffsworker/

View file

@ -1,2 +0,0 @@
#!/bin/sh
exec svlogd /var/log/gunicorn_registry/

View file

@ -1,2 +0,0 @@
#!/bin/sh
exec svlogd /var/log/gunicorn_verbs/

View file

@ -1,2 +0,0 @@
#!/bin/sh
exec svlogd /var/log/gunicorn_web/

View file

@ -1,2 +0,0 @@
#!/bin/sh
exec svlogd /var/log/nginx/

View file

@ -1,2 +0,0 @@
#!/bin/sh
exec svlogd -t /var/log/notificationworker/

View file

@ -1,10 +0,0 @@
#! /bin/sh
echo 'Linking config files to logs directory'
for svc in `ls /etc/service/`
do
if [ ! -d /var/log/$svc ]; then
mkdir -p /var/log/$svc
ln -s /svlogd_config /var/log/$svc/config
fi
done

View file

@ -0,0 +1,2 @@
#!/bin/sh
exec logger -i -t buildlogsarchiver

View file

@ -0,0 +1,2 @@
#!/bin/sh
exec logger -i -t buildmanager

View file

@ -0,0 +1,2 @@
#!/bin/sh
exec logger -i -t diffsworker

View file

@ -0,0 +1,2 @@
#!/bin/sh
exec logger -i -t gunicorn_registry

View file

@ -0,0 +1,2 @@
#!/bin/sh
exec logger -i -t gunicorn_verbs

View file

@ -0,0 +1,2 @@
#!/bin/sh
exec logger -i -t gunicorn_web

View file

@ -0,0 +1,2 @@
#!/bin/sh
exec logger -i -t nginx

View file

@ -0,0 +1,2 @@
#!/bin/sh
exec logger -i -t notificationworker

View file

@ -1,3 +0,0 @@
s100000000
t86400
n4

View file

@ -5,4 +5,4 @@ real_ip_header proxy_protocol;
log_format elb_pp '$proxy_protocol_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent"';
access_log /var/log/nginx/nginx.access.log elb_pp;
access_log /dev/stdout elb_pp;

View file

@ -1,7 +1,7 @@
# vim: ft=nginx
pid /tmp/nginx.pid;
error_log /var/log/nginx/nginx.error.log;
error_log /dev/stdout;
worker_processes 2;
worker_priority -10;

View file

@ -1,6 +1,5 @@
# vim: ft=nginx
client_body_temp_path /var/log/nginx/client_body 1 2;
server_name _;
keepalive_timeout 5;
@ -36,7 +35,7 @@ location /v1/repositories/ {
proxy_pass http://registry_app_server;
proxy_read_timeout 2000;
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
proxy_temp_path /tmp 1 2;
limit_req zone=repositories burst=10;
}
@ -47,7 +46,7 @@ location /v1/ {
proxy_request_buffering off;
proxy_pass http://registry_app_server;
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
proxy_temp_path /tmp 1 2;
client_max_body_size 20G;
}
@ -58,7 +57,7 @@ location /c1/ {
proxy_request_buffering off;
proxy_pass http://verbs_app_server;
proxy_temp_path /var/log/nginx/proxy_temp 1 2;
proxy_temp_path /tmp 1 2;
limit_req zone=verbs burst=10;
}

View file

@ -163,6 +163,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 = {

View file

@ -950,6 +950,7 @@ def change_password(user, new_password):
pw_hash = hash_password(new_password)
user.invalid_login_attempts = 0
user.password_hash = pw_hash
user.uuid = str(uuid4())
user.save()
# Remove any password required notifications for the user.

View file

@ -1,6 +1,11 @@
import ldap
import logging
import json
import itertools
import uuid
import struct
from util.aes import AESCipher
from util.validation import generate_valid_usernames
from data import model
@ -106,6 +111,7 @@ class LDAPUsers(object):
return found_user is not None
class UserAuthentication(object):
def __init__(self, app=None):
self.app = app
@ -138,5 +144,81 @@ class UserAuthentication(object):
app.extensions['authentication'] = users
return users
def _get_secret_key(self):
""" Returns the secret key to use for encrypting and decrypting. """
from app import app
app_secret_key = app.config['SECRET_KEY']
secret_key = None
# First try parsing the key as an int.
try:
big_int = int(app_secret_key)
secret_key = str(bytearray.fromhex('{:02x}'.format(big_int)))
except ValueError:
pass
# Next try parsing it as an UUID.
if secret_key is None:
try:
secret_key = uuid.UUID(app_secret_key).bytes
except ValueError:
pass
if secret_key is None:
secret_key = str(bytearray(map(ord, app_secret_key)))
# Otherwise, use the bytes directly.
return ''.join(itertools.islice(itertools.cycle(secret_key), 32))
def encrypt_user_password(self, password):
""" Returns an encrypted version of the user's password. """
data = {
'password': password
}
message = json.dumps(data)
cipher = AESCipher(self._get_secret_key())
return cipher.encrypt(message)
def _decrypt_user_password(self, encrypted):
""" Attempts to decrypt the given password and returns it. """
cipher = AESCipher(self._get_secret_key())
try:
message = cipher.decrypt(encrypted)
except ValueError:
return None
except TypeError:
return None
try:
data = json.loads(message)
except ValueError:
return None
return data.get('password', encrypted)
def verify_user(self, username_or_email, password, basic_auth=False):
# First try to decode the password as a signed token.
if basic_auth:
import features
decrypted = self._decrypt_user_password(password)
if decrypted is None:
# This is a normal password.
if features.REQUIRE_ENCRYPTED_BASIC_AUTH:
msg = ('Client login with unecrypted passwords is disabled. Please generate an ' +
'encrypted password in the user admin panel for use here.')
return (None, msg)
else:
password = decrypted
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)

View file

@ -1,6 +1,7 @@
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
@ -224,8 +225,13 @@ class User(ApiResource):
if 'password' in user_data:
logger.debug('Changing password for user: %s', user.username)
log_action('account_change_password', user.username)
# Change the user's password.
model.change_password(user, user_data['password'])
# Login again to reset their session cookie.
common_login(user)
if features.MAILING:
send_password_changed(user.username, user.email)
@ -335,13 +341,51 @@ 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)
return {
'key': authentication.encrypt_user_password(password)
}
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 +451,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')

View file

@ -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.')

View file

@ -34,7 +34,7 @@
</td>
</tr>
<tr>
<td>User Creation:</td>
<td class="non-input">User Creation:</td>
<td colspan="2">
<div class="co-checkbox">
<input id="ftuc" type="checkbox" ng-model="config.FEATURE_USER_CREATION">
@ -46,6 +46,23 @@
</div>
</td>
</tr>
<tr>
<td class="non-input">Encrypted Client Password:</td>
<td colspan="2">
<div class="co-checkbox">
<input id="ftet" type="checkbox" ng-model="config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH">
<label for="ftet">Require Encrypted Client Passwords</label>
</div>
<div class="help-text">
If enabled, users will not be able to login from the Docker command
line with a non-encrypted password and must generate an encrypted
password to use.
</div>
<div class="help-text" ng-if="config.AUTHENTICATION_TYPE == 'LDAP'">
This feature is <strong>highly recommended</strong> for setups with LDAP authentication, as Docker currently stores passwords in <strong>plaintext</strong> on user's machines.
</div>
</td>
</tr>
</table>
</div>
</div>
@ -293,6 +310,16 @@
</p>
</div>
<div class="alert alert-warning" ng-if="config.AUTHENTICATION_TYPE == 'LDAP' && !config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH">
It is <strong>highly recommended</strong> to require encrypted client passwords. LDAP passwords used in the Docker client will be stored in <strong>plaintext</strong>!
<a href="javascript:void(0)" ng-click="config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH = true">Enable this requirement now</a>.
</div>
<div class="alert alert-success" ng-if="config.AUTHENTICATION_TYPE == 'LDAP' && config.FEATURE_REQUIRE_ENCRYPTED_BASIC_AUTH">
Note: The "Require Encrypted Client Passwords" feature is currently enabled which will
prevent LDAP passwords from being saved as plaintext by the Docker client.
</div>
<table class="config-table">
<tr>
<td class="non-input">Authentication:</td>
@ -305,7 +332,6 @@
</tr>
</table>
<table class="config-table" ng-if="config.AUTHENTICATION_TYPE == 'LDAP'">
<tr>
<td>LDAP URI:</td>

View file

@ -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 generated an encrypted version:', generateToken);
};
$scope.detachExternalLogin = function(kind) {
var params = {
'servicename': kind

View file

@ -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 +
'<form style="margin-top: 10px" action="javascript:void(0)">' +
'<input id="passDialogBox" class="form-control" type="password" placeholder="Current Password">' +
'</form>',
"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;
}]);

View file

@ -32,7 +32,7 @@
<!-- Non-billing -->
<li quay-classes="{'!Features.BILLING': 'active'}"><a href="javascript:void(0)" data-toggle="tab" data-target="#email">Account E-mail</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#robots">Robot Accounts</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Change Password</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#password">Password</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#external" quay-show="Features.GITHUB_LOGIN || Features.GOOGLE_LOGIN">External Logins</a></li>
<li><a href="javascript:void(0)" data-toggle="tab" data-target="#authorized" ng-click="loadAuthedApps()">Authorized Applications</a></li>
<li quay-show="Features.USER_LOG_ACCESS || hasPaidBusinessPlan">
@ -139,8 +139,31 @@
</div>
</div>
<!-- Change password tab -->
<!-- Password tab -->
<div id="password" class="tab-pane">
<!-- Encrypted Password -->
<div class="row">
<div class="panel">
<div class="panel-title">Generate Encrypted Password</div>
<div class="panel-body">
<div class="alert alert-info" ng-if="!Features.REQUIRE_ENCRYPTED_BASIC_AUTH">
Due to Docker storing passwords entered on the command line in <strong>plaintext</strong>, it is highly recommended to use the button below to generate an an encrypted version of your password.
</div>
<div class="alert alert-warning" ng-if="Features.REQUIRE_ENCRYPTED_BASIC_AUTH">
This installation is set to <strong>require</strong> encrypted passwords when
using the Docker command line interface. To generate an encrypted password, click the button below.
</div>
<button class="btn btn-primary" ng-click="generateClientToken()">
<i class="fa fa-key" style="margin-right: 6px;"></i>Generate Encrypted Password
</button>
</div>
</div>
</div>
<!-- Change Password -->
<div class="row">
<div class="panel">
<div class="panel-title">Change Password</div>
@ -152,6 +175,9 @@
<span class="help-block" ng-show="changePasswordSuccess">Password changed successfully</span>
<div ng-show="!updatingUser" class="panel-body">
<div class="alert alert-warning">Note: Changing your password will also invalidate any generated encrypted passwords.</div>
<form class="form-change col-md-6" id="changePasswordForm" name="changePasswordForm" ng-submit="changePassword()"
ng-show="!awaitingConfirmation && !registering">
<input type="password" class="form-control" placeholder="Your new password" ng-model="cuser.password" required
@ -370,6 +396,26 @@
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="clientTokenModal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Encrypted Password</h4>
</div>
<div class="modal-body">
<div style="margin-bottom: 10px;">Your generated encrypted password:</div>
<div class="copy-box" value="generatedClientToken"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Dismiss</button>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<!-- Modal message dialog -->
<div class="modal fade" id="reallyconvertModal">
<div class="modal-dialog">

View file

@ -27,7 +27,8 @@ from endpoints.api.repoemail import RepositoryAuthorizedEmail
from endpoints.api.repositorynotification import RepositoryNotification, RepositoryNotificationList
from endpoints.api.user import (PrivateRepositories, ConvertToOrganization, Recovery, Signout,
Signin, User, UserAuthorizationList, UserAuthorization, UserNotification,
VerifyUser, DetachExternal, StarredRepositoryList, StarredRepository)
VerifyUser, DetachExternal, StarredRepositoryList, StarredRepository,
ClientKey)
from endpoints.api.repotoken import RepositoryToken, RepositoryTokenList
from endpoints.api.prototype import PermissionPrototype, PermissionPrototypeList
from endpoints.api.logs import UserLogs, OrgLogs, RepositoryLogs
@ -529,6 +530,26 @@ class TestVerifyUser(ApiTestCase):
self._run_test('POST', 200, 'devtable', {u'password': 'password'})
class TestClientKey(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)
self._set_url(ClientKey)
def test_post_anonymous(self):
self._run_test('POST', 401, None, {u'password': 'LQ0N'})
def test_post_freshuser(self):
self._run_test('POST', 400, 'freshuser', {u'password': 'LQ0N'})
def test_post_reader(self):
self._run_test('POST', 200, 'reader', {u'password': 'password'})
def test_post_devtable(self):
self._run_test('POST', 200, 'devtable', {u'password': 'password'})
class TestListPlans(ApiTestCase):
def setUp(self):
ApiTestCase.setUp(self)

32
util/aes.py Normal file
View file

@ -0,0 +1,32 @@
import base64
import hashlib
from Crypto import Random
from Crypto.Cipher import AES
class AESCipher(object):
""" Helper class for encrypting and decrypting data via AES.
Copied From: http://stackoverflow.com/a/21928790
"""
def __init__(self, key):
self.bs = 32
self.key = key
def encrypt(self, raw):
raw = self._pad(raw)
iv = Random.new().read(AES.block_size)
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return base64.b64encode(iv + cipher.encrypt(raw))
def decrypt(self, enc):
enc = base64.b64decode(enc)
iv = enc[:AES.block_size]
cipher = AES.new(self.key, AES.MODE_CBC, iv)
return self._unpad(cipher.decrypt(enc[AES.block_size:])).decode('utf-8')
def _pad(self, s):
return s + (self.bs - len(s) % self.bs) * chr(self.bs - len(s) % self.bs)
@staticmethod
def _unpad(s):
return s[:-ord(s[len(s)-1:])]