Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Macaroon-based API keys #6084

Merged
merged 58 commits into from
Jul 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
10e14b3
Add pymacaroons to our requirements
dstufft Jul 15, 2018
1553101
Add a database model for holding our macaroons
dstufft Jul 16, 2018
ba84539
Authenticate the user using Macaroons
dstufft Jul 16, 2018
2f12075
Add support for actually authorizing the request
dstufft Jul 16, 2018
e3e1fa6
macaroons: Fix typo
woodruffw Jun 24, 2019
48faf29
warehouse: Update macaroon model, migration
woodruffw Jun 24, 2019
acfa52e
warehouse: Fix import order
woodruffw Jun 24, 2019
4923ef2
warehouse: Add create_macaroon
woodruffw Jun 24, 2019
e9a029f
warehouse: Initial API key views, forms
woodruffw Jun 27, 2019
8f3bd83
warehouse: Tweak comment
woodruffw Jun 28, 2019
b927928
warehouse: Add callout, parameters for verification
woodruffw Jun 28, 2019
3a1d4bc
warehouse: Break caveats out, change versioning scheme
woodruffw Jul 2, 2019
9cdec46
warehouse: Functional macarooon validation
woodruffw Jul 2, 2019
851ad3e
warehouse: Stringify UUID for ACL check
woodruffw Jul 2, 2019
d65c343
warehouse: Describe macaroons as "tokens"
woodruffw Jul 2, 2019
76ac351
warehouse: Invert condition, consistent return type
woodruffw Jul 2, 2019
1d8ebc3
warehouse: Per-package tokens
woodruffw Jul 8, 2019
4dbe938
warehouse: Remove unused import
woodruffw Jul 8, 2019
aa5cf14
tests: Fix broken tests
woodruffw Jul 8, 2019
68f8f9e
tests: Add macaroon form tests
woodruffw Jul 8, 2019
5a80f31
tests: Add macaroon views tests
woodruffw Jul 8, 2019
f6601c4
warehouse: Update docstrings
woodruffw Jul 9, 2019
263fe98
tests: Add macaroon auth policy tests
woodruffw Jul 9, 2019
3eaf5a0
warehouse: Consistent underscore
woodruffw Jul 9, 2019
a1087d4
tests: Auth policy helper tests
woodruffw Jul 9, 2019
7b40c49
warehouse: Simplify V1Caveat a bit
woodruffw Jul 9, 2019
9741f0d
tests: More auth policy tests, initial caveats tests
woodruffw Jul 9, 2019
bfd7e3c
tests: Fill out macaroon caveats tests
woodruffw Jul 13, 2019
7493b7f
tests: Add macaroon_service fixture
woodruffw Jul 13, 2019
90013e3
tests: Fill in macaroon models, services tests
woodruffw Jul 13, 2019
ca27ae4
warehouse: Exception-driven macaroon verification
woodruffw Jul 16, 2019
c88a312
tests: Update caveat tests
woodruffw Jul 16, 2019
a6d18d0
warehouse: Require unique descriptions, better 403 message
woodruffw Jul 18, 2019
64fe1b8
tests: Update tests
woodruffw Jul 18, 2019
a729d6c
warehouse: Form errors, improved routing
woodruffw Jul 18, 2019
fc7a829
warehouse: Use HTTPSeeOther for delete_macaroon
woodruffw Jul 18, 2019
d1e1c4c
tests: Fix routes, forms tests
woodruffw Jul 18, 2019
da707d0
tests: Fix service/views tests, fill in coverage
woodruffw Jul 18, 2019
8fd2a2b
warehouse: Re-add identifier to table, dump scope
woodruffw Jul 18, 2019
ac07cf3
warehouse: Remove unused import
woodruffw Jul 18, 2019
33f25c5
warehouse: Macro indentation
woodruffw Jul 18, 2019
e935cd8
warehouse: Update comment
woodruffw Jul 22, 2019
45765b9
Update API token UI
nlhkabu Jul 23, 2019
b5a3dfc
warehouse: Flexible token removal
woodruffw Jul 23, 2019
a4bb86e
warehouse: Include token description in success flash
woodruffw Jul 23, 2019
2d4c28f
tests: Update macaroon services tests
woodruffw Jul 23, 2019
480c4b7
warehouse: Use macaroon object directly for description
woodruffw Jul 23, 2019
44101ae
tests: Update views tests
woodruffw Jul 23, 2019
2ede760
warehouse, tests: two_factor_provisioning_allowed -> has_primary_veri…
woodruffw Jul 23, 2019
5cef6aa
tests: Auto-format
woodruffw Jul 23, 2019
5208b65
warehouse: Guard token creation with a verified email
woodruffw Jul 23, 2019
271600b
tests: Update views tests
woodruffw Jul 23, 2019
e8ee987
warehouse: confirm_modal formatting
woodruffw Jul 23, 2019
42cbcc7
Add API token creation instructions to FAQ
brainwane Jul 24, 2019
5127d9b
warehouse: Flatten confirm_modal call
woodruffw Jul 24, 2019
8ac5045
warehouse: Remove dangling paren
woodruffw Jul 24, 2019
4b90eb7
warehouse: Fix sass-lint error
woodruffw Jul 24, 2019
8f7595a
fix linting issue for _api-token.scss
ewdurbin Jul 25, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions requirements/main.in
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ psycopg2
pycurl
pyqrcode
pyramid>=1.9,<1.11.0
pymacaroons
pyramid_jinja2>=2.5
pyramid_mailer>=0.14.1
pyramid_multiauth
Expand Down
35 changes: 35 additions & 0 deletions requirements/main.txt
Original file line number Diff line number Diff line change
Expand Up @@ -420,6 +420,41 @@ pycurl==7.43.0.3 \
pygments==2.4.2 \
--hash=sha256:71e430bc85c88a430f000ac1d9b331d2407f681d6f6aec95e8bcfbc3df5b0127 \
--hash=sha256:881c4c157e45f30af185c1ffe8d549d48ac9127433f2c380c24b84572ad66297
pymacaroons==0.13.0 \
--hash=sha256:1e6bba42a5f66c245adf38a5a4006a99dcc06a0703786ea636098667d42903b8 \
--hash=sha256:3e14dff6a262fdbf1a15e769ce635a8aea72e6f8f91e408f9a97166c53b91907
PyNaCl==1.2.1 \
--hash=sha256:04e30e5bdeeb2d5b34107f28cd2f5bbfdc6c616f3be88fc6f53582ff1669eeca \
--hash=sha256:0bfa0d94d2be6874e40f896e0a67e290749151e7de767c5aefbad1121cad7512 \
--hash=sha256:11aa4e141b2456ce5cecc19c130e970793fa3a2c2e6fbb8ad65b28f35aa9e6b6 \
--hash=sha256:13bdc1fe084ff9ac7653ae5a924cae03bf4bb07c6667c9eb5b6eb3c570220776 \
--hash=sha256:14339dc233e7a9dda80a3800e64e7ff89d0878ba23360eea24f1af1b13772cac \
--hash=sha256:1d33e775fab3f383167afb20b9927aaf4961b953d76eeb271a5703a6d756b65b \
--hash=sha256:2a42b2399d0428619e58dac7734838102d35f6dcdee149e0088823629bf99fbb \
--hash=sha256:2dce05ac8b3c37b9e2f65eab56c544885607394753e9613fd159d5e2045c2d98 \
--hash=sha256:63cfccdc6217edcaa48369191ae4dca0c390af3c74f23c619e954973035948cd \
--hash=sha256:6453b0dae593163ffc6db6f9c9c1597d35c650598e2c39c0590d1757207a1ac2 \
--hash=sha256:73a5a96fb5fbf2215beee2353a128d382dbca83f5341f0d3c750877a236569ef \
--hash=sha256:8abb4ef79161a5f58848b30ab6fb98d8c466da21fdd65558ce1d7afc02c70b5f \
--hash=sha256:8ac1167195b32a8755de06efd5b2d2fe76fc864517dab66aaf65662cc59e1988 \
--hash=sha256:8f505f42f659012794414fa57c498404e64db78f1d98dfd40e318c569f3c783b \
--hash=sha256:9c8a06556918ee8e3ab48c65574f318f5a0a4d31437fc135da7ee9d4f9080415 \
--hash=sha256:a1e25fc5650cf64f01c9e435033e53a4aca9de30eb9929d099f3bb078e18f8f2 \
--hash=sha256:be71cd5fce04061e1f3d39597f93619c80cdd3558a6c9ba99a546f144a8d8101 \
--hash=sha256:c5b1a7a680218dee9da0f1b5e24072c46b3c275d35712bc1d505b85bb03441c0 \
--hash=sha256:cb785db1a9468841a1265c9215c60fe5d7af2fb1b209e3316a152704607fc582 \
--hash=sha256:cf6877124ae6a0698404e169b3ba534542cfbc43f939d46b927d956daf0a373a \
--hash=sha256:d0eb5b2795b7ee2cbcfcadacbe95a13afbda048a262bd369da9904fecb568975 \
--hash=sha256:d3a934e2b9f20abac009d5b6951067cfb5486889cb913192b4d8288b216842f1 \
--hash=sha256:d795f506bcc9463efb5ebb0f65ed77921dcc9e0a50499dedd89f208445de9ecb \
--hash=sha256:d8aaf7e5d6b0e0ef7d6dbf7abeb75085713d0100b4eb1a4e4e857de76d77ac45 \
--hash=sha256:de2aaca8386cf4d70f1796352f2346f48ddb0bed61dc43a3ce773ba12e064031 \
--hash=sha256:e0d38fa0a75f65f556fb912f2c6790d1fa29b7dd27a1d9cc5591b281321eaaa9 \
--hash=sha256:eb2acabbd487a46b38540a819ef67e477a674481f84a82a7ba2234b9ba46f752 \
--hash=sha256:eeee629828d0eb4f6d98ac41e9a3a6461d114d1d0aa111a8931c049359298da0 \
--hash=sha256:f5836463a3c0cca300295b229b6c7003c415a9d11f8f9288ddbd728e2746524c \
--hash=sha256:f5ce9e26d25eb0b2d96f3ef0ad70e1d3ae89b5d60255c462252a3e456a48c053 \
--hash=sha256:fabf73d5d0286f9e078774f3435601d2735c94ce9e514ac4fb945701edead7e4
pyopenssl==19.0.0 \
--hash=sha256:aeca66338f6de19d1aa46ed634c3b9ae519a64b458f8468aec688e7e3c20f200 \
--hash=sha256:c727930ad54b10fc157015014b666f2d8b41f70c0d03e83ab67624fd3dd5d1e6
Expand Down
12 changes: 9 additions & 3 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@
from sqlalchemy import event

