Add a restricted mode (read authentication required)

Enable running the registry in a mode where authentication is required
for pulling images.  This could be useful in an environment where even
an intermediate or buildset registry should require authentication to
pull images.  Or it could make this more useful as a general registry
(that's not a priority use case for this project, but this doesn't add
much complexity).

If a "read" level user is specified, then we assume that anonymous
read access should not be allowed.

Change-Id: I1455a1031590ff0206a4b6da0d8c08093cf0e3cd
This commit is contained in:
James E. Blair 2021-04-27 19:30:33 -07:00
parent bf98edd796
commit b635c65cf3
5 changed files with 141 additions and 12 deletions

View File

@ -0,0 +1,70 @@
# Test push and pull from the registry in restricted mode (read access
# restricted)
- name: Start the registry
shell:
cmd: docker-compose up -d
chdir: "{{ ansible_user_dir }}/src/opendev.org/zuul/zuul-registry/playbooks/functional-test/restricted"
- name: Print list of images
command: docker image ls --all --digests --no-trunc
register: image_list
failed_when: "'test/image' in image_list.stdout"
- name: Copy the test image into local docker image storage
command: >
skopeo copy
docker-archive:{{ workspace }}/test.img
docker-daemon:localhost:9000/test/image:latest
- name: Log in to registry
command: docker login localhost:9000 -u writeuser -p writepass
- name: Push the test image to the registry
command: docker push localhost:9000/test/image
- name: Remove the test image from the local cache
command: docker rmi localhost:9000/test/image
- name: Log out of registry
command: docker logout localhost:9000
- name: Try to pull the image from the registry unauthenticated
command: docker pull localhost:9000/test/image
register: result
failed_when: result.rc != 1
- name: Log in to registry
command: docker login localhost:9000 -u readuser -p readpass
- name: Print list of images
command: docker image ls --all --digests --no-trunc
register: image_list
failed_when: "'test/image' in image_list.stdout"
- name: Pull the image from the registry
command: docker pull localhost:9000/test/image
- name: Print list of images
command: docker image ls --all --digests --no-trunc
register: image_list
failed_when: "'test/image' not in image_list.stdout"
- name: Try to pull an image that does not exist
command: docker pull localhost:9000/test/dne
register: result
failed_when: result.rc != 1
- name: Remove the test image from the local cache
command: docker rmi localhost:9000/test/image
- name: Stop the registry
shell:
cmd: docker-compose down
chdir: "{{ ansible_user_dir }}/src/opendev.org/zuul/zuul-registry/playbooks/functional-test/restricted"
- name: Clean up docker volumes
command: docker volume prune -f
- name: Log out of registry
command: docker logout localhost:9000

View File

@ -0,0 +1,17 @@
registry:
address: '0.0.0.0'
port: 9000
public-url: https://localhost:9000
tls-cert: /tls/cert.pem
tls-key: /tls/cert.key
secret: test_token_secret
users:
- name: writeuser
pass: writepass
access: write
- name: readuser
pass: readpass
access: read
storage:
driver: filesystem
root: /storage

View File

@ -0,0 +1,12 @@
# Version 2 is the latest that is supported by docker-compose in
# Ubuntu Xenial.
version: '2'
services:
registry:
image: zuul/zuul-registry
volumes:
- "./conf/:/conf/:z"
- "/tmp/registry-test/tls/:/tls:z"
ports:
- "9000:9000"

View File

@ -35,6 +35,12 @@
- name: Run docker test tasks
include_tasks: docker.yaml
- hosts: all
name: Run restricted buildset registry test
tasks:
- name: Run restricted buildset test tasks
include_tasks: restricted.yaml
- hosts: all
name: Run podman standard registry test
tasks:

View File

@ -1,4 +1,5 @@
# Copyright 2019 Red Hat, Inc.
# Copyright 2021 Acme Gating, LLC
#
# This module is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@ -47,11 +48,20 @@ class Authorization(cherrypy.Tool):
self.secret = secret
self.public_url = public_url
self.rw = {}
self.ro = {}
self.anonymous_read = True
for user in users:
if user['access'] == self.WRITE:
self.rw[user['name']] = user['pass']
if user['access'] == self.READ:
self.ro[user['name']] = user['pass']
self.anonymous_read = False
if self.anonymous_read:
self.log.info("Anonymous read access enabled")
else:
self.log.info("Anonymous read access disabled")
cherrypy.Tool.__init__(self, 'before_handler',
self.check_auth,
priority=1)
@ -90,10 +100,13 @@ class Authorization(cherrypy.Tool):
and level is None):
level = self.READ
if level is None:
# No scope was provided, so this is an authentication
# request; treat it as requesting 'write' access so that
# we validate the password.
level = self.WRITE
if self.anonymous_read:
# No scope was provided, so this is an authentication
# request; treat it as requesting 'write' access so
# that we validate the password.
level = self.WRITE
else:
level = self.READ
return level
@cherrypy.expose
@ -119,19 +132,30 @@ class Authorization(cherrypy.Tool):
level = self._get_level(kw.get('scope', ''))
self.log.info('Authenticate level %s', level)
if level == self.WRITE:
if auth_header and 'Basic' in auth_header:
cred = auth_header.split()[1]
cred = base64.decodebytes(cred.encode('utf8')).decode('utf8')
user, pw = cred.split(':', 1)
if not self.check(self.rw, user, pw):
self.unauthorized()
else:
self.unauthorized()
self._check_creds(auth_header, [self.rw])
elif level == self.READ and not self.anonymous_read:
self._check_creds(auth_header, [self.rw, self.ro])
# If we permit anonymous read and we're requesting read, no
# check is performed.
self.log.debug('Generate %s token', level)
token = jwt.encode({'level': level}, 'secret', algorithm='HS256')
return {'token': token,
'access_token': token}
def _check_creds(self, auth_header, credstores):
# If the password is okay, fall through; otherwise call
# unauthorized for the side effect of raising an exception.
if auth_header and 'Basic' in auth_header:
cred = auth_header.split()[1]
cred = base64.decodebytes(cred.encode('utf8')).decode('utf8')
user, pw = cred.split(':', 1)
# Return true on the first credstore with the user, false otherwise
if not next(filter(
lambda cs: self.check(cs, user, pw), credstores), False):
self.unauthorized()
else:
self.unauthorized()
class RegistryAPI:
"""Registry API server.