Skip to content

Commit 650f2b2

Browse files
authored
Return only own tokens in api/auth/access_tokens (#9950)
1 parent 875acf0 commit 650f2b2

9 files changed

Lines changed: 189 additions & 138 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
### Changed
2+
3+
- Admins will no longer see access tokens of other users on the token management page
4+
(<https://github.com/cvat-ai/cvat/pull/9950>)
5+
6+
### Removed
7+
8+
- \[Server API\] Only own access tokens will be returned in the `GET /api/auth/access_tokens`
9+
responses for everyone, including admins
10+
(<https://github.com/cvat-ai/cvat/pull/9950>)
11+
- \[Server API\] The `owner` filters are removed from the `GET /api/auth/access_tokens` endpoint
12+
(<https://github.com/cvat-ai/cvat/pull/9950>)

cvat/apps/access_tokens/rules/access_tokens.rego

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,5 @@ q_user_is_owner(user) := [
5454
{"owner_id": user.id},
5555
]
5656

57-
filter := [] if { # Django Q object to filter list of entries
58-
utils.is_admin
59-
} else := qobject if {
60-
user := input.auth.user
61-
qobject := q_user_is_owner(user)
62-
}
57+
# Django Q object to filter list of entries
58+
filter := q_user_is_owner(input.auth.user)

cvat/apps/access_tokens/views.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -57,19 +57,15 @@ class AccessTokensViewSet(
5757
search_fields = ("name",)
5858
filter_fields = list(search_fields) + [
5959
"id",
60-
"owner",
6160
"created_date",
6261
"updated_date",
6362
"expiry_date",
6463
"last_used_date",
6564
"read_only",
6665
]
67-
simple_filters = list(search_fields) + ["owner"]
66+
simple_filters = list(search_fields)
6867
ordering_fields = list(filter_fields)
6968
ordering = "-id"
70-
lookup_fields = {
71-
"owner": "owner__id",
72-
}
7369

7470
iam_organization_field = None
7571
iam_permission_class = AccessTokenPermission

cvat/schema.yml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,19 +103,14 @@ paths:
103103
104104
Details about the syntax used can be found at the link: https://jsonlogic.com/
105105
106-
Available filter_fields: ['name', 'id', 'owner', 'created_date', 'updated_date', 'expiry_date', 'last_used_date', 'read_only'].
106+
Available filter_fields: ['name', 'id', 'created_date', 'updated_date', 'expiry_date', 'last_used_date', 'read_only'].
107107
schema:
108108
type: string
109109
- name: name
110110
in: query
111111
description: A simple equality filter for the name field
112112
schema:
113113
type: string
114-
- name: owner
115-
in: query
116-
description: A simple equality filter for the owner field
117-
schema:
118-
type: integer
119114
- name: page
120115
required: false
121116
in: query
@@ -138,7 +133,7 @@ paths:
138133
required: false
139134
in: query
140135
description: 'Which field to use when ordering the results. Available ordering_fields:
141-
[''name'', ''id'', ''owner'', ''created_date'', ''updated_date'', ''expiry_date'',
136+
[''name'', ''id'', ''created_date'', ''updated_date'', ''expiry_date'',
142137
''last_used_date'', ''read_only'']'
143138
schema:
144139
type: string

tests/python/rest_api/test_access_tokens.py

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,13 @@
22
import json
33
from contextlib import ExitStack
44
from datetime import datetime, timedelta, timezone
5+
from http import HTTPStatus
56
from io import StringIO
67
from time import sleep
78

89
import pytest
910
from cvat_sdk.api_client import exceptions, models
1011
from cvat_sdk.api_client.api_client import ApiClient, Endpoint
11-
from cvat_sdk.core.utils import filter_dict
1212
from deepdiff import DeepDiff
1313
from pytest_cases import parametrize
1414

@@ -88,26 +88,29 @@ def test_can_get_access_token(self, admin_user, access_tokens):
8888

8989
assert DeepDiff(expected, actual, exclude_paths=["private_key"]) == {}
9090

91-
def test_cannot_see_foreign_tokens(self, users, access_tokens_by_username):
91+
@parametrize("is_admin", [True, False])
92+
def test_cannot_see_foreign_tokens(self, users, access_tokens_by_username, is_admin):
9293
token_owner, token_owner_tokens = next(iter(access_tokens_by_username.items()))
9394
token = token_owner_tokens[0]
9495

9596
other_user = next(
96-
u for u in users if u["username"] != token_owner and not u["is_superuser"]
97+
u
98+
for u in users
99+
if u["username"] != token_owner
100+
if u["is_superuser"] == is_admin
101+
if not access_tokens_by_username.get(u["username"])
97102
)
98103

99-
with (
100-
make_api_client(other_user["username"]) as api_client,
101-
pytest.raises(exceptions.ForbiddenException),
102-
):
103-
api_client.auth_api.retrieve_access_tokens(token["id"])
104+
with make_api_client(other_user["username"]) as api_client:
105+
_, response = api_client.auth_api.retrieve_access_tokens(
106+
token["id"], _check_status=False
107+
)
108+
if is_admin:
109+
assert response.status == HTTPStatus.OK
110+
else:
111+
assert response.status == HTTPStatus.FORBIDDEN
104112

105-
assert (
106-
api_client.auth_api.list_access_tokens(
107-
filter=json.dumps({"!": {"==": [{"var": "owner"}, other_user["id"]]}})
108-
)[0].count
109-
== 0
110-
)
113+
assert api_client.auth_api.list_access_tokens()[0].count == 0
111114

112115
def test_can_get_self(self, access_tokens_by_username):
113116
_, user_tokens = next(iter(access_tokens_by_username.items()))
@@ -160,21 +163,19 @@ def test_can_only_see_alive_tokens(self, token_eol_reason: str, admin_user):
160163

161164

162165
class TestAccessTokenListFilters(CollectionSimpleFilterTestBase):
163-
field_lookups = {"owner": ["owner", "id"]}
164-
165-
@pytest.fixture(scope="session")
166-
def _cleaned_access_tokens(self, access_tokens):
167-
return [filter_dict(t, drop=("private_key",)) for t in access_tokens]
168-
169166
@pytest.fixture(autouse=True)
170-
def setup(self, restore_db_per_class, admin_user, _cleaned_access_tokens):
171-
self.user = admin_user
172-
self.samples = _cleaned_access_tokens
167+
def setup(self, restore_db_per_class, users_by_name, raw_access_tokens_by_username):
168+
# Only own keys are visible to each user
169+
self.user, self.samples = next(
170+
(username, user_tokens)
171+
for username, user_tokens in raw_access_tokens_by_username.items()
172+
if users_by_name[username]["is_superuser"] and user_tokens
173+
)
173174

174175
def _get_endpoint(self, api_client: ApiClient) -> Endpoint:
175176
return api_client.auth_api.list_access_tokens_endpoint
176177

177-
@pytest.mark.parametrize("field", ("name", "owner"))
178+
@pytest.mark.parametrize("field", ("name",))
178179
def test_can_use_simple_filter_for_object_list(self, field):
179180
return super()._test_can_use_simple_filter_for_object_list(field)
180181

tests/python/rest_api/test_check_objects_integrity.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@ def test_check_objects_integrity(self, path: Path):
4141
)
4242
== {}
4343
)
44+
elif endpoint == "auth/access_tokens":
45+
objects = json.load(f)
46+
assert set(objects) == {"user"}
47+
48+
for username, tokens in objects["user"].items():
49+
response = config.get_method(
50+
username, "auth/access_tokens", page_size=100, sort="id"
51+
).json()["results"]
52+
assert (
53+
DeepDiff(
54+
tokens,
55+
response,
56+
ignore_order=True,
57+
)
58+
== {}
59+
)
4460
else:
4561
response = config.get_method("admin1", endpoint, page_size="all")
4662
json_objs = json.load(f)
Lines changed: 83 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,87 @@
11
{
2-
"count": 4,
3-
"next": null,
4-
"previous": null,
5-
"results": [
6-
{
7-
"created_date": "2025-08-12T10:30:10.206000Z",
8-
"expiry_date": null,
9-
"id": 7,
10-
"last_used_date": null,
11-
"name": "test readonly key",
12-
"owner": {
13-
"first_name": "Admin",
14-
"id": 18,
15-
"last_name": "Second",
16-
"url": "http://localhost:8080/api/users/18",
17-
"username": "admin2"
2+
"user": {
3+
"admin1": [],
4+
"admin2": [
5+
{
6+
"created_date": "2025-07-17T16:17:35.436000Z",
7+
"expiry_date": null,
8+
"id": 3,
9+
"last_used_date": null,
10+
"name": "test key",
11+
"owner": {
12+
"first_name": "Admin",
13+
"id": 18,
14+
"last_name": "Second",
15+
"url": "http://localhost:8080/api/users/18",
16+
"username": "admin2"
17+
},
18+
"read_only": false,
19+
"updated_date": "2025-07-17T16:17:35.436000Z"
1820
},
19-
"read_only": true,
20-
"updated_date": "2025-08-12T10:30:10.206000Z"
21-
},
22-
{
23-
"created_date": "2025-07-17T16:19:46.576000Z",
24-
"expiry_date": null,
25-
"id": 5,
26-
"last_used_date": null,
27-
"name": "test readwrite key",
28-
"owner": {
29-
"first_name": "User",
30-
"id": 2,
31-
"last_name": "First",
32-
"url": "http://localhost:8080/api/users/2",
33-
"username": "user1"
21+
{
22+
"created_date": "2025-08-12T10:30:10.206000Z",
23+
"expiry_date": null,
24+
"id": 7,
25+
"last_used_date": null,
26+
"name": "test readonly key",
27+
"owner": {
28+
"first_name": "Admin",
29+
"id": 18,
30+
"last_name": "Second",
31+
"url": "http://localhost:8080/api/users/18",
32+
"username": "admin2"
33+
},
34+
"read_only": true,
35+
"updated_date": "2025-08-12T10:30:10.206000Z"
36+
}
37+
],
38+
"lonely_user": [],
39+
"user1": [
40+
{
41+
"created_date": "2025-07-17T16:19:18.229000Z",
42+
"expiry_date": null,
43+
"id": 4,
44+
"last_used_date": null,
45+
"name": "test readonly key",
46+
"owner": {
47+
"first_name": "User",
48+
"id": 2,
49+
"last_name": "First",
50+
"url": "http://localhost:8080/api/users/2",
51+
"username": "user1"
52+
},
53+
"read_only": true,
54+
"updated_date": "2025-07-17T16:19:18.229000Z"
3455
},
35-
"read_only": false,
36-
"updated_date": "2025-07-17T16:19:46.576000Z"
37-
},
38-
{
39-
"created_date": "2025-07-17T16:19:18.229000Z",
40-
"expiry_date": null,
41-
"id": 4,
42-
"last_used_date": null,
43-
"name": "test readonly key",
44-
"owner": {
45-
"first_name": "User",
46-
"id": 2,
47-
"last_name": "First",
48-
"url": "http://localhost:8080/api/users/2",
49-
"username": "user1"
50-
},
51-
"read_only": true,
52-
"updated_date": "2025-07-17T16:19:18.229000Z"
53-
},
54-
{
55-
"created_date": "2025-07-17T16:17:35.436000Z",
56-
"expiry_date": null,
57-
"id": 3,
58-
"last_used_date": null,
59-
"name": "test key",
60-
"owner": {
61-
"first_name": "Admin",
62-
"id": 18,
63-
"last_name": "Second",
64-
"url": "http://localhost:8080/api/users/18",
65-
"username": "admin2"
66-
},
67-
"read_only": false,
68-
"updated_date": "2025-07-17T16:17:35.436000Z"
69-
}
70-
]
56+
{
57+
"created_date": "2025-07-17T16:19:46.576000Z",
58+
"expiry_date": null,
59+
"id": 5,
60+
"last_used_date": null,
61+
"name": "test readwrite key",
62+
"owner": {
63+
"first_name": "User",
64+
"id": 2,
65+
"last_name": "First",
66+
"url": "http://localhost:8080/api/users/2",
67+
"username": "user1"
68+
},
69+
"read_only": false,
70+
"updated_date": "2025-07-17T16:19:46.576000Z"
71+
}
72+
],
73+
"user10": [],
74+
"user2": [],
75+
"user3": [],
76+
"user4": [],
77+
"user5": [],
78+
"user6": [],
79+
"user7": [],
80+
"user8": [],
81+
"user9": [],
82+
"worker1": [],
83+
"worker2": [],
84+
"worker3": [],
85+
"worker4": []
86+
}
7187
}

0 commit comments

Comments
 (0)