from warehouse import admin, config, static
from warehouse.accounts import services
from warehouse.accounts import services as account_services
from warehouse.macaroons import services as macaroon_services
from warehouse.metrics import IMetricsService

from .common.db import Session
Expand Down Expand Up @@ -216,12 +217,17 @@ def restart_savepoint(session, transaction):

@pytest.yield_fixture
def user_service(db_session, metrics):
return services.DatabaseUserService(db_session, metrics=metrics)
return account_services.DatabaseUserService(db_session, metrics=metrics)


@pytest.yield_fixture
def macaroon_service(db_session):
return macaroon_services.DatabaseMacaroonService(db_session)


@pytest.yield_fixture
def token_service(app_config):
return services.TokenService(secret="secret", salt="salt", max_age=21600)
return account_services.TokenService(secret="secret", salt="salt", max_age=21600)


class QueryRecorder:
Expand Down
12 changes: 9 additions & 3 deletions tests/unit/accounts/test_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,18 +261,22 @@ def test_without_userid(self):


def test_includeme(monkeypatch):
macaroon_authn_obj = pretend.stub()
macaroon_authn_cls = pretend.call_recorder(lambda callback: macaroon_authn_obj)
basic_authn_obj = pretend.stub()
basic_authn_cls = pretend.call_recorder(lambda check: basic_authn_obj)
session_authn_obj = pretend.stub()
session_authn_cls = pretend.call_recorder(lambda callback: session_authn_obj)
authn_obj = pretend.stub()
authn_cls = pretend.call_recorder(lambda *a: authn_obj)
authz_obj = pretend.stub()
authz_cls = pretend.call_recorder(lambda: authz_obj)
authz_cls = pretend.call_recorder(lambda *a, **kw: authz_obj)
monkeypatch.setattr(accounts, "BasicAuthAuthenticationPolicy", basic_authn_cls)
monkeypatch.setattr(accounts, "SessionAuthenticationPolicy", session_authn_cls)
monkeypatch.setattr(accounts, "MacaroonAuthenticationPolicy", macaroon_authn_cls)
monkeypatch.setattr(accounts, "MultiAuthenticationPolicy", authn_cls)
monkeypatch.setattr(accounts, "ACLAuthorizationPolicy", authz_cls)
monkeypatch.setattr(accounts, "MacaroonAuthorizationPolicy", authz_cls)

config = pretend.stub(
registry=pretend.stub(settings={}),
Expand Down Expand Up @@ -312,5 +316,7 @@ def test_includeme(monkeypatch):
assert config.set_authorization_policy.calls == [pretend.call(authz_obj)]
assert basic_authn_cls.calls == [pretend.call(check=accounts._basic_auth_login)]
assert session_authn_cls.calls == [pretend.call(callback=accounts._authenticate)]
assert authn_cls.calls == [pretend.call([session_authn_obj, basic_authn_obj])]
assert authz_cls.calls == [pretend.call()]
assert authn_cls.calls == [
pretend.call([session_authn_obj, basic_authn_obj, macaroon_authn_obj])
]
assert authz_cls.calls == [pretend.call(), pretend.call(policy=authz_obj)]
6 changes: 2 additions & 4 deletions tests/unit/accounts/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,17 +45,15 @@ def test_travel_cant_find(self, db_request):
("foo@bar.com", False, False),
],
)
def test_two_factor_provisioning_allowed(
self, db_session, email, verified, allowed
):
def test_has_primary_verified_email(self, db_session, email, verified, allowed):
user = DBUserFactory.create()

if email:
e = Email(email=email, user=user, primary=True, verified=verified)
db_session.add(e)
db_session.flush()

assert user.two_factor_provisioning_allowed == allowed
assert user.has_primary_verified_email == allowed


