diff --git a/Dockerfile b/Dockerfile index 5a1bf0a1e..f319fe048 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,7 @@ COPY requirements.txt requirements-tests.txt ./ # Due to the following bug, pip results must be piped to a file before grepping: # https://github.com/pypa/pip/pull/3304 RUN cat requirements.txt | grep -v "^-e" | awk -F'==' '{print $1}' | xargs venv/bin/pip --disable-pip-version-check show > pipinfo.txt && \ - test -z $(cat pipinfo.txt | grep GPL | grep -v LGPL) && \ + test -z "$(cat pipinfo.txt | grep GPL | grep -v LGPL)" && \ rm pipinfo.txt RUN virtualenv --distribute venv \ @@ -57,4 +57,4 @@ RUN PYTHONPATH=$QUAYPATH venv/bin/alembic heads | grep -E '^[0-9a-f]+ \(head\)$' RUN ./scripts/detect-config.sh -CMD ./quay-entrypoint.sh \ No newline at end of file +CMD ./quay-entrypoint.sh diff --git a/auth/test/test_registry_jwt.py b/auth/test/test_registry_jwt.py index 12000a95a..78ac029bc 100644 --- a/auth/test/test_registry_jwt.py +++ b/auth/test/test_registry_jwt.py @@ -110,30 +110,31 @@ def test_token_with_access(access, initialized_db): @pytest.mark.parametrize('token', [ - (_token( + pytest.param(_token( _token_data(access=[{ 'toipe': 'repository', 'namesies': 'somens/somerepo', - 'akshuns': ['pull', 'push', '*']}]))), - (_token(_token_data(audience='someotherapp'))), - (_token(_delete_field(_token_data(), 'aud'))), - (_token(_token_data(nbf=int(time.time()) + 600))), - (_token(_delete_field(_token_data(), 'nbf'))), - (_token(_token_data(iat=int(time.time()) + 600))), - (_token(_delete_field(_token_data(), 'iat'))), - (_token(_token_data(exp=int(time.time()) + MAX_SIGNED_S * 2))), - (_token(_token_data(exp=int(time.time()) - 60))), - (_token(_delete_field(_token_data(), 'exp'))), - (_token(_delete_field(_token_data(), 'sub'))), - (_token(_token_data(iss='badissuer'))), - (_token(_delete_field(_token_data(), 'iss'))), - (_token(_token_data(), skip_header=True)), - (_token(_token_data(), key_id='someunknownkey')), - (_token(_token_data(), key_id='kid7')), - (_token(_token_data(), alg='none', private_key=None)), - ('some random token'), - ('Bearer: sometokenhere'), - ('\nBearer: dGVzdA'),]) + 'akshuns': ['pull', 'push', '*']}])), id='bad access'), + pytest.param(_token(_token_data(audience='someotherapp')), id='bad aud'), + pytest.param(_token(_delete_field(_token_data(), 'aud')), id='no aud'), + pytest.param(_token(_token_data(nbf=int(time.time()) + 600)), id='future nbf'), + pytest.param(_token(_delete_field(_token_data(), 'nbf')), id='no nbf'), + pytest.param(_token(_token_data(iat=int(time.time()) + 600)), id='future iat'), + pytest.param(_token(_delete_field(_token_data(), 'iat')), id='no iat'), + pytest.param(_token(_token_data(exp=int(time.time()) + MAX_SIGNED_S * 2)), id='exp too long'), + pytest.param(_token(_token_data(exp=int(time.time()) - 60)), id='expired'), + pytest.param(_token(_delete_field(_token_data(), 'exp')), id='no exp'), + pytest.param(_token(_delete_field(_token_data(), 'sub')), id='no sub'), + pytest.param(_token(_token_data(iss='badissuer')), id='bad iss'), + pytest.param(_token(_delete_field(_token_data(), 'iss')), id='no iss'), + pytest.param(_token(_token_data(), skip_header=True), id='no header'), + pytest.param(_token(_token_data(), key_id='someunknownkey'), id='bad key'), + pytest.param(_token(_token_data(), key_id='kid7'), id='bad key :: kid7'), + pytest.param(_token(_token_data(), alg='none', private_key=None), id='none alg'), + pytest.param('some random token', id='random token'), + pytest.param('Bearer: sometokenhere', id='extra bearer'), + pytest.param('\nBearer: dGVzdA', id='leading newline'), +]) def test_invalid_jwt(token, initialized_db): with pytest.raises(InvalidJWTException): _parse_token(token) diff --git a/requirements-nover.txt b/requirements-nover.txt index c7cf76009..5226a7d01 100644 --- a/requirements-nover.txt +++ b/requirements-nover.txt @@ -8,7 +8,7 @@ -e git+https://github.com/coreos/pyapi-gitlab.git@timeout#egg=pyapi-gitlab -e git+https://github.com/coreos/resumablehashlib.git#egg=resumablehashlib -e git+https://github.com/jepcastelein/marketo-rest-python.git#egg=marketorestpython --e git+https://github.com/app-registry/appr-server.git#egg=cnr-server +-e git+https://github.com/app-registry/appr-server.git@c2ef3b88afe926a92ef5f2e11e7d4a259e286a17#egg=cnr_server # naming has changed APScheduler==3.0.5 Flask-Login Flask-Mail @@ -77,5 +77,4 @@ tzlocal xhtml2pdf recaptcha2 mockredispy -cnr yapf diff --git a/requirements.txt b/requirements.txt index 3dfb81be3..36043a933 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,139 +1,147 @@ aiowsgi==0.6 -alembic==0.9.1 +alembic==0.9.8 -e git+https://github.com/coreos/mockldap.git@59a46efbe8c7cd8146a87a7c4f2b09746b953e11#egg=mockldap --e git+https://github.com/coreos/py-bitbucket.git@07a80f63388d004f05f58441983bdf195f9b666e#egg=py_bitbucket +-e git+https://github.com/coreos/py-bitbucket.git@55a1ada645f2fb6369147996ec71edd7828d91c8#egg=py_bitbucket -e git+https://github.com/coreos/pyapi-gitlab.git@136c3970d591136a4f766a846c5d22aad52e124f#egg=pyapi_gitlab -e git+https://github.com/coreos/resumablehashlib.git@b1b631249589b07adf40e0ee545b323a501340b4#egg=resumablehashlib -e git+https://github.com/DevTable/aniso8601-fake.git@bd7762c7dea0498706d3f57db60cd8a8af44ba90#egg=aniso8601 -e git+https://github.com/DevTable/anunidecode.git@d59236a822e578ba3a0e5e5abbd3855873fa7a88#egg=anunidecode -e git+https://github.com/app-registry/appr-server.git@c2ef3b88afe926a92ef5f2e11e7d4a259e286a17#egg=cnr_server --e git+https://github.com/DevTable/container-cloud-config.git@bce675537904175f6975024a4c89269027ea6792#egg=container_cloud_config +-e git+https://github.com/DevTable/container-cloud-config.git@8e8ae177c3d5cd6608f64250fcd8770022d61562#egg=container_cloud_config -e git+https://github.com/DevTable/python-etcd.git@f1168cb02a2a8c83bec1108c6fcd8615ef463b14#egg=python_etcd --e git+https://github.com/jarus/flask-testing.git@18baff32969a0634a414ce61d2dd4a77433817a8#egg=Flask_Testing --e git+https://github.com/jepcastelein/marketo-rest-python.git@e26af0c5acd3d4ad899383442703a053120aa7ab#egg=marketorestpython +-e git+https://github.com/jarus/flask-testing.git@17f19d7fee0e1e176703fc7cb04917a77913ba1a#egg=Flask_Testing +-e git+https://github.com/jepcastelein/marketo-rest-python.git@6134d1129f2435b313c4301503a4b74974d79a42#egg=marketorestpython -e git+https://github.com/NateFerrero/oauth2lib.git@d161b010f8a596826050a09e5e94d59443cc12d9#egg=oauth2lib -appdirs==1.4.3 APScheduler==3.0.5 -asn1crypto==0.22.0 +asn1crypto==0.24.0 autobahn==0.9.3.post3 azure-common==1.1.8 +azure-nspkg==2.0.0 azure-storage-blob==1.1.0 -Babel==2.4.0 -beautifulsoup4==4.5.3 +azure-storage-common==1.1.0 +azure-storage-nspkg==3.0.0 +Babel==2.5.3 +beautifulsoup4==4.6.0 bencode==1.0 -bintrees==2.0.6 +bintrees==2.0.7 bitmath==1.3.1.2 blinker==1.4 -boto==2.46.1 -boto3==1.4.7 +boto==2.48.0 +boto3==1.5.36 +botocore==1.8.50 cachetools==1.1.6 -certifi==2017.11.5 -cffi==1.10.0 +certifi==2018.1.18 +cffi==1.11.4 +chardet==3.0.4 click==6.7 -contextlib2==0.5.4 -cryptography==1.8.1 -debtcollector==1.13.0 -decorator==4.0.11 +contextlib2==0.5.5 +cryptography==2.1.4 +debtcollector==1.19.0 +decorator==4.2.1 enum34==1.1.6 -Flask-Login==0.4.0 +Flask-Cors==3.0.3 +Flask-Login==0.4.1 Flask-Mail==0.9.1 Flask-Principal==0.4.0 -Flask-RESTful==0.3.5 -Flask==0.12.1 +Flask-RESTful==0.3.6 +Flask==0.12.2 funcparserlib==0.3.6 funcsigs==1.0.2 functools32==3.2.3.post2 -furl==1.0.0 +furl==1.0.1 future==0.16.0 -futures==3.0.5 -geoip2==2.5.0 -gevent==1.2.1 +futures==3.2.0 +geoip2==2.7.0 +gevent==1.2.2 gipc==0.6.0 -greenlet==0.4.12 +greenlet==0.4.13 gunicorn==18.0 hiredis==0.2.0 html5lib==0.9999999 httmock==1.2.6 +httplib2==0.10.3 httpretty==0.8.10 -idna==2.5 -ipaddress==1.0.18 -iso8601==0.1.11 +idna==2.6 +ipaddress==1.0.19 +iso8601==0.1.12 itsdangerous==0.24 -Jinja2==2.9.6 +Jinja2==2.10 +jmespath==0.9.3 jsonpath-rw==1.4.0 jsonschema==2.6.0 -keystoneauth1==2.19.0 -Mako==1.0.6 +keystoneauth1==3.4.0 +Mako==1.0.7 marisa-trie==0.7.4 MarkupSafe==1.0 +maxminddb==1.3.0 mixpanel==4.3.2 mock==2.0.0 mockredispy==2.9.3 -monotonic==1.3 +monotonic==1.4 moto==0.4.25 -msgpack-python==0.4.8 +msgpack==0.5.6 namedlist==1.7 -ndg-httpsclient==0.4.2 +ndg-httpsclient==0.4.4 netaddr==0.7.19 -netifaces==0.10.5 -oauthlib==2.0.2 -olefile==0.44 +netifaces==0.10.6 +oauthlib==2.0.6 orderedmultidict==0.7.11 -oslo.config==3.24.0 -oslo.i18n==3.15.0 -oslo.serialization==2.18.0 -oslo.utils==3.25.0 -packaging==16.8 -pathvalidate==0.15.0 -pbr==2.0.0 +oslo.config==5.2.0 +oslo.i18n==3.19.0 +oslo.serialization==2.24.0 +oslo.utils==3.35.0 +pathvalidate==0.16.3 +pbr==3.1.1 peewee==2.8.1 -Pillow==4.1.0 -ply==3.10 -positional==1.1.1 -psutil==5.2.1 -psycopg2==2.7.1 -pyasn1==0.3.7 +Pillow==5.0.0 +ply==3.11 +psutil==5.4.3 +psycopg2==2.7.4 +pyasn1==0.4.2 py-bcrypt==0.4 -pycparser==2.17 -pycryptodome==3.4.5 -pycryptodomex==3.4.5 -PyGithub==1.34 +pycparser==2.18 +pycryptodome==3.4.11 +pycryptodomex==3.4.12 +PyGithub==1.36 pygpgme==0.3 -pyjwkest==1.3.2 -PyJWT==1.4.2 +pyjwkest==1.4.0 +PyJWT==1.5.3 PyMySQL==0.6.7 -pyOpenSSL==16.2.0 +pyOpenSSL==17.5.0 pyparsing==2.2.0 PyPDF2==1.26.0 -python-dateutil==2.6.0 +python-dateutil==2.6.1 python-editor==1.0.3 -python-keystoneclient==3.10.0 -python-ldap==2.4.32 -python-magic==0.4.13 -python-swiftclient==3.3.0 -pytz==2017.2 +python-keystoneclient==3.15.0 +python-ldap==2.5.2 +python-magic==0.4.15 +python-swiftclient==3.5.0 +pytz==2018.3 PyYAML==3.12 -raven==6.0.0 +raven==6.5.0 recaptcha2==0.1 -redis==2.10.5 +redis==2.10.6 redlock==1.2.0 reportlab==2.7 requests-oauthlib==0.8.0 requests[security]==2.18.4 -rfc3986==0.4.1 +rfc3986==1.1.0 +s3transfer==0.1.13 semantic-version==2.6.0 -six==1.10.0 +simplejson==3.13.2 +six==1.11.0 SQLAlchemy==1.1.5 -stevedore==1.21.0 +stevedore==1.28.0 stringscore==0.1.0 -stripe==1.51.0 +stripe==1.79.0 toposort==1.5 trollius==2.1 -tzlocal==1.3 -urllib3==1.20 -waitress==1.0.2 -WebOb==1.7.2 -Werkzeug==0.12.1 -wrapt==1.10.10 -xhtml2pdf==0.0.6 -xmltodict==0.10.2 +tzlocal==1.5.1 +urllib3==1.22 +waitress==1.1.0 +WebOb==1.7.4 +Werkzeug==0.14.1 +wrapt==1.10.11 +xhtml2pdf==0.2.1 +xmltodict==0.11.0 +yapf==0.20.2 diff --git a/util/security/jwtutil.py b/util/security/jwtutil.py index dc190838f..7e2bc6cee 100644 --- a/util/security/jwtutil.py +++ b/util/security/jwtutil.py @@ -1,5 +1,6 @@ import re +from calendar import timegm from datetime import datetime, timedelta from jwt import PyJWT from jwt.exceptions import ( @@ -42,6 +43,9 @@ class StrictJWT(PyJWT): # Do all of the other checks super(StrictJWT, self)._validate_claims(payload, options, audience, issuer, leeway, **kwargs) + now = timegm(datetime.utcnow().utctimetuple()) + self._reject_future_iat(payload, now, leeway) + if 'exp' in payload and options.get('exp_max_s') is not None: # Validate that the expiration was not more than exp_max_s seconds after the issue time # or in the absense of an issue time, more than exp_max_s in the future from now @@ -58,6 +62,16 @@ class StrictJWT(PyJWT): raise InvalidTokenError('Token was signed for more than %s seconds from %s', max_signed_s, start_time) + def _reject_future_iat(self, payload, now, leeway): + try: + iat = int(payload['iat']) + except ValueError: + raise DecodeError('Issued At claim (iat) must be an integer.') + + if iat > (now + leeway): + raise InvalidIssuedAtError('Issued At claim (iat) cannot be in' + ' the future.') + def exp_max_s_option(max_exp_s): return {