Merge ~cjwatson/launchpad:git-delete-expired-access-tokens into launchpad:master

Proposed by Colin Watson
Status: Merged
Approved by: Colin Watson
Approved revision: f26c2e6b2eba79c17fa28bf738aa44993b375c1f
Merge reported by: Otto Co-Pilot
Merged at revision: not available
Proposed branch: ~cjwatson/launchpad:git-delete-expired-access-tokens
Merge into: launchpad:master
Diff against target: 173 lines (+65/-12)
5 files modified
lib/lp/code/model/gitrepository.py (+3/-1)
lib/lp/code/model/tests/test_gitrepository.py (+10/-0)
lib/lp/services/auth/interfaces.py (+13/-3)
lib/lp/services/auth/model.py (+13/-8)
lib/lp/services/auth/tests/test_model.py (+26/-0)
Reviewer Review Type Date Requested Status
Guruprasad Approve
Review via email: mp+436489@code.launchpad.net

Commit message

Delete expired access tokens when deleting a Git repository

Description of the change

Attempting to delete a repository with an expired access token failed with:

  update or delete on table "gitrepository" violates foreign key constraint "accesstoken_git_repository_fkey" on table "accesstoken"

Delete expired access tokens as well as valid ones.

To post a comment you must log in.
Revision history for this message
Guruprasad (lgp171188) wrote :