class TestUser:
Expand Down
3 changes: 2 additions & 1 deletion tests/unit/forklift/test_legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -2212,7 +2212,8 @@ def test_upload_fails_without_permission(self, pyramid_config, db_request):
assert db_request.help_url.calls == [pretend.call(_anchor="project-name")]
assert resp.status_code == 403
assert resp.status == (
"403 The user '{0}' isn't allowed to upload to project '{1}'. "
"403 The credential associated with user '{0}' "
"isn't allowed to upload to project '{1}'. "
"See /the/help/url/ for more information."
).format(user2.username, project.name)

Expand Down
11 changes: 11 additions & 0 deletions tests/unit/macaroons/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
219 changes: 219 additions & 0 deletions tests/unit/macaroons/test_auth_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import uuid

import pretend
import pytest

from pyramid.interfaces import IAuthenticationPolicy, IAuthorizationPolicy
from pyramid.security import Denied
from zope.interface.verify import verifyClass

from warehouse.macaroons import auth_policy
from warehouse.macaroons.interfaces import IMacaroonService
from warehouse.macaroons.services import InvalidMacaroon


@pytest.mark.parametrize(
["auth", "result"],
[
(None, None),
("notarealtoken", None),
("maybeafuturemethod foobar", None),
("token foobar", "foobar"),
("basic QHRva2VuOmZvb2Jhcg==", "foobar"), # "@token:foobar"
],
)
def test_extract_http_macaroon(auth, result):
request = pretend.stub(
headers=pretend.stub(get=pretend.call_recorder(lambda k: auth))
)

assert auth_policy._extract_http_macaroon(request) == result


@pytest.mark.parametrize(
["auth", "result"],
[
("notbase64", None),
("bm90YXJlYWx0b2tlbg==", None), # "notarealtoken"
("QGJhZHVzZXI6Zm9vYmFy", None), # "@baduser:foobar"
("QHRva2VuOmZvb2Jhcg==", "foobar"), # "@token:foobar"
],
)
def test_extract_basic_macaroon(auth, result):
assert auth_policy._extract_basic_macaroon(auth) == result


class TestMacaroonAuthenticationPolicy:
def test_verify(self):
assert verifyClass(
IAuthenticationPolicy, auth_policy.MacaroonAuthenticationPolicy
)

def test_unauthenticated_userid_invalid_macaroon(self, monkeypatch):
_extract_http_macaroon = pretend.call_recorder(lambda r: None)
monkeypatch.setattr(
auth_policy, "_extract_http_macaroon", _extract_http_macaroon
)

policy = auth_policy.MacaroonAuthenticationPolicy()

vary_cb = pretend.stub()
add_vary_cb = pretend.call_recorder(lambda *v: vary_cb)
monkeypatch.setattr(auth_policy, "add_vary_callback", add_vary_cb)

request = pretend.stub(
add_response_callback=pretend.call_recorder(lambda cb: None)
)

assert policy.unauthenticated_userid(request) is None
assert _extract_http_macaroon.calls == [pretend.call(request)]
assert add_vary_cb.calls == [pretend.call("Authorization")]
assert request.add_response_callback.calls == [pretend.call(vary_cb)]

def test_unauthenticated_userid_valid_macaroon(self, monkeypatch):
_extract_http_macaroon = pretend.call_recorder(lambda r: b"not a real macaroon")
monkeypatch.setattr(
auth_policy, "_extract_http_macaroon", _extract_http_macaroon
)

policy = auth_policy.MacaroonAuthenticationPolicy()

vary_cb = pretend.stub()
add_vary_cb = pretend.call_recorder(lambda *v: vary_cb)
monkeypatch.setattr(auth_policy, "add_vary_callback", add_vary_cb)

