diff --git a/playbooks/functional-test/restricted.yaml b/playbooks/functional-test/restricted.yaml new file mode 100644 index 0000000..a89d802 --- /dev/null +++ b/playbooks/functional-test/restricted.yaml @@ -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 diff --git a/playbooks/functional-test/restricted/conf/registry.yaml b/playbooks/functional-test/restricted/conf/registry.yaml new file mode 100644 index 0000000..70b178f --- /dev/null +++ b/playbooks/functional-test/restricted/conf/registry.yaml @@ -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 diff --git a/playbooks/functional-test/restricted/docker-compose.yaml b/playbooks/functional-test/restricted/docker-compose.yaml new file mode 100644 index 0000000..4d6c45d --- /dev/null +++ b/playbooks/functional-test/restricted/docker-compose.yaml @@ -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" diff --git a/playbooks/functional-test/run.yaml b/playbooks/functional-test/run.yaml index 73b0a95..8e4da26 100644 --- a/playbooks/functional-test/run.yaml +++ b/playbooks/functional-test/run.yaml @@ -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: diff --git a/zuul_registry/main.py b/zuul_registry/main.py index abf6bd0..3726126 100644 --- a/zuul_registry/main.py +++ b/zuul_registry/main.py @@ -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.