LGTM 👍

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/lib/lp/code/model/gitrepository.py b/lib/lp/code/model/gitrepository.py
2index 902f87c..f78c20c 100644
3--- a/lib/lp/code/model/gitrepository.py
4+++ b/lib/lp/code/model/gitrepository.py
5@@ -2121,7 +2121,9 @@ class GitRepository(
6 # activity logs for removed repositories anyway.
7 self.grants.remove()
8 self.rules.remove()
9- removeSecurityProxy(self.getAccessTokens()).remove()
10+ removeSecurityProxy(
11+ self.getAccessTokens(include_expired=True)
12+ ).remove()
13 getUtility(IRevisionStatusReportSet).deleteForRepository(self)
14 getUtility(ICIBuildSet).deleteByGitRepository(self)
15
16diff --git a/lib/lp/code/model/tests/test_gitrepository.py b/lib/lp/code/model/tests/test_gitrepository.py
17index 63a9a09..b21e131 100644
18--- a/lib/lp/code/model/tests/test_gitrepository.py
19+++ b/lib/lp/code/model/tests/test_gitrepository.py
20@@ -1400,6 +1400,10 @@ class TestGitRepositoryDeletion(TestCaseWithFactory):
21
22 def test_related_access_tokens_deleted(self):
23 _, token = self.factory.makeAccessToken(target=self.repository)
24+ _, expired_token = self.factory.makeAccessToken(
25+ target=self.repository,
26+ date_expires=datetime.now(pytz.UTC) - timedelta(minutes=1),
27+ )
28 other_repository = self.factory.makeGitRepository()
29 _, other_token = self.factory.makeAccessToken(target=other_repository)
30 self.repository.destroySelf()
31@@ -1408,6 +1412,12 @@ class TestGitRepositoryDeletion(TestCaseWithFactory):
32 self.assertRaises(
33 LostObjectError, getattr, removeSecurityProxy(token), "target"
34 )
35+ self.assertRaises(
36+ LostObjectError,
37+ getattr,
38+ removeSecurityProxy(expired_token),
39+ "target",
40+ )
41 # An unrelated repository's access tokens are still present.
42 self.assertEqual(
43 other_repository, removeSecurityProxy(other_token).target
44diff --git a/lib/lp/services/auth/interfaces.py b/lib/lp/services/auth/interfaces.py
45index dd3315c..be90837 100644
46--- a/lib/lp/services/auth/interfaces.py
47+++ b/lib/lp/services/auth/interfaces.py
48@@ -175,12 +175,15 @@ class IAccessTokenSet(Interface):
49 :param owner: An `IPerson`.
50 """
51
52- def findByTarget(target, visible_by_user=None):
53+ def findByTarget(target, visible_by_user=None, include_expired=False):
54 """Return all access tokens for this target.
55
56 :param target: An `IAccessTokenTarget`.
57 :param visible_by_user: If given, return only access tokens visible
58 by this user.
59+ :param include_expired: If True, include expired access tokens.
60+ This must only be used for non-authentication purposes, such as
61+ deleting database rows.
62 """
63
64 def getByTargetAndID(target, token_id, visible_by_user=None):
65@@ -208,8 +211,15 @@ class IAccessTokenTarget(Interface):
66 @operation_returns_collection_of(IAccessToken)
67 @export_read_operation()
68 @operation_for_version("devel")
69- def getAccessTokens(visible_by_user=None):
70- """Return personal access tokens for this target."""
71+ def getAccessTokens(visible_by_user=None, include_expired=False):
72+ """Return personal access tokens for this target.
73+
74+ :param visible_by_user: If given, return only access tokens visible
75+ by this user.
76+ :param include_expired: If True, include expired access tokens.
77+ This must only be used for non-authentication purposes, such as
78+ deleting database rows.
79+ """
80
81
82 patch_reference_property(IAccessToken, "target", IAccessTokenTarget)
83diff --git a/lib/lp/services/auth/model.py b/lib/lp/services/auth/model.py
84index c436650..f2e1d3a 100644
85--- a/lib/lp/services/auth/model.py
86+++ b/lib/lp/services/auth/model.py
87@@ -175,7 +175,9 @@ class AccessTokenSet:
88 """See `IAccessTokenSet`."""
89 return IStore(AccessToken).find(AccessToken, owner=owner)
90
91- def findByTarget(self, target, visible_by_user=None):
92+ def findByTarget(
93+ self, target, visible_by_user=None, include_expired=False
94+ ):
95 """See `IAccessTokenSet`."""
96 clauses = []
97 if IGitRepository.providedBy(target):
98@@ -203,12 +205,13 @@ class AccessTokenSet:
99 )
100 else:
101 raise TypeError("Unsupported target: {!r}".format(target))
102- clauses.append(
103- Or(
104- AccessToken.date_expires == None,
105- AccessToken.date_expires > UTC_NOW,
106+ if not include_expired:
107+ clauses.append(
108+ Or(
109+ AccessToken.date_expires == None,
110+ AccessToken.date_expires > UTC_NOW,
111+ )
112 )
113- )
114 return (
115 IStore(AccessToken)
116 .find(AccessToken, *clauses)
117@@ -227,8 +230,10 @@ class AccessTokenSet:
118 class AccessTokenTargetMixin:
119 """Mix this into classes that implement `IAccessTokenTarget`."""
120
121- def getAccessTokens(self, visible_by_user=None):
122+ def getAccessTokens(self, visible_by_user=None, include_expired=False):
123 """See `IAccessTokenTarget`."""
124 return getUtility(IAccessTokenSet).findByTarget(
125- self, visible_by_user=visible_by_user
126+ self,
127+ visible_by_user=visible_by_user,
128+ include_expired=include_expired,
129 )
130diff --git a/lib/lp/services/auth/tests/test_model.py b/lib/lp/services/auth/tests/test_model.py
131index 0823994..81f3023 100644
132--- a/lib/lp/services/auth/tests/test_model.py
133+++ b/lib/lp/services/auth/tests/test_model.py
134@@ -297,6 +297,12 @@ class TestAccessTokenSet(TestCaseWithFactory):
135 [current_token, expires_soon_token],
136 getUtility(IAccessTokenSet).findByTarget(target),
137 )
138+ self.assertContentEqual(
139+ [current_token, expires_soon_token, expired_token],
140+ getUtility(IAccessTokenSet).findByTarget(
141+ target, include_expired=True
142+ ),
143+ )
144
145 def test_getByTargetAndID(self):
146 targets = [self.factory.makeGitRepository() for _ in range(3)]
147@@ -420,6 +426,26 @@ class TestAccessTokenTargetBase:
148 [entry["description"] for entry in response.jsonBody()["entries"]],
149 )
150
151+ def test_getAccessTokens_excludes_expired(self):
152+ with person_logged_in(self.owner):
153+ self.factory.makeAccessToken(
154+ owner=self.owner, description="Current", target=self.target
155+ )
156+ self.factory.makeAccessToken(
157+ owner=self.owner,
158+ description="Expired",
159+ target=self.target,
160+ date_expires=datetime.now(pytz.UTC) - timedelta(minutes=1),
161+ )
162+ response = self.webservice.named_get(
163+ self.target_url, "getAccessTokens", api_version="devel"
164+ )
165+ self.assertEqual(200, response.status)
166+ self.assertContentEqual(
167+ ["Current"],
168+ [entry["description"] for entry in response.jsonBody()["entries"]],
169+ )
170+
171 def test_getAccessTokens_permissions(self):
172 webservice = webservice_for_person(None)
173 response = webservice.named_get(

Subscribers

People subscribed via source and target branches

to status/vote changes: