diff --git a/Dockerfile b/Dockerfile
index 7ba5e7501..d1307b102 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -53,6 +53,7 @@ RUN mkdir /usr/local/nginx/logs/
 
 # Run the tests
 RUN TEST=true venv/bin/python -m unittest discover -f
+RUN TEST=true venv/bin/python -m test.registry_tests -f
 
 VOLUME ["/conf/stack", "/var/log", "/datastorage", "/tmp", "/conf/etcd"]
 
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 000000000..d94d81588
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,38 @@
+# Quay.io Roadmap
+
+**work in progress**
+
+### Short Term
+- Framework for microservice decomposition
+- Improve documentation
+  - Ability to answer 80% of tickets with a link to the docs
+  - Eliminate old UI screenshots/references
+- Auth provider as a service
+  - Registry v2 compatible
+
+### Medium Term
+- Registry v2 support
+  - Forward and backward compatible with registry v1
+- Support ACI push spec
+  - Translate between ACI and docker images transparently
+- Integrate docs with the search bar
+  - Full text search?
+- Running on top of Tectonic
+- BitTorrent distribution support
+- Fully launch our API
+  - Versioned and backward compatible
+  - Adequate documentation
+
+### Long Term
+- Become the Tectonic app store
+  - Pods/apps as top level concept
+- Builds as top level concept
+  - Multiple Quay.io repos from a single git push
+  - Multi-step builds
+    - build artifact
+    - bundle artifact
+    - test bundle
+- Immediately consistent multi-region data availability
+  - Cockroach?
+- 2 factor auth
+  - How to integrate with Docker CLI?
\ No newline at end of file
diff --git a/buildman/component/buildcomponent.py b/buildman/component/buildcomponent.py
index f5703bb65..a59cb421a 100644
--- a/buildman/component/buildcomponent.py
+++ b/buildman/component/buildcomponent.py
@@ -55,6 +55,7 @@ class BuildComponent(BaseComponent):
   def onConnect(self):
     self.join(self.builder_realm)
 
+  @trollius.coroutine
   def onJoin(self, details):
     logger.debug('Registering methods and listeners for component %s', self.builder_realm)
     yield trollius.From(self.register(self._on_ready, u'io.quay.buildworker.ready'))
@@ -277,6 +278,9 @@ class BuildComponent(BaseComponent):
       # Send the notification that the build has completed successfully.
       self._current_job.send_notification('build_success', image_id=kwargs.get('image_id'))
     except ApplicationError as aex:
+      build_id = self._current_job.repo_build.uuid
+      logger.exception('Got remote exception for build: %s', build_id)
+
       worker_error = WorkerError(aex.error, aex.kwargs.get('base_error'))
 
       # Write the error to the log.
@@ -310,6 +314,7 @@ class BuildComponent(BaseComponent):
 
   @trollius.coroutine
   def _on_ready(self, token, version):
+    logger.debug('On ready called (token "%s")', token)
     self._worker_version = version
 
     if not version in SUPPORTED_WORKER_VERSIONS:
@@ -343,6 +348,10 @@ class BuildComponent(BaseComponent):
 
   def _on_heartbeat(self):
     """ Updates the last known heartbeat. """
+    if not self._current_job or self._component_status == ComponentStatus.TIMED_OUT:
+      return
+
+    logger.debug('Got heartbeat for build %s', self._current_job.repo_build.uuid)
     self._last_heartbeat = datetime.datetime.utcnow()
 
   @trollius.coroutine
@@ -374,9 +383,15 @@ class BuildComponent(BaseComponent):
       logger.debug('Checking heartbeat on realm %s', self.builder_realm)
       if (self._last_heartbeat and
           self._last_heartbeat < datetime.datetime.utcnow() - HEARTBEAT_DELTA):
+        logger.debug('Heartbeat on realm %s has expired: %s', self.builder_realm,
+                     self._last_heartbeat)
+
         yield trollius.From(self._timeout())
         raise trollius.Return()
 
+      logger.debug('Heartbeat on realm %s is valid: %s.', self.builder_realm,
+                   self._last_heartbeat)
+
       yield trollius.From(trollius.sleep(HEARTBEAT_TIMEOUT))
 
   @trollius.coroutine
diff --git a/buildman/templates/cloudconfig.yaml b/buildman/templates/cloudconfig.yaml
index 24b7bafe7..053f1f617 100644
--- a/buildman/templates/cloudconfig.yaml
+++ b/buildman/templates/cloudconfig.yaml
@@ -47,6 +47,7 @@ coreos:
     {{ dockersystemd('builder-logs',
                      'quay.io/kelseyhightower/journal-2-logentries',
                      extra_args='--env-file /root/overrides.list -v /run/journald.sock:/run/journald.sock',
+                     flattened=True,
                      after_units=['quay-builder.service']
                      ) | indent(4) }}
     {%- endif %}
diff --git a/conf/nginx.conf b/conf/nginx.conf
index 77a78f70e..5e49b1977 100644
--- a/conf/nginx.conf
+++ b/conf/nginx.conf
@@ -7,18 +7,26 @@ http {
     include hosted-http-base.conf;
     include rate-limiting.conf;
 
+    ssl_certificate ./stack/ssl.cert;
+    ssl_certificate_key ./stack/ssl.key;
+    ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
+    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
+    ssl_session_cache shared:SSL:10m;
+    ssl_session_timeout 5m;
+    ssl_stapling on;
+    ssl_stapling_verify on;
+    ssl_prefer_server_ciphers on;
+
     server {
         include server-base.conf;
 
         listen 443 default;
 
         ssl on;
-        ssl_certificate ./stack/ssl.cert;
-        ssl_certificate_key ./stack/ssl.key;
-        ssl_session_timeout 5m;
-        ssl_protocols SSLv3 TLSv1;
-        ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
-        ssl_prefer_server_ciphers on;
+
+        # This header must be set only for HTTPS
+        add_header Strict-Transport-Security "max-age=63072000; preload";
+
     }
 
     server {
@@ -28,11 +36,8 @@ http {
         listen 8443 default proxy_protocol;
 
         ssl on;
-        ssl_certificate ./stack/ssl.cert;
-        ssl_certificate_key ./stack/ssl.key;
-        ssl_session_timeout 5m;
-        ssl_protocols SSLv3 TLSv1;
-        ssl_ciphers ALL:!ADH:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv3:+EXP;
-        ssl_prefer_server_ciphers on;
+
+        # This header must be set only for HTTPS
+        add_header Strict-Transport-Security "max-age=63072000; preload";
     }
 }
diff --git a/conf/server-base.conf b/conf/server-base.conf
index 3853fbccf..bfa6c012f 100644
--- a/conf/server-base.conf
+++ b/conf/server-base.conf
@@ -8,6 +8,11 @@ if ($args ~ "_escaped_fragment_") {
     rewrite ^ /snapshot$uri;
 }
 
+# Disable the ability to be embedded into iframes
+add_header X-Frame-Options DENY;
+
+
+# Proxy Headers
 proxy_set_header X-Forwarded-For $proper_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;
 proxy_set_header Host $http_host;
diff --git a/data/model/legacy.py b/data/model/legacy.py
index 67daaa540..464131f55 100644
--- a/data/model/legacy.py
+++ b/data/model/legacy.py
@@ -566,6 +566,12 @@ def list_federated_logins(user):
                       FederatedLogin.user == user)
 
 
+def lookup_federated_login(user, service_name):
+  try:
+    return list_federated_logins(user).where(LoginService.name == service_name).get()
+  except FederatedLogin.DoesNotExist:
+    return None
+
 def create_confirm_email_code(user, new_email=None):
   if new_email:
     if not validate_email(new_email):
@@ -636,6 +642,13 @@ def find_user_by_email(email):
     return None
 
 
+def get_nonrobot_user(username):
+  try:
+    return User.get(User.username == username, User.organization == False, User.robot == False)
+  except User.DoesNotExist:
+    return None
+
+
 def get_user(username):
   try:
     return User.get(User.username == username, User.organization == False)
diff --git a/data/users.py b/data/users.py
index a50d462b9..f528694a3 100644
--- a/data/users.py
+++ b/data/users.py
@@ -9,6 +9,7 @@ import os
 from util.aes import AESCipher
 from util.validation import generate_valid_usernames
 from data import model
+from collections import namedtuple
 
 logger = logging.getLogger(__name__)
 if os.environ.get('LDAP_DEBUG') == '1':
@@ -28,6 +29,8 @@ class DatabaseUsers(object):
 
     return (result, None)
 
+  def confirm_existing_user(self, username, password):
+    return self.verify_user(username, password)
 
   def user_exists(self, username):
     return model.get_user(username) is not None
@@ -43,6 +46,7 @@ class LDAPConnection(object):
   def __enter__(self):
     trace_level = 2 if os.environ.get('LDAP_DEBUG') == '1' else 0
     self._conn = ldap.initialize(self._ldap_uri, trace_level=trace_level)
+    self._conn.set_option(ldap.OPT_REFERRALS, 1)
     self._conn.simple_bind_s(self._user_dn, self._user_pw)
 
     return self._conn
@@ -52,6 +56,8 @@ class LDAPConnection(object):
 
 
 class LDAPUsers(object):
+  _LDAPResult = namedtuple('LDAPResult', ['dn', 'attrs'])
+
   def __init__(self, ldap_uri, base_dn, admin_dn, admin_passwd, user_rdn, uid_attr, email_attr):
     self._ldap_conn = LDAPConnection(ldap_uri, admin_dn, admin_passwd)
     self._ldap_uri = ldap_uri
