|
14 | 14 | import uuid
|
15 | 15 |
|
16 | 16 | from pymacaroons import Macaroon
|
17 |
| -from pyramid import authentication |
18 |
| -from pyramid.interfaces import IAuthenticationPolicy |
| 17 | +from pyramid import authentication, security |
| 18 | +from pyramid.interfaces import IAuthenticationPolicy, IAuthorizationPolicy |
19 | 19 | from zope.interface.verify import verifyClass
|
20 | 20 |
|
21 | 21 | from warehouse.accounts import auth_policy
|
22 |
| -from warehouse.accounts.interfaces import IUserService |
| 22 | +from warehouse.accounts.interfaces import IUserService, IAccountTokenService |
23 | 23 |
|
24 | 24 | from ...common.db.accounts import AccountTokenFactory, UserFactory
|
25 | 25 |
|
@@ -112,174 +112,223 @@ def test_unauthenticated_userid(self, monkeypatch):
|
112 | 112 |
|
113 | 113 | class TestAccountTokenAuthenticationPolicy:
|
114 | 114 | def test_verify(self):
|
115 |
| - assert isinstance( |
116 |
| - auth_policy.AccountTokenAuthenticationPolicy(pretend.stub()), |
117 |
| - authentication.CallbackAuthenticationPolicy, |
| 115 | + assert verifyClass( |
| 116 | + IAuthenticationPolicy, auth_policy.AccountTokenAuthenticationPolicy |
118 | 117 | )
|
119 | 118 |
|
120 |
| - def test_account_token_routes_allowed(self): |
121 |
| - request = pretend.stub(matched_route=pretend.stub(name="not_a_real_route")) |
| 119 | + def test_routes_not_allowed(self): |
| 120 | + request = pretend.stub(matched_route=pretend.stub(name="not_allowed_route")) |
122 | 121 |
|
123 |
| - policy = auth_policy.AccountTokenAuthenticationPolicy(pretend.stub()) |
124 |
| - assert policy.unauthenticated_userid(request) is None |
| 122 | + authn_policy = auth_policy.AccountTokenAuthenticationPolicy( |
| 123 | + pretend.stub(), ["allowed_route"] |
| 124 | + ) |
| 125 | + |
| 126 | + assert authn_policy.unauthenticated_userid(request) is None |
125 | 127 |
|
126 |
| - def test_account_token_required_parameter(self): |
| 128 | + def test_require_known(self): |
| 129 | + # Ensure we don't accept just any macaroon |
127 | 130 | request = pretend.stub(
|
128 |
| - matched_route=pretend.stub(name="forklift.legacy.file_upload"), params={} |
| 131 | + find_service=lambda iface, **kw: { |
| 132 | + IAccountTokenService: pretend.stub( |
| 133 | + get_unverified_macaroon=(lambda: (None, None)) |
| 134 | + ) |
| 135 | + }[iface], |
| 136 | + matched_route=pretend.stub(name="allowed_route"), |
| 137 | + add_response_callback=lambda x: None, |
129 | 138 | )
|
130 | 139 |
|
131 |
| - policy = auth_policy.AccountTokenAuthenticationPolicy(pretend.stub()) |
132 |
| - assert policy.unauthenticated_userid(request) is None |
| 140 | + authn_policy = auth_policy.AccountTokenAuthenticationPolicy( |
| 141 | + pretend.stub(), ["allowed_route"] |
| 142 | + ) |
| 143 | + |
| 144 | + assert authn_policy.unauthenticated_userid(request) is None |
| 145 | + |
| 146 | + def test_macaroon_verifier(self, db_request): |
| 147 | + user = UserFactory.create(username="test_user") |
| 148 | + |
| 149 | + account_token = AccountTokenFactory.create( |
| 150 | + secret="some_secret", username=user.username |
| 151 | + ) |
| 152 | + |
| 153 | + macaroon = Macaroon( |
| 154 | + location="pypi.org", identifier="some_id", key="wrong_secret" |
| 155 | + ) |
133 | 156 |
|
134 |
| - def test_account_token_malformed(self): |
135 | 157 | request = pretend.stub(
|
136 |
| - matched_route=pretend.stub(name="forklift.legacy.file_upload"), |
137 |
| - params={"account_token": "DEADBEEF"}, |
| 158 | + find_service=lambda iface, **kw: { |
| 159 | + IAccountTokenService: pretend.stub( |
| 160 | + get_unverified_macaroon=(lambda: (macaroon, account_token)) |
| 161 | + ) |
| 162 | + }[iface], |
| 163 | + matched_route=pretend.stub(name="allowed_route"), |
| 164 | + add_response_callback=lambda x: None, |
138 | 165 | )
|
139 | 166 |
|
140 |
| - policy = auth_policy.AccountTokenAuthenticationPolicy(pretend.stub()) |
141 |
| - assert policy.unauthenticated_userid(request) is None |
| 167 | + authn_policy = auth_policy.AccountTokenAuthenticationPolicy( |
| 168 | + pretend.stub(), ["allowed_route"] |
| 169 | + ) |
| 170 | + |
| 171 | + assert authn_policy.unauthenticated_userid(request) is None |
| 172 | + |
| 173 | + def test_account_token_auth(self, db_request): |
| 174 | + # Test basic happy path |
| 175 | + user = UserFactory.create(username="test_user") |
| 176 | + account_token = AccountTokenFactory.create( |
| 177 | + secret="some_secret", username=user.username |
| 178 | + ) |
142 | 179 |
|
143 |
| - def test_account_token_bad_settings(self): |
144 |
| - # Test bad location |
145 | 180 | macaroon = Macaroon(
|
146 |
| - location="notpypi.org", identifier="example_id", key="example_secret" |
| 181 | + location="pypi.org", identifier=str(account_token.id), key="some_secret" |
147 | 182 | )
|
148 | 183 |
|
149 | 184 | request = pretend.stub(
|
150 |
| - matched_route=pretend.stub(name="forklift.legacy.file_upload"), |
151 |
| - params={"account_token": macaroon.serialize()}, |
152 |
| - registry=pretend.stub( |
153 |
| - settings={ |
154 |
| - "account_token.id": "example_id", |
155 |
| - "account_token.secret": "example_secret", |
156 |
| - } |
157 |
| - ), |
| 185 | + find_service=lambda iface, **kw: { |
| 186 | + IUserService: pretend.stub( |
| 187 | + find_userid=(lambda x: user.id if x == "test_user" else None) |
| 188 | + ), |
| 189 | + IAccountTokenService: pretend.stub( |
| 190 | + get_unverified_macaroon=(lambda: (macaroon, account_token)), |
| 191 | + update_last_used=(lambda x: None), |
| 192 | + ), |
| 193 | + }[iface], |
| 194 | + matched_route=pretend.stub(name="allowed_route"), |
| 195 | + add_response_callback=lambda x: None, |
| 196 | + session={}, |
158 | 197 | )
|
159 | 198 |
|
160 |
| - policy = auth_policy.AccountTokenAuthenticationPolicy(pretend.stub()) |
161 |
| - assert policy.unauthenticated_userid(request) is None |
| 199 | + authn_policy = auth_policy.AccountTokenAuthenticationPolicy( |
| 200 | + pretend.stub(), ["allowed_route"] |
| 201 | + ) |
162 | 202 |
|
163 |
| - # Test bad identifier |
164 |
| - macaroon = Macaroon( |
165 |
| - location="pypi.org", identifier="bad_id", key="example_secret" |
| 203 | + assert authn_policy.unauthenticated_userid(request) == user.id |
| 204 | + |
| 205 | + # Make sure we allow first-party and third-party caveats |
| 206 | + macaroon.add_first_party_caveat("first party caveat") |
| 207 | + |
| 208 | + macaroon.add_third_party_caveat( |
| 209 | + location="mysite.com", key="anykey", key_id="anykeyid" |
166 | 210 | )
|
167 | 211 |
|
168 |
| - request.params["account_token"] = macaroon.serialize() |
169 |
| - policy = auth_policy.AccountTokenAuthenticationPolicy(pretend.stub()) |
170 |
| - assert policy.unauthenticated_userid(request) is None |
| 212 | + assert authn_policy.unauthenticated_userid(request) == user.id |
171 | 213 |
|
172 |
| - # Tamper with macaroon |
173 |
| - macaroon = Macaroon( |
174 |
| - location="pypi.org", identifier="example_id", key="example_secret" |
| 214 | + def test_account_token_interface(self): |
| 215 | + def _authenticate(a, b): |
| 216 | + return a, b |
| 217 | + |
| 218 | + policy = auth_policy.AccountTokenAuthenticationPolicy(_authenticate, ["route"]) |
| 219 | + |
| 220 | + assert policy.remember("", "") == [] |
| 221 | + assert policy.forget("") == [] |
| 222 | + assert policy._auth_callback(1, 2) == (1, 2) |
| 223 | + assert policy._routes_allowed == ["route"] |
| 224 | + |
| 225 | + |
| 226 | +class TestAccountTokenAuthorizationPolicy: |
| 227 | + def test_verify(self): |
| 228 | + assert verifyClass( |
| 229 | + IAuthorizationPolicy, auth_policy.AccountTokenAuthorizationPolicy |
175 | 230 | )
|
176 | 231 |
|
177 |
| - serialized = macaroon.serialize() |
| 232 | + def test_have_request(self, monkeypatch): |
| 233 | + monkeypatch.setattr(auth_policy, "get_current_request", lambda: None) |
| 234 | + authz_policy = auth_policy.AccountTokenAuthorizationPolicy(pretend.stub()) |
178 | 235 |
|
179 |
| - request.params["account_token"] = "".join( |
180 |
| - (serialized[:-8], "AAAAAAA", serialized[-1:]) |
| 236 | + assert isinstance( |
| 237 | + authz_policy.permits(pretend.stub(), pretend.stub(), pretend.stub()), |
| 238 | + security.Denied, |
| 239 | + ) |
| 240 | + |
| 241 | + def test_macaroon_verifier(self, db_request, monkeypatch): |
| 242 | + user = UserFactory.create(username="test_user") |
| 243 | + |
| 244 | + account_token = AccountTokenFactory.create( |
| 245 | + secret="some_secret", username=user.username |
181 | 246 | )
|
182 |
| - assert policy.unauthenticated_userid(request) is None |
183 | 247 |
|
184 |
| - def test_account_token_with_no_user(self, db_request): |
185 | 248 | macaroon = Macaroon(
|
186 |
| - location="pypi.org", identifier="example_id", key="example_secret" |
| 249 | + location="pypi.org", identifier="some_id", key="wrong_secret" |
187 | 250 | )
|
188 | 251 |
|
| 252 | + monkeypatch.setattr(auth_policy, "get_current_request", lambda: request) |
189 | 253 | request = pretend.stub(
|
190 | 254 | find_service=lambda iface, **kw: {
|
191 |
| - IUserService: pretend.stub(find_userid_by_account_token=pretend.stub()) |
192 |
| - }[iface], |
193 |
| - params={"account_token": macaroon.serialize()}, |
194 |
| - registry=pretend.stub( |
195 |
| - settings={ |
196 |
| - "account_token.id": "example_id", |
197 |
| - "account_token.secret": "example_secret", |
198 |
| - } |
199 |
| - ), |
200 |
| - matched_route=pretend.stub(name="forklift.legacy.file_upload"), |
201 |
| - db=db_request, |
202 |
| - session={}, |
| 255 | + IAccountTokenService: pretend.stub( |
| 256 | + get_unverified_macaroon=(lambda: (macaroon, account_token)) |
| 257 | + ) |
| 258 | + }[iface] |
203 | 259 | )
|
204 | 260 |
|
205 |
| - policy = auth_policy.AccountTokenAuthenticationPolicy(pretend.stub()) |
206 |
| - assert policy.unauthenticated_userid(request) is None |
| 261 | + authz_policy = auth_policy.AccountTokenAuthorizationPolicy(pretend.stub()) |
207 | 262 |
|
208 |
| - def test_account_token_auth(self, db_request): |
209 |
| - # Test basic happy path |
| 263 | + assert isinstance( |
| 264 | + authz_policy.permits(pretend.stub(), pretend.stub(), pretend.stub()), |
| 265 | + security.Denied, |
| 266 | + ) |
| 267 | + |
| 268 | + def test_account_token_authz(self, db_request, monkeypatch): |
210 | 269 | user = UserFactory.create(username="test_user")
|
211 |
| - account_token = AccountTokenFactory.create(username=user.username) |
212 |
| - account_token_id = str(account_token.id) |
213 | 270 |
|
214 |
| - macaroon = Macaroon( |
215 |
| - location="pypi.org", identifier="example_id", key="example_secret" |
| 271 | + account_token = AccountTokenFactory.create( |
| 272 | + secret="some_secret", username=user.username |
216 | 273 | )
|
217 | 274 |
|
218 |
| - macaroon.add_first_party_caveat(f"id: {account_token_id}") |
| 275 | + macaroon = Macaroon( |
| 276 | + location="pypi.org", identifier="some_id", key="some_secret" |
| 277 | + ) |
219 | 278 |
|
| 279 | + monkeypatch.setattr(auth_policy, "get_current_request", lambda: request) |
220 | 280 | request = pretend.stub(
|
221 | 281 | find_service=lambda iface, **kw: {
|
222 |
| - IUserService: pretend.stub( |
223 |
| - find_userid_by_account_token=( |
224 |
| - lambda x: user.id if x == account_token_id else None |
225 |
| - ) |
| 282 | + IAccountTokenService: pretend.stub( |
| 283 | + get_unverified_macaroon=(lambda: (macaroon, account_token)) |
226 | 284 | )
|
227 |
| - }[iface], |
228 |
| - params={"account_token": macaroon.serialize()}, |
229 |
| - registry=pretend.stub( |
230 |
| - settings={ |
231 |
| - "account_token.id": "example_id", |
232 |
| - "account_token.secret": "example_secret", |
233 |
| - } |
234 |
| - ), |
235 |
| - matched_route=pretend.stub(name="forklift.legacy.file_upload"), |
236 |
| - db=db_request, |
237 |
| - session={}, |
| 285 | + }[iface] |
238 | 286 | )
|
239 | 287 |
|
240 |
| - policy = auth_policy.AccountTokenAuthenticationPolicy(pretend.stub()) |
241 |
| - assert policy.unauthenticated_userid(request) == user.id |
| 288 | + authz_policy = auth_policy.AccountTokenAuthorizationPolicy( |
| 289 | + pretend.stub(permits=(lambda *args, **kwargs: "allow")) |
| 290 | + ) |
242 | 291 |
|
243 |
| - # Test package_list caveats |
244 |
| - macaroon.add_first_party_caveat("package_list: pyexample1 pyexample2") |
245 |
| - macaroon.add_third_party_caveat( |
246 |
| - location="mysite.com", key="anykey", key_id="anykeyid" |
| 292 | + assert ( |
| 293 | + authz_policy.permits(pretend.stub(), pretend.stub(), pretend.stub()) |
| 294 | + == "allow" |
247 | 295 | )
|
248 |
| - request.params["account_token"] = macaroon.serialize() |
249 |
| - request.session["account_token_package_list"] = None |
250 | 296 |
|
251 |
| - assert policy.unauthenticated_userid(request) == user.id |
252 |
| - assert request.session["account_token_package_list"] == "pyexample1 pyexample2" |
| 297 | + # Make sure we allow first-party and third-party caveats |
| 298 | + macaroon.add_first_party_caveat("first party caveat") |
253 | 299 |
|
254 |
| - # Ensure you can't overwrite previous caveats |
255 |
| - takeover_user = UserFactory.create(username="takeover_user") |
256 |
| - takeover_account_token = AccountTokenFactory.create( |
257 |
| - username=takeover_user.username |
| 300 | + macaroon.add_third_party_caveat( |
| 301 | + location="mysite.com", key="anykey", key_id="anykeyid" |
258 | 302 | )
|
259 |
| - takeover_account_token_id = str(takeover_account_token.id) |
260 |
| - |
261 |
| - macaroon.add_first_party_caveat(f"id: {takeover_account_token_id}") |
262 |
| - macaroon.add_first_party_caveat("package_list: additionalpackage") |
263 | 303 |
|
264 |
| - request.params["account_token"] = macaroon.serialize() |
265 |
| - request.session["account_token_package_list"] = None |
| 304 | + assert ( |
| 305 | + authz_policy.permits(pretend.stub(), pretend.stub(), pretend.stub()) |
| 306 | + == "allow" |
| 307 | + ) |
266 | 308 |
|
267 |
| - assert policy.unauthenticated_userid(request) == user.id |
268 |
| - assert request.session["account_token_package_list"] == "pyexample1 pyexample2" |
| 309 | + def test_missing_macaroon(self, monkeypatch): |
| 310 | + monkeypatch.setattr(auth_policy, "get_current_request", lambda: request) |
269 | 311 |
|
270 |
| - def test_first_party_caveat_validation(self): |
271 |
| - policy = auth_policy.AccountTokenAuthenticationPolicy(pretend.stub()) |
| 312 | + request = pretend.stub( |
| 313 | + find_service=lambda iface, **kw: { |
| 314 | + IAccountTokenService: pretend.stub( |
| 315 | + get_unverified_macaroon=(lambda: (None, None)) |
| 316 | + ) |
| 317 | + }[iface] |
| 318 | + ) |
272 | 319 |
|
273 |
| - assert policy._validate_first_party_caveat("id") |
274 |
| - assert policy._validate_first_party_caveat("package_list") |
275 |
| - assert not policy._validate_first_party_caveat("not_valid") |
| 320 | + authz_policy = auth_policy.AccountTokenAuthorizationPolicy( |
| 321 | + pretend.stub(permits=(lambda *args, **kwargs: "allow")) |
| 322 | + ) |
276 | 323 |
|
277 |
| - def test_account_token_interface(self): |
278 |
| - def _authenticate(a, b): |
279 |
| - return a, b |
| 324 | + assert ( |
| 325 | + authz_policy.permits(pretend.stub(), pretend.stub(), pretend.stub()) |
| 326 | + == "allow" |
| 327 | + ) |
280 | 328 |
|
281 |
| - policy = auth_policy.AccountTokenAuthenticationPolicy(_authenticate) |
| 329 | + def test_principals_allowed_by_permission(self): |
| 330 | + authz_policy = auth_policy.AccountTokenAuthorizationPolicy( |
| 331 | + pretend.stub(principals_allowed_by_permission=(lambda a, b: (a, b))) |
| 332 | + ) |
282 | 333 |
|
283 |
| - assert policy.remember("", "") == [] |
284 |
| - assert policy.forget("") == [] |
285 |
| - assert policy._auth_callback(1, 2) == (1, 2) |
| 334 | + assert authz_policy.principals_allowed_by_permission(1, 2) == (1, 2) |
0 commit comments