userid = uuid.uuid4()
user_service = pretend.stub(
find_userid=pretend.call_recorder(lambda username: userid)
)
request = pretend.stub(
find_service=pretend.call_recorder(lambda interface, **kw: user_service),
add_response_callback=pretend.call_recorder(lambda cb: None),
)

assert policy.unauthenticated_userid(request) == str(userid)
assert _extract_http_macaroon.calls == [pretend.call(request)]
assert request.find_service.calls == [
pretend.call(IMacaroonService, context=None)
]
assert user_service.find_userid.calls == [pretend.call(b"not a real macaroon")]
assert add_vary_cb.calls == [pretend.call("Authorization")]
assert request.add_response_callback.calls == [pretend.call(vary_cb)]

def test_remember(self):
policy = auth_policy.MacaroonAuthenticationPolicy()
assert policy.remember(pretend.stub(), pretend.stub()) == []

def test_forget(self):
policy = auth_policy.MacaroonAuthenticationPolicy()
assert policy.forget(pretend.stub()) == []


class TestMacaroonAuthorizationPolicy:
def test_verify(self):
assert verifyClass(
IAuthorizationPolicy, auth_policy.MacaroonAuthorizationPolicy
)

def test_permits_no_active_request(self, monkeypatch):
get_current_request = pretend.call_recorder(lambda: None)
monkeypatch.setattr(auth_policy, "get_current_request", get_current_request)

backing_policy = pretend.stub(
permits=pretend.call_recorder(lambda *a, **kw: pretend.stub())
)
policy = auth_policy.MacaroonAuthorizationPolicy(policy=backing_policy)
result = policy.permits(pretend.stub(), pretend.stub(), pretend.stub())

assert result == Denied("There was no active request.")

def test_permits_no_macaroon(self, monkeypatch):
request = pretend.stub()
get_current_request = pretend.call_recorder(lambda: request)
monkeypatch.setattr(auth_policy, "get_current_request", get_current_request)

_extract_http_macaroon = pretend.call_recorder(lambda r: None)
monkeypatch.setattr(
auth_policy, "_extract_http_macaroon", _extract_http_macaroon
)

permits = pretend.stub()
backing_policy = pretend.stub(
permits=pretend.call_recorder(lambda *a, **kw: permits)
)
policy = auth_policy.MacaroonAuthorizationPolicy(policy=backing_policy)
result = policy.permits(pretend.stub(), pretend.stub(), pretend.stub())

assert result == permits

def test_permits_invalid_macaroon(self, monkeypatch):
macaroon_service = pretend.stub(verify=pretend.raiser(InvalidMacaroon("foo")))
request = pretend.stub(
find_service=pretend.call_recorder(lambda interface, **kw: macaroon_service)
)
get_current_request = pretend.call_recorder(lambda: request)
monkeypatch.setattr(auth_policy, "get_current_request", get_current_request)

_extract_http_macaroon = pretend.call_recorder(lambda r: b"not a real macaroon")
monkeypatch.setattr(
auth_policy, "_extract_http_macaroon", _extract_http_macaroon
)

permits = pretend.stub()
backing_policy = pretend.stub(
permits=pretend.call_recorder(lambda *a, **kw: permits)
)
policy = auth_policy.MacaroonAuthorizationPolicy(policy=backing_policy)
result = policy.permits(pretend.stub(), pretend.stub(), pretend.stub())

assert result == Denied("The supplied token was invalid: foo")

def test_permits_valid_macaroon(self, monkeypatch):
macaroon_service = pretend.stub(
verify=pretend.call_recorder(lambda *a: pretend.stub())
)
request = pretend.stub(
find_service=pretend.call_recorder(lambda interface, **kw: macaroon_service)
)
get_current_request = pretend.call_recorder(lambda: request)
monkeypatch.setattr(auth_policy, "get_current_request", get_current_request)

_extract_http_macaroon = pretend.call_recorder(lambda r: b"not a real macaroon")
monkeypatch.setattr(
auth_policy, "_extract_http_macaroon", _extract_http_macaroon
)

permits = pretend.stub()
backing_policy = pretend.stub(
permits=pretend.call_recorder(lambda *a, **kw: permits)
)
policy = auth_policy.MacaroonAuthorizationPolicy(policy=backing_policy)
result = policy.permits(pretend.stub(), pretend.stub(), pretend.stub())

assert result == permits

def test_principals_allowed_by_permission(self):
principals = pretend.stub()
backing_policy = pretend.stub(
principals_allowed_by_permission=pretend.call_recorder(
lambda *a: principals
)
)
policy = auth_policy.MacaroonAuthorizationPolicy(policy=backing_policy)

assert (
policy.principals_allowed_by_permission(pretend.stub(), pretend.stub())
is principals
)
Loading