@@ -60,6 +66,25 @@ class LDAPUsers(object):
     self._uid_attr = uid_attr
     self._email_attr = email_attr
 
+  def _get_ldap_referral_dn(self, referral_exception):
+    logger.debug('Got referral: %s', referral_exception.args[0])
+    if not referral_exception.args[0] or not referral_exception.args[0].get('info'):
+      logger.debug('LDAP referral missing info block')
+      return None
+
+    referral_info = referral_exception.args[0]['info']
+    if not referral_info.startswith('Referral:\n'):
+      logger.debug('LDAP referral missing Referral header')
+      return None
+
+    referral_uri = referral_info[len('Referral:\n'):]
+    if not referral_uri.startswith('ldap:///'):
+      logger.debug('LDAP referral URI does not start with ldap:///')
+      return None
+
+    referral_dn = referral_uri[len('ldap:///'):]
+    return referral_dn
+
   def _ldap_user_search(self, username_or_email):
     with self._ldap_conn as conn:
       logger.debug('Incoming username or email param: %s', username_or_email.__repr__())
@@ -70,22 +95,56 @@ class LDAPUsers(object):
       logger.debug('Conducting user search: %s under %s', query, user_search_dn)
       try:
         pairs = conn.search_s(user_search_dn, ldap.SCOPE_SUBTREE, query.encode('utf-8'))
+      except ldap.REFERRAL as re:
+        referral_dn = self._get_ldap_referral_dn(re)
+        if not referral_dn:
+          return None
+
+        try:
+          subquery = u'(%s=%s)' % (self._uid_attr, username_or_email)
+          pairs = conn.search_s(referral_dn, ldap.SCOPE_BASE, subquery)
+        except ldap.LDAPError:
+          logger.exception('LDAP referral search exception')
+          return None
+
       except ldap.LDAPError:
         logger.exception('LDAP search exception')
         return None
 
       logger.debug('Found matching pairs: %s', pairs)
-      if len(pairs) < 1:
+
+      results = [LDAPUsers._LDAPResult(*pair) for pair in pairs]
+
+      # Filter out pairs without DNs. Some LDAP impls will return such
+      # pairs.
+      with_dns = [result for result in results if result.dn]
+      if len(with_dns) < 1:
         return None
 
-      for pair in pairs:
-        if pair[0] is not None:
-          logger.debug('Found user: %s', pair)
-          return pair
+      # If we have found a single pair, then return it.
+      if len(with_dns) == 1:
+        return with_dns[0]
 
-      return None
+      # Otherwise, there are multiple pairs with DNs, so find the one with the mail
+      # attribute (if any).
+      with_mail = [result for result in results if result.attrs.get(self._email_attr)]
+      return with_mail[0] if with_mail else with_dns[0]
 
-  def verify_user(self, username_or_email, password):
+  def confirm_existing_user(self, username, password):
+    """ Verify the username and password by looking up the *LDAP* username and confirming the
+        password.
+    """
+    db_user = model.get_user(username)
+    if not db_user:
+      return (None, 'Invalid user')
+
+    federated_login = model.lookup_federated_login(db_user, 'ldap')
+    if not federated_login:
+      return (None, 'Invalid user')
+
+    return self.verify_user(federated_login.service_ident, password, create_new_user=False)
+
+  def verify_user(self, username_or_email, password, create_new_user=True):
     """ Verify the credentials with LDAP and if they are valid, create or update the user
         in our database. """
 
@@ -94,17 +153,29 @@ class LDAPUsers(object):
       return (None, 'Anonymous binding not allowed')
 
     found_user = self._ldap_user_search(username_or_email)
-
     if found_user is None:
       return (None, 'Username not found')
 
     found_dn, found_response = found_user
+    logger.debug('Found user for LDAP username %s; validating password', username_or_email)
+    logger.debug('DN %s found: %s', found_dn, found_response)
 
     # First validate the password by binding as the user
-    logger.debug('Found user %s; validating password', username_or_email)
     try:
       with LDAPConnection(self._ldap_uri, found_dn, password.encode('utf-8')):
         pass
+    except ldap.REFERRAL as re:
+      referral_dn = self._get_ldap_referral_dn(re)
+      if not referral_dn:
+        return (None, 'Invalid username')
+
+      try:
+        with LDAPConnection(self._ldap_uri, referral_dn, password.encode('utf-8')):
+          pass
+      except ldap.INVALID_CREDENTIALS:
+        logger.exception('Invalid LDAP credentials')
+        return (None, 'Invalid password')
+
     except ldap.INVALID_CREDENTIALS:
       logger.exception('Invalid LDAP credentials')
       return (None, 'Invalid password')
@@ -121,6 +192,9 @@ class LDAPUsers(object):
     db_user = model.verify_federated_login('ldap', username)
 
     if not db_user:
+      if not create_new_user:
+        return (None, 'Invalid user')
+
       # We must create the user in our db
       valid_username = None
       for valid_username in generate_valid_usernames(username):
@@ -232,6 +306,13 @@ class UserAuthentication(object):
 
     return data.get('password', encrypted)
 
+  def confirm_existing_user(self, username, password):
+    """ Verifies that the given password matches to the given DB username. Unlike verify_user, this
+        call first translates the DB user via the FederatedLogin table (where applicable).
+    """
+    return self.state.confirm_existing_user(username, password)
+
+
   def verify_user(self, username_or_email, password, basic_auth=False):
     # First try to decode the password as a signed token.
     if basic_auth:
diff --git a/endpoints/api/superuser.py b/endpoints/api/superuser.py
index c1406dcb8..947b2a8db 100644
--- a/endpoints/api/superuser.py
+++ b/endpoints/api/superuser.py
@@ -238,8 +238,8 @@ class SuperUserSendRecoveryEmail(ApiResource):
   @nickname('sendInstallUserRecoveryEmail')
   def post(self, username):
     if SuperUserPermission().can():
-      user = model.get_user(username)
-      if not user or user.organization or user.robot:
+      user = model.get_nonrobot_user(username)
+      if not user:
         abort(404)
 
       if superusers.is_superuser(username):
@@ -288,8 +288,8 @@ class SuperUserManagement(ApiResource):
   def get(self, username):
     """ Returns information about the specified user. """
     if SuperUserPermission().can():
-      user = model.get_user(username)
-      if not user or user.organization or user.robot:
+      user = model.get_nonrobot_user(username)
+      if not user:
         abort(404)
 
       return user_view(user)
@@ -302,8 +302,8 @@ class SuperUserManagement(ApiResource):
   def delete(self, username):
     """ Deletes the specified user. """
     if SuperUserPermission().can():
-      user = model.get_user(username)
-      if not user or user.organization or user.robot:
+      user = model.get_nonrobot_user(username)
+      if not user:
         abort(404)
 
       if superusers.is_superuser(username):
@@ -321,8 +321,8 @@ class SuperUserManagement(ApiResource):
   def put(self, username):
     """ Updates information about the specified user. """
     if SuperUserPermission().can():
-        user = model.get_user(username)
-        if not user or user.organization or user.robot:
+        user = model.get_nonrobot_user(username)
+        if not user:
           abort(404)
 
         if superusers.is_superuser(username):
diff --git a/endpoints/api/user.py b/endpoints/api/user.py
index 92199ec68..6a0567963 100644
--- a/endpoints/api/user.py
+++ b/endpoints/api/user.py
@@ -283,7 +283,7 @@ class User(ApiResource):
     user_data = request.get_json()
     invite_code = user_data.get('invite_code', '')
 
-    existing_user = model.get_user(user_data['username'])
+    existing_user = model.get_nonrobot_user(user_data['username'])
     if existing_user:
       raise request_error(message='The username already exists')
 
@@ -372,8 +372,7 @@ class ClientKey(ApiResource):
     """  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)
+    (result, error_message) = authentication.confirm_existing_user(username, password)
     if not result:
       raise request_error(message=error_message)
 
@@ -541,7 +540,17 @@ class VerifyUser(ApiResource):
     """ Verifies the signed in the user with the specified credentials. """
     signin_data = request.get_json()
     password = signin_data['password']
-    return conduct_signin(get_authenticated_user().username, password)
+
+    username = get_authenticated_user().username
+    (result, error_message) = authentication.confirm_existing_user(username, password)
+    if not result:
+      return {
+        'message': error_message,
+        'invalidCredentials': True,
+      }, 403
+
+    common_login(result)
+    return {'success': True}
 
 
 @resource('/v1/signout')
@@ -815,8 +824,8 @@ class Users(ApiResource):
   @nickname('getUserInformation')
   def get(self, username):
     """ Get user information for the specified user. """
-    user = model.get_user(username)
-    if user is None or user.organization or user.robot:
+    user = model.get_nonrobot_user(username)
+    if user is None:
       abort(404)
 
     return user_view(user)
diff --git a/endpoints/notificationmethod.py b/endpoints/notificationmethod.py
index 4d2d685f8..8626fd68f 100644
--- a/endpoints/notificationmethod.py
+++ b/endpoints/notificationmethod.py
@@ -71,7 +71,7 @@ class QuayNotificationMethod(NotificationMethod):
     target_info = config_data['target']
 
     if target_info['kind'] == 'user':
-      target = model.get_user(target_info['name'])
+      target = model.get_nonrobot_user(target_info['name'])
       if not target:
         # Just to be safe.
         return (True, 'Unknown user %s' % target_info['name'], [])
diff --git a/endpoints/trigger.py b/endpoints/trigger.py
index 1a07a357a..692c23990 100644
--- a/endpoints/trigger.py
+++ b/endpoints/trigger.py
@@ -244,7 +244,11 @@ class BitbucketBuildTrigger(BuildTriggerHandler):
   def _get_authorized_client(self):
     base_client = self._get_client()
     auth_token = self.auth_token or 'invalid:invalid'
-    (access_token, access_token_secret) = auth_token.split(':')
+    token_parts = auth_token.split(':')
+    if len(token_parts) != 2:
+      token_parts = ['invalid', 'invalid']
+
+    (access_token, access_token_secret) = token_parts
     return base_client.get_authorized_client(access_token, access_token_secret)
 
   def _get_repository_client(self):
@@ -253,6 +257,13 @@ class BitbucketBuildTrigger(BuildTriggerHandler):
     bitbucket_client = self._get_authorized_client()
     return bitbucket_client.for_namespace(namespace).repositories().get(name)
 
+  def _get_default_branch(self, repository, default_value='master'):
+    (result, data, _) = repository.get_main_branch()
+    if result:
+      return data['name']
+
+    return default_value
+
   def get_oauth_url(self):
     bitbucket_client = self._get_client()
     (result, data, err_msg) = bitbucket_client.get_authorization_url()
@@ -372,6 +383,9 @@ class BitbucketBuildTrigger(BuildTriggerHandler):
     # Find the first matching branch.
     repo_branches = self.list_field_values('branch_name') or []
     branches = find_matching_branches(config, repo_branches)
+    if not branches:
+      branches = [self._get_default_branch(repository)]
+
     (result, data, err_msg) = repository.get_path_contents('', revision=branches[0])
     if not result:
       raise RepositoryReadException(err_msg)
@@ -432,10 +446,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler):
 
     # Lookup the default branch associated with the repository. We use this when building
     # the tags.
-    default_branch = ''
-    (result, data, _) = repository.get_main_branch()
-    if result:
-      default_branch = data['name']
+    default_branch = self._get_default_branch(repository)
 
     # Lookup the commit sha.
     (result, data, _) = repository.changesets().get(commit_sha)
@@ -488,30 +499,36 @@ class BitbucketBuildTrigger(BuildTriggerHandler):
     # Parse the JSON payload.
     payload_json = request.form.get('payload')
     if not payload_json:
+      logger.debug('Skipping BitBucket request due to missing payload')
       raise SkipRequestException()
 
     try:
       payload = json.loads(payload_json)
     except ValueError:
+      logger.debug('Skipping BitBucket request due to invalid payload')
       raise SkipRequestException()
 
     logger.debug('BitBucket trigger payload %s', payload)
 
     # Make sure we have a commit in the payload.
     if not payload.get('commits'):
+      logger.debug('Skipping BitBucket request due to missing commits block')
       raise SkipRequestException()
 
     # Check if this build should be skipped by commit message.
     commit = payload['commits'][0]
     commit_message = commit['message']
     if should_skip_commit(commit_message):
+      logger.debug('Skipping BitBucket request due to commit message request')
       raise SkipRequestException()
 
     # Check to see if this build should be skipped by ref.
     if not commit.get('branch') and not commit.get('tag'):
+      logger.debug('Skipping BitBucket request due to missing branch and tag')
       raise SkipRequestException()
 
     ref = 'refs/heads/' + commit['branch'] if commit.get('branch') else 'refs/tags/' + commit['tag']
+    logger.debug('Checking BitBucket request: %s', ref)
     raise_if_skipped(self.config, ref)
 
     commit_sha = commit['node']
@@ -523,10 +540,7 @@ class BitbucketBuildTrigger(BuildTriggerHandler):
     repository = self._get_repository_client()
 
     # Find the branch to build.
-    branch_name = run_parameters.get('branch_name')
-    (result, data, _) = repository.get_main_branch()
-    if result:
-      branch_name = branch_name or data['name']
+    branch_name = run_parameters.get('branch_name') or self._get_default_branch(repository)
 
     # Lookup the commit SHA for the branch.
     (result, data, _) = repository.get_branches()
@@ -1048,7 +1062,7 @@ class CustomBuildTrigger(BuildTriggerHandler):
     }
 
     prepared = PreparedBuild(self.trigger)
-    prepared.tags = [commit_sha]
+    prepared.tags = [commit_sha[:7]]
     prepared.name_from_sha(commit_sha)
     prepared.subdirectory = config['subdir']
     prepared.metadata = metadata
diff --git a/endpoints/web.py b/endpoints/web.py
index db12a0e65..b526790dc 100644
--- a/endpoints/web.py
+++ b/endpoints/web.py
@@ -9,10 +9,11 @@ from health.healthcheck import get_healthchecker
 
 from data import model
 from data.model.oauth import DatabaseAuthorizationProvider
-from app import app, billing as stripe, build_logs, avatar, signer
+from app import app, billing as stripe, build_logs, avatar, signer, log_archive
 from auth.auth import require_session_login, process_oauth
 from auth.permissions import (AdministerOrganizationPermission, ReadRepositoryPermission,
-                              SuperUserPermission, AdministerRepositoryPermission)
+                              SuperUserPermission, AdministerRepositoryPermission,
+                              ModifyRepositoryPermission)
 
 from util.invoice import renderInvoiceToPdf
 from util.seo import render_snapshot
@@ -250,6 +251,31 @@ def robots():
   return send_from_directory('static', 'robots.txt')
 
 
+@web.route('/buildlogs/<build_uuid>', methods=['GET'])
+@route_show_if(features.BUILD_SUPPORT)
+@require_session_login
+def buildlogs(build_uuid):
+  build = model.get_repository_build(build_uuid)
+  if not build:
+    abort(403)
+
+  repo = build.repository
+  if not ModifyRepositoryPermission(repo.namespace_user.username, repo.name).can():
+    abort(403)
+
+  # If the logs have been archived, just return a URL of the completed archive
+  if build.logs_archived:
+    return redirect(log_archive.get_file_url(build.uuid))
+
+  _, logs = build_logs.get_log_entries(build.uuid, 0)
+  response = jsonify({
+    'logs': [log for log in logs]
+  })
+
+  response.headers["Content-Disposition"] = "attachment;filename=" + build.uuid + ".json"
+  return response
+
+
 @web.route('/receipt', methods=['GET'])
 @route_show_if(features.BILLING)
 @require_session_login
diff --git a/health/healthcheck.py b/health/healthcheck.py
index 11a365e34..98de22435 100644
--- a/health/healthcheck.py
+++ b/health/healthcheck.py
@@ -77,10 +77,11 @@ class LocalHealthCheck(HealthCheck):
 
 
 class ProductionHealthCheck(HealthCheck):
-  def __init__(self, app, access_key, secret_key):
+  def __init__(self, app, access_key, secret_key, db_instance='quay'):
     super(ProductionHealthCheck, self).__init__(app)
     self.access_key = access_key
     self.secret_key = secret_key
+    self.db_instance = db_instance
 
   @classmethod
   def check_name(cls):
@@ -115,7 +116,10 @@ class ProductionHealthCheck(HealthCheck):
         aws_access_key_id=self.access_key, aws_secret_access_key=self.secret_key)
       response = region.describe_db_instances()['DescribeDBInstancesResponse']
       result = response['DescribeDBInstancesResult']
-      instances = result['DBInstances']
+      instances = [i for i in result['DBInstances'] if i['DBInstanceIdentifier'] == self.db_instance]
+      if not instances:
+        return 'error'
+
       status = instances[0]['DBInstanceStatus']
       return status
     except:
diff --git a/requirements-nover.txt b/requirements-nover.txt
index d76e671b5..8cbb82cf8 100644
--- a/requirements-nover.txt
+++ b/requirements-nover.txt
@@ -21,7 +21,6 @@ paramiko
 xhtml2pdf
 redis
 hiredis
-docker-py
 flask-restful==0.2.12
 jsonschema
 git+https://github.com/NateFerrero/oauth2lib.git
@@ -38,12 +37,12 @@ psycopg2
 pyyaml
 git+https://github.com/DevTable/aniso8601-fake.git
 git+https://github.com/DevTable/anunidecode.git
-git+https://github.com/DevTable/avatar-generator.git
 git+https://github.com/DevTable/pygithub.git
 git+https://github.com/DevTable/container-cloud-config.git
 git+https://github.com/DevTable/python-etcd.git
 git+https://github.com/coreos/py-bitbucket.git
 git+https://github.com/coreos/pyapi-gitlab.git
+git+https://github.com/coreos/mockldap.git
 gipc
 pyOpenSSL
 pygpgme
@@ -51,4 +50,6 @@ cachetools
 mock
 psutil
 stringscore
-mockldap
+python-swiftclient
+python-keystoneclient
+Flask-Testing
\ No newline at end of file
diff --git a/requirements.txt b/requirements.txt
index 08eddb442..4531f5227 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,76 +1,99 @@
-APScheduler==3.0.1
+APScheduler==3.0.3
+Babel==1.3
 Flask==0.10.1
 Flask-Login==0.2.11
 Flask-Mail==0.9.1
 Flask-Principal==0.4.0
 Flask-RESTful==0.2.12
+Flask-Testing==0.4.2
 Jinja2==2.7.3
-LogentriesLogger==0.2.1
-Mako==1.0.0
+Logentries==0.7
+Mako==1.0.1
 MarkupSafe==0.23
-Pillow==2.7.0
-PyMySQL==0.6.3
+Pillow==2.8.1
+PyMySQL==0.6.6
 PyPDF2==1.24
 PyYAML==3.11
-SQLAlchemy==0.9.8
-WebOb==1.4
-Werkzeug==0.9.6
-aiowsgi==0.3
-alembic==0.7.4
+SQLAlchemy==1.0.3
+WebOb==1.4.1
+Werkzeug==0.10.4
+aiowsgi==0.5
+alembic==0.7.5.post2
+argparse==1.3.0
 autobahn==0.9.3-3
 backports.ssl-match-hostname==3.4.0.2
 beautifulsoup4==4.3.2
 blinker==1.3
-boto==2.35.1
+boto==2.38.0
 cachetools==1.0.0
-docker-py==0.7.1
-ecdsa==0.11
+certifi==2015.04.28
+cffi==0.9.2
+cryptography==0.8.2
+ecdsa==0.13
+enum34==1.0.4
+funcparserlib==0.3.6
 futures==2.2.0
 gevent==1.0.1
 gipc==0.5.0
 greenlet==0.4.5
 gunicorn==18.0
-hiredis==0.1.5
-html5lib==0.999
+hiredis==0.2.0
+html5lib==0.99999
+iso8601==0.1.10
 itsdangerous==0.24
 jsonschema==2.4.0
-marisa-trie==0.7
-mixpanel-py==3.2.1
+marisa-trie==0.7.2
+mixpanel-py==4.0.2
 mock==1.0.1
-mockldap==0.2.4
+msgpack-python==0.4.6
+netaddr==0.7.14
+netifaces==0.10.4
+oauthlib==0.7.2
+oslo.config==1.11.0
+oslo.i18n==1.6.0
+oslo.serialization==1.5.0
+oslo.utils==1.5.0
 paramiko==1.15.2
-peewee==2.4.7
+pbr==0.11.0
+peewee==2.6.0
+prettytable==0.7.2
 psutil==2.2.1
-psycopg2==2.5.4
+psycopg2==2.6
 py-bcrypt==0.4
+pyOpenSSL==0.15.1
+pyasn1==0.1.7
+pycparser==2.12
 pycrypto==2.6.1
-python-dateutil==2.4.0
+pygpgme==0.3
+python-dateutil==2.4.2
+python-keystoneclient==1.4.0
 python-ldap==2.4.19
 python-magic==0.4.6
-pygpgme==0.3
-pytz==2014.10
-pyOpenSSL==0.14
-raven==5.1.1
+python-swiftclient==2.4.0
+pytz==2015.2
+raven==5.3.0
 redis==2.10.3
 reportlab==2.7
-requests==2.5.1
+requests==2.6.2
 requests-oauthlib==0.4.2
+simplejson==3.7.1
 six==1.9.0
+stevedore==1.4.0
 stringscore==0.1.0
-stripe==1.20.1
+stripe==1.22.2
 trollius==1.0.4
-tzlocal==1.1.2
-urllib3==1.10.2
+tzlocal==1.1.3
+urllib3==1.10.3
 waitress==0.8.9
-websocket-client==0.23.0
+websocket-client==0.30.0
 wsgiref==0.1.2
 xhtml2pdf==0.0.6
 git+https://github.com/DevTable/aniso8601-fake.git
 git+https://github.com/DevTable/anunidecode.git
-git+https://github.com/DevTable/avatar-generator.git
 git+https://github.com/DevTable/pygithub.git
 git+https://github.com/DevTable/container-cloud-config.git
 git+https://github.com/DevTable/python-etcd.git
 git+https://github.com/NateFerrero/oauth2lib.git
 git+https://github.com/coreos/py-bitbucket.git
 git+https://github.com/coreos/pyapi-gitlab.git
+git+https://github.com/coreos/mockldap.git
\ No newline at end of file
diff --git a/static/css/core-ui.css b/static/css/core-ui.css
index 58ab72b81..adf6bc084 100644
--- a/static/css/core-ui.css
+++ b/static/css/core-ui.css
@@ -388,6 +388,29 @@ a:focus {
   width: 400px;
 }
 
+.config-map-field-element table {
+  margin-bottom: 10px;
+}
+
+.config-map-field-element .form-control-container {
+  border-top: 1px solid #eee;
+  padding-top: 10px;
+}
+
+.config-map-field-element .form-control-container select, .config-map-field-element .form-control-container input {
+  margin-bottom: 10px;
+}
+
+.config-map-field-element .empty {
+  color: #ccc;
+  margin-bottom: 10px;
+  display: block;
+}
+
+.config-map-field-element .item-title {
+  font-weight: bold;
+}
+
 .config-contact-field {
   margin-bottom: 4px;
 }
diff --git a/static/css/directives/ui/build-logs-view.css b/static/css/directives/ui/build-logs-view.css
index e69a3ad57..746a6c1d7 100644
--- a/static/css/directives/ui/build-logs-view.css
+++ b/static/css/directives/ui/build-logs-view.css
@@ -157,6 +157,24 @@
   transition: all 0.15s ease-in-out;
 }
 
+.build-logs-view .download-button i.fa {
+  margin-right: 10px;
+}
+
+.build-logs-view .download-button {
+  position: absolute;
+  top: 6px;
+  right: 124px;
+  z-index: 2;
+  transition: all 0.15s ease-in-out;
+}
+
+.build-logs-view .download-button:not(:hover) {
+  background: transparent;
+  border: 1px solid transparent;
+  color: #ddd;
+}
+
 .build-logs-view .copy-button:not(.zeroclipboard-is-hover) {
   background: transparent;
   border: 1px solid transparent;
diff --git a/static/directives/build-logs-view.html b/static/directives/build-logs-view.html
index f99a7eb00..aa79cec8a 100644
--- a/static/directives/build-logs-view.html
+++ b/static/directives/build-logs-view.html
@@ -3,6 +3,12 @@
     <i class="fa fa-clipboard"></i>Copy Logs
   </button>
 
+  <a id="downloadButton" class="btn btn-primary download-button"
+     ng-href="/buildlogs/{{ currentBuild.id }}"
+     target="_blank">
+    <i class="fa fa-download"></i>Download Logs
+  </a>
+
   <span class="cor-loader" ng-if="!logEntries"></span>
 
   <span class="no-logs" ng-if="!logEntries.length && currentBuild.phase == 'waiting'">
diff --git a/static/directives/config/config-map-field.html b/static/directives/config/config-map-field.html
new file mode 100644
index 000000000..7089e2010
--- /dev/null
+++ b/static/directives/config/config-map-field.html
@@ -0,0 +1,20 @@
+<div class="config-map-field-element">
+  <table class="table" ng-show="hasValues(binding)">
+    <tr class="item" ng-repeat="(key, value) in binding">
+      <td class="item-title">{{ key }}</td>
+      <td class="item-value">{{ value }}</td>
+      <td class="item-delete">
+        <a href="javascript:void(0)" ng-click="removeKey(key)">Remove</a>
+      </td>
+    </tr>
+  </table>
+  <span class="empty" ng-if="!hasValues(binding)">No entries defined</span>
+  <form class="form-control-container" ng-submit="addEntry()">
+    Add Key-Value:
+    <select ng-model="newKey">
+      <option ng-repeat="key in keys" value="{{ key }}">{{ key }}</option>
+    </select>
+    <input type="text" class="form-control" placeholder="Value" ng-model="newValue">
+    <button class="btn btn-default" style="display: inline-block">Add Entry</button>
+  </form>
+</div>
diff --git a/static/directives/config/config-setup-tool.html b/static/directives/config/config-setup-tool.html
index 6e9458f61..3e935e756 100644
--- a/static/directives/config/config-setup-tool.html
+++ b/static/directives/config/config-setup-tool.html
@@ -112,6 +112,11 @@
                  A valid SSL certificate and private key files are required to use this option.
               </div>
 
+              <div class="co-alert co-alert-info" ng-if="config.PREFERRED_URL_SCHEME == 'https'">
+                Enabling SSL also enables <a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security">HTTP Strict Transport Security</a>.<br/>
+                This prevents downgrade attacks and cookie theft, but browsers will reject all future insecure connections on this hostname.
+              </div>
+
               <table class="config-table"  ng-if="config.PREFERRED_URL_SCHEME == 'https'">
                 <tr>
                   <td class="non-input">Certificate:</td>
@@ -198,6 +203,7 @@
                   <option value="S3Storage">Amazon S3</option>
                   <option value="GoogleCloudStorage">Google Cloud Storage</option>
                   <option value="RadosGWStorage">Ceph Object Gateway (RADOS)</option>
+                  <option value="SwiftStorage">OpenStack Storage (Swift)</option>
                 </select>
               </td>
             </tr>
@@ -206,10 +212,15 @@
             <tr ng-repeat="field in STORAGE_CONFIG_FIELDS[config.DISTRIBUTED_STORAGE_CONFIG.local[0]]">
               <td>{{ field.title }}:</td>
               <td>
+                <span class="config-map-field"
+                      binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]"
+                      ng-if="field.kind == 'map'"
+                      keys="field.keys"></span>
                 <span class="config-string-field"
                       binding="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]"
                       placeholder="{{ field.placeholder }}"
-                      ng-if="field.kind == 'text'"></span>
+                      ng-if="field.kind == 'text'"
+                      is-optional="field.optional"></span>
                 <div class="co-checkbox" ng-if="field.kind == 'bool'">
                   <input id="dsc-{{ field.name }}" type="checkbox"
                          ng-model="config.DISTRIBUTED_STORAGE_CONFIG.local[1][field.name]">
@@ -849,4 +860,4 @@
     </div><!-- /.modal -->
 
   </div>
-</div>
\ No newline at end of file
+</div>
diff --git a/static/directives/config/config-string-field.html b/static/directives/config/config-string-field.html
index 7714fd541..703891f89 100644
--- a/static/directives/config/config-string-field.html
+++ b/static/directives/config/config-string-field.html
@@ -2,7 +2,7 @@
   <form name="fieldform" novalidate>
     <input type="text" class="form-control" placeholder="{{ placeholder || '' }}"
            ng-model="binding" ng-trim="false" ng-minlength="1"
-           ng-pattern="getRegexp(pattern)" required>
+           ng-pattern="getRegexp(pattern)" ng-required="!isOptional">
     <div class="alert alert-danger" ng-show="errorMessage">
         {{ errorMessage }}
     </div>
diff --git a/static/directives/repo-view/repo-panel-settings.html b/static/directives/repo-view/repo-panel-settings.html
index 04e128579..720a96772 100644
--- a/static/directives/repo-view/repo-panel-settings.html
+++ b/static/directives/repo-view/repo-panel-settings.html
@@ -63,6 +63,12 @@
 
         <!-- Build Status Badge -->
         <div class="panel-body panel-section hidden-xs">
+
+          <!-- Token Info Banner -->
+          <div class="co-alert co-alert-info" ng-if="!repository.is_public">
+             Note: This badge contains a token so the badge can be seen by external users. The token does not grant any other access and is safe to share!
+          </div>
+
           <!-- Status Image -->
           <a ng-href="/repository/{{ repository.namespace }}/{{ repository.name }}">
             <img ng-src="/repository/{{ repository.namespace }}/{{ repository.name }}/status?token={{ repository.status_token }}"
diff --git a/static/directives/repository-events-table.html b/static/directives/repository-events-table.html
index 5ec0a02db..a2331e7fe 100644
--- a/static/directives/repository-events-table.html
+++ b/static/directives/repository-events-table.html
@@ -15,10 +15,10 @@
 
           <div class="empty" ng-if="!notifications.length">
             <div class="empty-primary-msg">No notifications have been setup for this repository.</div>
-            <div class="empty-secondary-msg hidden-xs" ng-if="repository.can_write">
+            <div class="empty-secondary-msg hidden-sm hidden-xs" ng-if="repository.can_write">
               Click the "Create Notification" button above to add a new notification for a repository event.
             </div>
-            <div class="empty-secondary-msg visible-xs" ng-if="repository.can_write">
+            <div class="empty-secondary-msg visible-sm visible-xs" ng-if="repository.can_write">
               <a href="javascript:void(0)" ng-click="askCreateNotification()">Click here</a> to add a new notification for a repository event.
             </div>
           </div>
diff --git a/static/js/core-config-setup.js b/static/js/core-config-setup.js
index 033bbfbb6..93c07c7a6 100644
--- a/static/js/core-config-setup.js
+++ b/static/js/core-config-setup.js
@@ -78,6 +78,19 @@ angular.module("core-config-setup", ['angularFileUpload'])
             {'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'}
+          ],
+
+          'SwiftStorage': [
+            {'name': 'auth_url', 'title': 'Swift Auth URL', 'placeholder': '', 'kind': 'text'},
+            {'name': 'swift_container', 'title': 'Swift Container Name', 'placeholder': 'mycontainer', 'kind': 'text'},
+            {'name': 'storage_path', 'title': 'Storage Path', 'placeholder': '/path/inside/container', 'kind': 'text'},
+
+            {'name': 'swift_user', 'title': 'Username', 'placeholder': 'accesskeyhere', 'kind': 'text'},
+            {'name': 'swift_password', 'title': 'Password/Key', 'placeholder': 'secretkeyhere', 'kind': 'text'},
+
+            {'name': 'ca_cert_path', 'title': 'CA Cert Filename', 'placeholder': 'conf/stack/swift.cert', 'kind': 'text', 'optional': true},
+            {'name': 'os_options', 'title': 'OS Options', 'kind': 'map',
+             'keys': ['tenant_id', 'auth_token', 'service_type', 'endpoint_type', 'tenant_name', 'object_storage_url', 'region_name']}
           ]
         };
 
@@ -760,6 +773,42 @@ angular.module("core-config-setup", ['angularFileUpload'])
     return directiveDefinitionObject;
   })
 
+ .directive('configMapField', function () {
+    var directiveDefinitionObject = {
+      priority: 0,
+      templateUrl: '/static/directives/config/config-map-field.html',
+      replace: false,
+      transclude: false,
+      restrict: 'C',
+      scope: {
+        'binding': '=binding',
+        'keys': '=keys'
+      },
+      controller: function($scope, $element) {
+        $scope.newKey = null;
+        $scope.newValue = null;
+
+        $scope.hasValues = function(binding) {
+          return binding && Object.keys(binding).length;
+        };
+
+        $scope.removeKey = function(key) {
+          delete $scope.binding[key];
+        };
+
+        $scope.addEntry = function() {
+          if (!$scope.newKey || !$scope.newValue) { return; }
+
+          $scope.binding = $scope.binding || {};
+          $scope.binding[$scope.newKey] = $scope.newValue;
+          $scope.newKey = null;
+          $scope.newValue = null;
+        }
+      }
+    };
+    return directiveDefinitionObject;
+  })
+
   .directive('configStringField', function () {
     var directiveDefinitionObject = {
       priority: 0,
@@ -772,7 +821,8 @@ angular.module("core-config-setup", ['angularFileUpload'])
         'placeholder': '@placeholder',
         'pattern': '@pattern',
         'defaultValue': '@defaultValue',
-        'validator': '&validator'
+        'validator': '&validator',
+        'isOptional': '=isOptional'
       },
       controller: function($scope, $element) {
         $scope.getRegexp = function(pattern) {
diff --git a/static/js/directives/repo-view/repo-panel-builds.js b/static/js/directives/repo-view/repo-panel-builds.js
index a1d8fdd31..72b3c05f5 100644
--- a/static/js/directives/repo-view/repo-panel-builds.js
+++ b/static/js/directives/repo-view/repo-panel-builds.js
@@ -260,7 +260,7 @@ angular.module('quay').directive('repoPanelBuilds', function () {
       };
 
       $scope.handleBuildStarted = function(build) {
-        if (!$scope.allBuilds) {
+        if ($scope.allBuilds) {
           $scope.allBuilds.push(build);
         }
         updateBuilds();
diff --git a/static/js/directives/ui/trigger-setup-githost.js b/static/js/directives/ui/trigger-setup-githost.js
index 9ad3bcf8f..03be87852 100644
--- a/static/js/directives/ui/trigger-setup-githost.js
+++ b/static/js/directives/ui/trigger-setup-githost.js
@@ -123,6 +123,7 @@ angular.module('quay').directive('triggerSetupGithost', function () {
 
         ApiService.listBuildTriggerSubdirs($scope.trigger['config'], params).then(function(resp) {
           if (resp['status'] == 'error') {
+            $scope.locations = [];
             callback(resp['message'] || 'Could not load Dockerfile locations');
             return;
           }
diff --git a/static/js/pages/new-organization.js b/static/js/pages/new-organization.js
index 85c451115..37613c1e7 100644
--- a/static/js/pages/new-organization.js
+++ b/static/js/pages/new-organization.js
@@ -45,6 +45,7 @@
     $scope.signinStarted = function() {
       if (Features.BILLING) {
         PlanService.getMinimumPlan(1, true, function(plan) {
+          if (!plan) { return; }
           PlanService.notePlan(plan.stripeId);
         });
       }
diff --git a/static/js/pages/user-view.js b/static/js/pages/user-view.js
index b3f809fb2..7d2a0035e 100644
--- a/static/js/pages/user-view.js
+++ b/static/js/pages/user-view.js
@@ -94,7 +94,7 @@
         }, ApiService.errorDisplay('Could not generate token'));
       };
 
-      UIService.showPasswordDialog('Enter your password to generated an encrypted version:', generateToken);
+      UIService.showPasswordDialog('Enter your password to generate an encrypted version:', generateToken);
     };
 
     $scope.changeEmail = function() {
diff --git a/static/js/services/angular-poll-channel.js b/static/js/services/angular-poll-channel.js
index adba49757..f4028a65f 100644
--- a/static/js/services/angular-poll-channel.js
+++ b/static/js/services/angular-poll-channel.js
@@ -1,7 +1,8 @@
 /**
  * Specialized class for conducting an HTTP poll, while properly preventing multiple calls.
  */
-angular.module('quay').factory('AngularPollChannel', ['ApiService', '$timeout', function(ApiService, $timeout) {
+angular.module('quay').factory('AngularPollChannel', ['ApiService', '$timeout', 'DocumentVisibilityService',
+    function(ApiService, $timeout, DocumentVisibilityService) {
   var _PollChannel = function(scope, requester, opt_sleeptime) {
     this.scope_ = scope;
     this.requester_ = requester;
@@ -50,6 +51,12 @@ angular.module('quay').factory('AngularPollChannel', ['ApiService', '$timeout',
   _PollChannel.prototype.call_ = function() {
     if (this.working) { return; }
 
+    // If the document is currently hidden, skip the call.
+    if (DocumentVisibilityService.isHidden()) {
+      this.setupTimer_();
+      return;
+    }
+
     var that = this;
     this.working = true;
     this.scope_.$apply(function() {
diff --git a/static/js/services/document-visibility-service.js b/static/js/services/document-visibility-service.js
new file mode 100644
index 000000000..f56ecc633
--- /dev/null
+++ b/static/js/services/document-visibility-service.js
@@ -0,0 +1,60 @@
+/**
+ * Helper service which fires off events when the document's visibility changes, as well as allowing
+ * other Angular code to query the state of the document's visibility directly.
+ */
+angular.module('quay').constant('CORE_EVENT', {
+  DOC_VISIBILITY_CHANGE: 'core.event.doc_visibility_change'
+});
+
+angular.module('quay').factory('DocumentVisibilityService', ['$rootScope', '$document', 'CORE_EVENT',
+    function($rootScope, $document, CORE_EVENT) {
+  var document = $document[0],
+  features,
+  detectedFeature;
+
+  function broadcastChangeEvent() {
+    $rootScope.$broadcast(CORE_EVENT.DOC_VISIBILITY_CHANGE,
+                          document[detectedFeature.propertyName]);
+  }
+
+  features = {
+    standard: {
+      eventName: 'visibilitychange',
+      propertyName: 'hidden'
+    },
+    moz: {
+      eventName: 'mozvisibilitychange',
+      propertyName: 'mozHidden'
+    },
+    ms: {
+      eventName: 'msvisibilitychange',
+      propertyName: 'msHidden'
+    },
+    webkit: {
+      eventName: 'webkitvisibilitychange',
+      propertyName: 'webkitHidden'
+    }
+  };
+
+  Object.keys(features).some(function(feature) {
+    if (document[features[feature].propertyName] !== undefined) {
+      detectedFeature = features[feature];
+      return true;
+    }
+  });
+
+  if (detectedFeature) {
+    $document.on(detectedFeature.eventName, broadcastChangeEvent);
+  }
+
+  return {
+    /**
+     * Is the window currently hidden or not.
+     */
+    isHidden: function() {
+      if (detectedFeature) {
+        return document[detectedFeature.propertyName];
+      }
+    }
+  };
+}]);
\ No newline at end of file
diff --git a/static/js/services/notification-service.js b/static/js/services/notification-service.js
index ade256a63..47eddfd2b 100644
--- a/static/js/services/notification-service.js
+++ b/static/js/services/notification-service.js
@@ -196,6 +196,10 @@ function($rootScope, $interval, UserService, ApiService, StringBuilderService, P
   };
 
   notificationService.getClasses = function(notifications) {
+    if (!notifications.length) {
+      return '';
+    }
+
     var classes = [];
     for (var i = 0; i < notifications.length; ++i) {
       var notification = notifications[i];
diff --git a/static/js/services/trigger-service.js b/static/js/services/trigger-service.js
index e7c5f6cba..fd35b26e6 100644
--- a/static/js/services/trigger-service.js
+++ b/static/js/services/trigger-service.js
@@ -122,7 +122,7 @@ angular.module('quay').factory('TriggerService', ['UtilService', '$sanitize', 'K
           'title': 'Commit',
           'type': 'regex',
           'name': 'commit_sha',
-          'regex': '^([A-Fa-f0-9]{7})$',
+          'regex': '^([A-Fa-f0-9]{7,})$',
           'placeholder': '1c002dd'
         }
       ],
diff --git a/storage/__init__.py b/storage/__init__.py
index 7893343c2..69f26def4 100644
--- a/storage/__init__.py
+++ b/storage/__init__.py
@@ -2,6 +2,7 @@ from storage.local import LocalStorage
 from storage.cloud import S3Storage, GoogleCloudStorage, RadosGWStorage
 from storage.fakestorage import FakeStorage
 from storage.distributedstorage import DistributedStorage
+from storage.swift import SwiftStorage
 
 
 STORAGE_DRIVER_CLASSES = {
@@ -9,6 +10,7 @@ STORAGE_DRIVER_CLASSES = {
   'S3Storage': S3Storage,
   'GoogleCloudStorage': GoogleCloudStorage,
   'RadosGWStorage': RadosGWStorage,
+  'SwiftStorage': SwiftStorage,
 }
 
 def get_storage_driver(storage_params):
diff --git a/storage/fakestorage.py b/storage/fakestorage.py
index a0773d0c8..f351ca150 100644
--- a/storage/fakestorage.py
+++ b/storage/fakestorage.py
@@ -1,27 +1,31 @@
 from storage.basestorage import BaseStorage
 
+_FAKE_STORAGE_MAP = {}
 
 class FakeStorage(BaseStorage):
   def _init_path(self, path=None, create=False):
     return path
 
   def get_content(self, path):
-    raise IOError('Fake files are fake!')
+    if not path in _FAKE_STORAGE_MAP:
+      raise IOError('Fake file %s not found' % path)
+
+    return _FAKE_STORAGE_MAP.get(path)
 
   def put_content(self, path, content):
-    return path
+    _FAKE_STORAGE_MAP[path] = content
 
   def stream_read(self, path):
-    yield ''
+    yield _FAKE_STORAGE_MAP[path]
 
   def stream_write(self, path, fp, content_type=None, content_encoding=None):
-    pass
+    _FAKE_STORAGE_MAP[path] = fp.read()
 
   def remove(self, path):
-    pass
+    _FAKE_STORAGE_MAP.pop(path, None)
 
   def exists(self, path):
-    return False
+    return path in _FAKE_STORAGE_MAP
 
   def get_checksum(self, path):
-    return 'abcdefg'
+    return path
diff --git a/storage/swift.py b/storage/swift.py
new file mode 100644
index 000000000..ddeae9105
--- /dev/null
+++ b/storage/swift.py
@@ -0,0 +1,188 @@
+""" Swift storage driver. Based on: github.com/bacongobbler/docker-registry-driver-swift/ """
+from swiftclient.client import Connection, ClientException
+from storage.basestorage import BaseStorage
+
+from random import SystemRandom
+import string
+import logging
+
+logger = logging.getLogger(__name__)
+
+class SwiftStorage(BaseStorage):
+  def __init__(self, swift_container, storage_path, auth_url, swift_user,
+               swift_password, auth_version=None, os_options=None, ca_cert_path=None):
+    self._swift_container = swift_container
+    self._storage_path = storage_path
+
+    self._auth_url = auth_url
+    self._ca_cert_path = ca_cert_path
+
+    self._swift_user = swift_user
+    self._swift_password = swift_password
+
+    self._auth_version = auth_version or 2
+    self._os_options = os_options or {}
+
+    self._initialized = False
+    self._swift_connection = None
+
+  def _initialize(self):
+    if self._initialized:
+      return
+
+    self._initialized = True
+    self._swift_connection = self._get_connection()
+
+  def _get_connection(self):
+    return Connection(
+        authurl=self._auth_url,
+        cacert=self._ca_cert_path,
+
+        user=self._swift_user,
+        key=self._swift_password,
+
+        auth_version=self._auth_version,
+        os_options=self._os_options)
+
+  def _get_relative_path(self, path):
+    if path.startswith(self._storage_path):
+      path = path[len(self._storage_path)]
+
+    if path.endswith('/'):
+      path = path[:-1]
+
+    return path
+
+  def _normalize_path(self, path=None):
+    path = self._storage_path + (path or '')
+
+    # Openstack does not like paths starting with '/' and we always normalize
+    # to remove trailing '/'
+    if path.startswith('/'):
+      path = path[1:]
+
+    if path.endswith('/'):
+      path = path[:-1]
+
+    return path
+
+  def _get_container(self, path):
+    self._initialize()
+    path = self._normalize_path(path)
+
+    if path and not path.endswith('/'):
+        path += '/'
+
+    try:
+       _, container = self._swift_connection.get_container(
+            container=self._swift_container,
+            prefix=path, delimiter='/')
+       return container
+    except:
+      logger.exception('Could not get container: %s', path)
+      raise IOError('Unknown path: %s' % path)
+
+  def _get_object(self, path, chunk_size=None):
+    self._initialize()
+    path = self._normalize_path(path)
+    try:
+      _, obj = self._swift_connection.get_object(self._swift_container, path,
+          resp_chunk_size=chunk_size)
+      return obj
+    except Exception:
+      logger.exception('Could not get object: %s', path)
+      raise IOError('Path %s not found' % path)
+
+  def _put_object(self, path, content, chunk=None, content_type=None, content_encoding=None):
+    self._initialize()
+    path = self._normalize_path(path)
+    headers = {}
+
+    if content_encoding is not None:
+      headers['Content-Encoding'] = content_encoding
+
+    try:
+      self._swift_connection.put_object(self._swift_container, path, content,
+                                        chunk_size=chunk, content_type=content_type,
+                                        headers=headers)
+    except ClientException:
+      # We re-raise client exception here so that validation of config during setup can see
+      # the client exception messages.
+      raise
+    except Exception:
+      logger.exception('Could not put object: %s', path)
+      raise IOError("Could not put content: %s" % path)
+
+  def _head_object(self, path):
+    self._initialize()
+    path = self._normalize_path(path)
+    try:
+      return self._swift_connection.head_object(self._swift_container, path)
+    except Exception:
+      logger.exception('Could not head object: %s', path)
+      return None
+
+  def get_direct_download_url(self, path, expires_in=60, requires_cors=False):
+    if requires_cors:
+      return None
+
+    # TODO(jschorr): This method is not strictly necessary but would result in faster operations
+    # when using this storage engine. However, the implementation (as seen in the link below)
+    # is not clean, so we punt on this for now.
+    # http://docs.openstack.org/juno/config-reference/content/object-storage-tempurl.html
+    return None
+
+  def get_content(self, path):
+    return self._get_object(path)
+
+  def put_content(self, path, content):
+    self._put_object(path, content)
+
+  def stream_read(self, path):
+    for data in self._get_object(path, self.buffer_size):
+      yield data
+
+  def stream_read_file(self, path):
+    raise NotImplementedError
+
+  def stream_write(self, path, fp, content_type=None, content_encoding=None):
+    self._put_object(path, fp, self.buffer_size, content_type=content_type,
+                     content_encoding=content_encoding)
+
+  def list_directory(self, path=None):
+    container = self._get_container(path)
+    if not container:
+      raise OSError('Unknown path: %s' % path)
+
+    for entry in container:
+      param = None
+      if 'name' in entry:
+        param = 'name'
+      elif 'subdir' in entry:
+        param = 'subdir'
+      else:
+        continue
+
+      yield self._get_relative_path(entry[param])
+
+  def exists(self, path):
+    return bool(self._head_object(path))
+
+  def remove(self, path):
+    self._initialize()
+    path = self._normalize_path(path)
+    try:
+      self._swift_connection.delete_object(self._swift_container, path)
+    except Exception:
+      raise IOError('Cannot delete path: %s' % path)
+
+  def _random_checksum(self, count):
+    chars = string.ascii_uppercase + string.digits
+    return ''.join(SystemRandom().choice(chars) for _ in range(count))
+
+  def get_checksum(self, path):
+    headers = self._head_object(path)
+    if not headers:
+      raise IOError('Cannot lookup path: %s' % path)
+
+    return headers.get('etag', '')[1:-1][:7] or self._random_checksum(7)
diff --git a/test/registry_tests.py b/test/registry_tests.py
new file mode 100644
index 000000000..bacc9ee8a
--- /dev/null
+++ b/test/registry_tests.py
@@ -0,0 +1,247 @@
+import unittest
+import requests
+
+from flask.blueprints import Blueprint
+from flask.ext.testing import LiveServerTestCase
+
+from app import app
+from endpoints.registry import registry
+from endpoints.index import index
+from endpoints.tags import tags
+from endpoints.api import api_bp
+from initdb import wipe_database, initialize_database, populate_database
+from endpoints.csrf import generate_csrf_token
+
+import endpoints.decorated
+import json
+
+import tarfile
+
+from cStringIO import StringIO
+from util.checksums import compute_simple
+
+try:
+  app.register_blueprint(index, url_prefix='/v1')
+  app.register_blueprint(tags, url_prefix='/v1')
+  app.register_blueprint(registry, url_prefix='/v1')
+  app.register_blueprint(api_bp, url_prefix='/api')
+except ValueError:
+  # Blueprint was already registered
+  pass
+
+
+# Add a test blueprint for generating CSRF tokens.
+testbp = Blueprint('testbp', __name__)
+@testbp.route('/csrf', methods=['GET'])
+def generate_csrf():
+  return generate_csrf_token()
+
+app.register_blueprint(testbp, url_prefix='/__test')
+
+
+class RegistryTestCase(LiveServerTestCase):
+  maxDiff = None
+
+  def create_app(self):
+    app.config['TESTING'] = True
+    return app
+
+  def setUp(self):
+    # Note: We cannot use the normal savepoint-based DB setup here because we are accessing
+    # different app instances remotely via a live webserver, which is multiprocess. Therefore, we
+    # completely clear the database between tests.
+    wipe_database()
+    initialize_database()
+    populate_database()
+
+    self.clearSession()
+
+  def clearSession(self):
+    self.session = requests.Session()
+    self.signature = None
+    self.docker_token = 'true'
+
+    # Load the CSRF token.
+    self.csrf_token = ''
+    self.csrf_token = self.conduct('GET', '/__test/csrf').text
+
+  def conduct(self, method, url, headers=None, data=None, auth=None, expected_code=200):
+    headers = headers or {}
+    headers['X-Docker-Token'] = self.docker_token
+
+    if self.signature and not auth:
+      headers['Authorization'] = 'token ' + self.signature
+
+    response = self.session.request(method, self.get_server_url() + url, headers=headers, data=data,
+                                    auth=auth, params=dict(_csrf_token=self.csrf_token))
+    if response.status_code != expected_code:
+      print response.text
+
+    if 'www-authenticate' in response.headers:
+      self.signature = response.headers['www-authenticate']
+
+    if 'X-Docker-Token' in response.headers:
+      self.docker_token = response.headers['X-Docker-Token']
+
+    self.assertEquals(response.status_code, expected_code)
+    return response
+
+  def ping(self):
+    self.conduct('GET', '/v1/_ping')
+
+  def do_login(self, username, password='password'):
+    self.ping()
+    result = self.conduct('POST', '/v1/users/',
+                           data=json.dumps(dict(username=username, password=password,
+                                                email='bar@example.com')),
+                           headers={"Content-Type": "application/json"},
+                           expected_code=400)
+
+    self.assertEquals(result.text, '"Username or email already exists"')
+    self.conduct('GET', '/v1/users/', auth=(username, password))
+
+  def do_push(self, namespace, repository, username, password, images):
+    auth = (username, password)
+
+    # Ping!
+    self.ping()
+
+    # PUT /v1/repositories/{namespace}/{repository}/
+    data = [{"id": image['id']} for image in images]
+    self.conduct('PUT', '/v1/repositories/%s/%s' % (namespace, repository),
+                 data=json.dumps(data), auth=auth,
+                 expected_code=201)
+
+    for image in images:
+      # PUT /v1/images/{imageID}/json
+      self.conduct('PUT', '/v1/images/%s/json' % image['id'], data=json.dumps(image))
+
+      # PUT /v1/images/{imageID}/layer
+      tar_file_info = tarfile.TarInfo(name='image_name')
+      tar_file_info.type = tarfile.REGTYPE
+      tar_file_info.size = len(image['id'])
+
+      layer_data = StringIO()
+
+      tar_file = tarfile.open(fileobj=layer_data, mode='w|gz')
+      tar_file.addfile(tar_file_info, StringIO(image['id']))
+      tar_file.close()
+
+      layer_bytes = layer_data.getvalue()
+      layer_data.close()
+
+      self.conduct('PUT', '/v1/images/%s/layer' % image['id'], data=StringIO(layer_bytes))
+
+      # PUT /v1/images/{imageID}/checksum
+      checksum = compute_simple(StringIO(layer_bytes), json.dumps(image))
+      self.conduct('PUT', '/v1/images/%s/checksum' % image['id'],
+                   headers={'X-Docker-Checksum-Payload': checksum})
+
+
+    # PUT /v1/repositories/{namespace}/{repository}/tags/latest
+    self.conduct('PUT', '/v1/repositories/%s/%s/tags/latest' % (namespace, repository),
+                 data='"' + images[0]['id'] + '"')
+
+    # PUT /v1/repositories/{namespace}/{repository}/images
+    self.conduct('PUT', '/v1/repositories/%s/%s/images' % (namespace, repository),
+                 expected_code=204)
+
+
+  def do_pull(self, namespace, repository, username=None, password='password', expected_code=200):
+    auth = None
+    if username:
+      auth = (username, password)
+
+    # Ping!
+    self.ping()
+
+    prefix = '/v1/repositories/%s/%s/' % (namespace, repository)
+
+    # GET /v1/repositories/{namespace}/{repository}/
+    self.conduct('GET', prefix + 'images', auth=auth, expected_code=expected_code)
+    if expected_code != 200:
+      return
+
+    # GET /v1/repositories/{namespace}/{repository}/
+    result = json.loads(self.conduct('GET', prefix + 'tags').text)
+
+    for image_id in result.values():
+      # /v1/images/{imageID}/{ancestry, json, layer}
+      image_prefix = '/v1/images/%s/' % image_id
+      self.conduct('GET', image_prefix + 'ancestry')
+      self.conduct('GET', image_prefix + 'json')
+      self.conduct('GET', image_prefix + 'layer')
+
+  def conduct_api_login(self, username, password):
+    self.conduct('POST', '/api/v1/signin',
+                 data=json.dumps(dict(username=username, password=password)),
+                 headers={'Content-Type': 'application/json'})
+
+  def change_repo_visibility(self, repository, namespace, visibility):
+    self.conduct('POST', '/api/v1/repository/%s/%s/changevisibility' % (repository, namespace),
+                 data=json.dumps(dict(visibility=visibility)),
+                 headers={'Content-Type': 'application/json'})
+
+
+class RegistryTests(RegistryTestCase):
+  def test_pull_publicrepo_anonymous(self):
+    # Add a new repository under the public user, so we have a real repository to pull.
+    images = [{
+      'id': 'onlyimagehere'
+    }]
+    self.do_push('public', 'newrepo', 'public', 'password', images)
+    self.clearSession()
+
+    # First try to pull the (currently private) repo anonymously, which should fail (since it is
+    # private)
+    self.do_pull('public', 'newrepo', expected_code=403)
+
+    # Make the repository public.
+    self.conduct_api_login('public', 'password')
+    self.change_repo_visibility('public', 'newrepo', 'public')
+    self.clearSession()
+
+    # Pull the repository anonymously, which should succeed because the repository is public.
+    self.do_pull('public', 'newrepo')
+
+
+  def test_pull_publicrepo_devtable(self):
+    # Add a new repository under the public user, so we have a real repository to pull.
+    images = [{
+      'id': 'onlyimagehere'
+    }]
+    self.do_push('public', 'newrepo', 'public', 'password', images)
+    self.clearSession()
+
+    # First try to pull the (currently private) repo as devtable, which should fail as it belongs
+    # to public.
+    self.do_pull('public', 'newrepo', 'devtable', 'password', expected_code=403)
+
+    # Make the repository public.
+    self.conduct_api_login('public', 'password')
+    self.change_repo_visibility('public', 'newrepo', 'public')
+    self.clearSession()
+
+    # Pull the repository as devtable, which should succeed because the repository is public.
+    self.do_pull('public', 'newrepo', 'devtable', 'password')
+
+
+  def test_pull_private_repo(self):
+    # Add a new repository under the devtable user, so we have a real repository to pull.
+    images = [{
+      'id': 'onlyimagehere'
+    }]
+    self.do_push('devtable', 'newrepo', 'devtable', 'password', images)
+    self.clearSession()
+
+    # First try to pull the (currently private) repo as public, which should fail as it belongs
+    # to devtable.
+    self.do_pull('devtable', 'newrepo', 'public', 'password', expected_code=403)
+
+    # Pull the repository as devtable, which should succeed because the repository is owned by
+    # devtable.
+    self.do_pull('devtable', 'newrepo', 'devtable', 'password')
+
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/test/test_ldap.py b/test/test_ldap.py
index 323a87a4e..4737d4518 100644
--- a/test/test_ldap.py
+++ b/test/test_ldap.py
@@ -38,45 +38,95 @@ class TestLDAP(unittest.TestCase):
         'ou': 'employees',
         'uid': ['nomail'],
         'userPassword': ['somepass']
-      }
+      },
+      'uid=cool.user,ou=employees,dc=quay,dc=io': {
+        'dc': ['quay', 'io'],
+        'ou': 'employees',
+        'uid': ['cool.user', 'referred'],
+        'userPassword': ['somepass'],
+        'mail': ['foo@bar.com']
+      },
+      'uid=referred,ou=employees,dc=quay,dc=io': {
+        'uid': ['referred'],
+        '_referral': 'ldap:///uid=cool.user,ou=employees,dc=quay,dc=io'
+      },
+      'uid=invalidreferred,ou=employees,dc=quay,dc=io': {
+        'uid': ['invalidreferred'],
+        '_referral': 'ldap:///uid=someinvaliduser,ou=employees,dc=quay,dc=io'
+      },
+      'uid=multientry,ou=subgroup1,ou=employees,dc=quay,dc=io': {
+        'uid': ['multientry'],
+        'mail': ['foo@bar.com'],
+        'userPassword': ['somepass'],
+      },
+      'uid=multientry,ou=subgroup2,ou=employees,dc=quay,dc=io': {
+        'uid': ['multientry'],
+        'another': ['key']
+      },
     })
 
     self.mockldap.start()
 
+    base_dn = ['dc=quay', 'dc=io']
+    admin_dn = 'uid=testy,ou=employees,dc=quay,dc=io'
+    admin_passwd = 'password'
+    user_rdn = ['ou=employees']
+    uid_attr = 'uid'
+    email_attr = 'mail'
+
+    ldap = LDAPUsers('ldap://localhost', base_dn, admin_dn, admin_passwd, user_rdn,
+                     uid_attr, email_attr)
+
+    self.ldap = ldap
+
+
   def tearDown(self):
     self.mockldap.stop()
     finished_database_for_testing(self)
     self.ctx.__exit__(True, None, None)
 
   def test_login(self):
-    base_dn = ['dc=quay', 'dc=io']
-    admin_dn = 'uid=testy,ou=employees,dc=quay,dc=io'
-    admin_passwd = 'password'
-    user_rdn = ['ou=employees']
-    uid_attr = 'uid'
-    email_attr = 'mail'
+    # Verify we can login.
+    (response, _) = self.ldap.verify_user('someuser', 'somepass')
+    self.assertEquals(response.username, 'someuser')
 
-    ldap = LDAPUsers('ldap://localhost', base_dn, admin_dn, admin_passwd, user_rdn,
-                     uid_attr, email_attr)
-
-    (response, _) = ldap.verify_user('someuser', 'somepass')
+    # Verify we can confirm the user.
+    (response, _) = self.ldap.confirm_existing_user('someuser', 'somepass')
     self.assertEquals(response.username, 'someuser')
 
   def test_missing_mail(self):
-    base_dn = ['dc=quay', 'dc=io']
-    admin_dn = 'uid=testy,ou=employees,dc=quay,dc=io'
-    admin_passwd = 'password'
-    user_rdn = ['ou=employees']
-    uid_attr = 'uid'
-    email_attr = 'mail'
-
-    ldap = LDAPUsers('ldap://localhost', base_dn, admin_dn, admin_passwd, user_rdn,
-                     uid_attr, email_attr)
-
-    (response, err_msg) = ldap.verify_user('nomail', 'somepass')
+    (response, err_msg) = self.ldap.verify_user('nomail', 'somepass')
     self.assertIsNone(response)
     self.assertEquals('Missing mail field "mail" in user record', err_msg)
 
+  def test_confirm_different_username(self):
+    # Verify that the user is logged in and their username was adjusted.
+    (response, _) = self.ldap.verify_user('cool.user', 'somepass')
+    self.assertEquals(response.username, 'cool_user')
+
+    # Verify we can confirm the user's quay username.
+    (response, _) = self.ldap.confirm_existing_user('cool_user', 'somepass')
+    self.assertEquals(response.username, 'cool_user')
+
+    # Verify that we *cannot* confirm the LDAP username.
+    (response, _) = self.ldap.confirm_existing_user('cool.user', 'somepass')
+    self.assertIsNone(response)
+
+  def test_referral(self):
+    (response, _) = self.ldap.verify_user('referred', 'somepass')
+    self.assertEquals(response.username, 'cool_user')
+
+    # Verify we can confirm the user's quay username.
+    (response, _) = self.ldap.confirm_existing_user('cool_user', 'somepass')
+    self.assertEquals(response.username, 'cool_user')
+
+  def test_invalid_referral(self):
+    (response, _) = self.ldap.verify_user('invalidreferred', 'somepass')
+    self.assertIsNone(response)
+
+  def test_multientry(self):
+    (response, _) = self.ldap.verify_user('multientry', 'somepass')
+    self.assertEquals(response.username, 'multientry')
 
 if __name__ == '__main__':
   unittest.main()
diff --git a/test/test_trigger.py b/test/test_trigger.py
new file mode 100644
index 000000000..d01853d96
--- /dev/null
+++ b/test/test_trigger.py
@@ -0,0 +1,29 @@
+import unittest
+import re
+
+from endpoints.trigger import matches_ref
+
+class TestRegex(unittest.TestCase):
+  def assertDoesNotMatch(self, ref, filt):
+    self.assertFalse(matches_ref(ref, re.compile(filt)))
+
+  def assertMatches(self, ref, filt):
+    self.assertTrue(matches_ref(ref, re.compile(filt)))
+
+  def test_matches_ref(self):
+    self.assertMatches('ref/heads/master', '.+')
+    self.assertMatches('ref/heads/master', 'heads/.+')
+    self.assertMatches('ref/heads/master', 'heads/master')
+
+    self.assertDoesNotMatch('ref/heads/foobar', 'heads/master')
+    self.assertDoesNotMatch('ref/heads/master', 'tags/master')
+
+    self.assertMatches('ref/heads/master', '(((heads/alpha)|(heads/beta))|(heads/gamma))|(heads/master)')
+    self.assertMatches('ref/heads/alpha', '(((heads/alpha)|(heads/beta))|(heads/gamma))|(heads/master)')
+    self.assertMatches('ref/heads/beta', '(((heads/alpha)|(heads/beta))|(heads/gamma))|(heads/master)')
+    self.assertMatches('ref/heads/gamma', '(((heads/alpha)|(heads/beta))|(heads/gamma))|(heads/master)')
+
+    self.assertDoesNotMatch('ref/heads/delta', '(((heads/alpha)|(heads/beta))|(heads/gamma))|(heads/master)')
+
+if __name__ == '__main__':
+  unittest.main()
diff --git a/tools/sendconfirmemail.py b/tools/sendconfirmemail.py
index 94345c573..2ec28132c 100644
--- a/tools/sendconfirmemail.py
+++ b/tools/sendconfirmemail.py
@@ -10,7 +10,7 @@ from flask import Flask, current_app
 from flask_mail import Mail
 
 def sendConfirmation(username):
-  user = model.get_user(username)
+  user = model.get_nonrobot_user(username)
   if not user:
     print 'No user found'
     return
diff --git a/tools/sendresetemail.py b/tools/sendresetemail.py
index e977c654e..0ead72283 100644
--- a/tools/sendresetemail.py
+++ b/tools/sendresetemail.py
@@ -10,7 +10,7 @@ from flask import Flask, current_app
 from flask_mail import Mail
 
 def sendReset(username):
-  user = model.get_user(username)
+  user = model.get_nonrobot_user(username)
   if not user:
     print 'No user found'
     return
diff --git a/util/cache.py b/util/cache.py
index 20c1a97c8..13c0949de 100644
--- a/util/cache.py
+++ b/util/cache.py
@@ -29,6 +29,7 @@ def no_cache(f):
   @wraps(f)
   def add_no_cache(*args, **kwargs):
     response = f(*args, **kwargs)
-    response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
+    if response is not None:
+      response.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
     return response
   return add_no_cache
diff --git a/util/headers.py b/util/headers.py
index 8967dfaa7..353cc61dc 100644
--- a/util/headers.py
+++ b/util/headers.py
@@ -11,7 +11,7 @@ def parse_basic_auth(header_value):
     return None
 
   try:
-    basic_parts = base64.b64decode(parts[1]).split(':')
+    basic_parts = base64.b64decode(parts[1]).split(':', 1)
     if len(basic_parts) != 2:
       return None