Skip to content

Commit 0d70909

Browse files
authored
Merge pull request #126 from GitGuardian/salomevoltz/add-api-tokens-endpoint-method
Add api_tokens() method to retrieve API token details
2 parents 6187258 + 0deba64 commit 0d70909

File tree

6 files changed

+237
-2
lines changed

6 files changed

+237
-2
lines changed

.gitguardian.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
secret:
2-
ignored-matches:
2+
ignored_matches:
33
- match: 5e9107aedc48b14af2749703ed3f83c2e1e6aca82ed86af980a2b925708c2da6
44
name: ''
5-
ignored-paths:
5+
ignored_paths:
66
- doc/**/*
77
- README.md
88
- CONTRIBUTING.md
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<!--
2+
A new scriv changelog fragment.
3+
4+
Uncomment the section that is right (remove the HTML comment wrapper).
5+
-->
6+
7+
<!--
8+
### Removed
9+
10+
- A bullet item for the Removed category.
11+
12+
-->
13+
14+
### Added
15+
16+
- `GGClient` now provides a `api_tokens()` method to retrieve API token details (see https://api.gitguardian.com/docs#tag/API-Tokens).
17+
18+
<!--
19+
### Changed
20+
21+
- A bullet item for the Changed category.
22+
23+
-->
24+
<!--
25+
### Deprecated
26+
27+
- A bullet item for the Deprecated category.
28+
29+
-->
30+
<!--
31+
### Fixed
32+
33+
- A bullet item for the Fixed category.
34+
35+
-->
36+
<!--
37+
### Security
38+
39+
- A bullet item for the Security category.
40+
41+
-->

pygitguardian/client.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
IaCScanResult,
2626
)
2727
from .models import (
28+
ApiTokensResponse,
2829
Detail,
2930
Document,
3031
DocumentSchema,
@@ -352,6 +353,35 @@ def health_check(self) -> HealthCheckResponse:
352353
secrets_engine_version=self.secrets_engine_version,
353354
)
354355

356+
def api_tokens(
357+
self, token: Optional[str] = None
358+
) -> Union[Detail, ApiTokensResponse]:
359+
"""
360+
api_tokens retrieves details of an API token
361+
If no token is passed, the endpoint retrieves details for the current API token.
362+
363+
use Detail.status_code to check the response status code of the API
364+
365+
200 if server is online, return token details
366+
:return: Detail or ApiTokensReponse and status code
367+
"""
368+
try:
369+
if not token:
370+
token = "self"
371+
resp = self.get(
372+
endpoint=f"api_tokens/{token}",
373+
)
374+
except requests.exceptions.ReadTimeout:
375+
result = Detail("The request timed out.")
376+
result.status_code = 504
377+
else:
378+
if resp.ok:
379+
result = ApiTokensResponse.from_dict(resp.json())
380+
else:
381+
result = load_detail(resp)
382+
result.status_code = resp.status_code
383+
return result
384+
355385
def content_scan(
356386
self,
357387
document: str,

pygitguardian/models.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -756,6 +756,91 @@ def __repr__(self) -> str:
756756
)
757757

758758

759+
class TokenType(str, Enum):
760+
PERSONAL_ACCESS_TOKEN = "personal_access_token"
761+
SERVICE_ACCOUNT = "service_account"
762+
763+
764+
class TokenStatus(str, Enum):
765+
ACTIVE = "active"
766+
EXPIRED = "expired"
767+
REVOKED = "revoked"
768+
769+
770+
class TokenScopes(str, Enum):
771+
SCAN = "scan"
772+
INCIDENTS_READ = "incidents:read"
773+
INCIDENTS_WRITE = "incidents:write"
774+
INCIDENTS_SHARE = "incidents:share"
775+
MEMBERS_READ = "members:read"
776+
MEMBERS_WRITE = "members:write"
777+
TEAMS_READ = "teams:read"
778+
TEAMS_WRITE = "teams:write"
779+
AUDIT_LOGS_READ = "audit_logs:read"
780+
HONEYTOKENS_READ = "honeytokens:read"
781+
HONEYTOKENS_WRITE = "honeytokens:write"
782+
API_TOKENS_READ = "api_tokens:read"
783+
API_TOKENS_WRITE = "api_tokens:write"
784+
IP_ALLOWLIST_READ = "ip_allowlist:read"
785+
IP_ALLOWLIST_WRITE = "ip_allowlist:write"
786+
SOURCES_READ = "sources:read"
787+
SOURCES_WRITE = "sources:write"
788+
NHI_WRITE = "nhi:write"
789+
790+
791+
class ApiTokensResponseSchema(BaseSchema):
792+
id = fields.UUID(required=True)
793+
name = fields.String(required=True)
794+
workspace_id = fields.Int(required=True)
795+
type = fields.Enum(TokenType, by_value=True, required=True)
796+
status = fields.Enum(TokenStatus, by_value=True, required=True)
797+
created_at = fields.AwareDateTime(required=True)
798+
last_used_at = fields.AwareDateTime(allow_none=True)
799+
expire_at = fields.AwareDateTime(allow_none=True)
800+
revoked_at = fields.AwareDateTime(allow_none=True)
801+
member_id = fields.Int(allow_none=True)
802+
creator_id = fields.Int(allow_none=True)
803+
scopes = fields.List(fields.Enum(TokenScopes, by_value=True), required=False)
804+
805+
@post_load
806+
def make_api_tokens_response(
807+
self, data: Dict[str, Any], **kwargs: Any
808+
) -> "ApiTokensResponse":
809+
return ApiTokensResponse(**data)
810+
811+
812+
class ApiTokensResponse(Base, FromDictMixin):
813+
SCHEMA = ApiTokensResponseSchema()
814+
815+
def __init__(
816+
self,
817+
id: UUID,
818+
name: str,
819+
workspace_id: int,
820+
type: TokenType,
821+
status: TokenStatus,
822+
created_at: datetime,
823+
last_used_at: Optional[datetime] = None,
824+
expire_at: Optional[datetime] = None,
825+
revoked_at: Optional[datetime] = None,
826+
member_id: Optional[int] = None,
827+
creator_id: Optional[int] = None,
828+
scopes: Optional[List[TokenScopes]] = None,
829+
):
830+
self.id = id
831+
self.name = name
832+
self.workspace_id = workspace_id
833+
self.type = type
834+
self.status = status
835+
self.created_at = created_at
836+
self.last_used_at = last_used_at
837+
self.expire_at = expire_at
838+
self.revoked_at = revoked_at
839+
self.member_id = member_id
840+
self.creator_id = creator_id
841+
self.scopes = scopes or []
842+
843+
759844
@dataclass
760845
class SecretScanPreferences:
761846
maximum_document_size: int = DOCUMENT_SIZE_THRESHOLD_BYTES

tests/test_client.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
MULTI_DOCUMENT_LIMIT,
2424
)
2525
from pygitguardian.models import (
26+
ApiTokensResponse,
2627
Detail,
2728
HoneytokenResponse,
2829
HoneytokenWithContextResponse,
@@ -867,6 +868,64 @@ def test_versions_from_headers(client: GGClient, method):
867868
assert other_client.secrets_engine_version == secrets_engine_version_value
868869

869870

871+
@responses.activate
872+
@pytest.mark.parametrize("token", ["self", "token"])
873+
def test_api_tokens(client: GGClient, token):
874+
"""
875+
GIVEN a ggclient
876+
WHEN calling api_tokens with or without a token
877+
THEN the method returns the token details
878+
"""
879+
mock_response = responses.get(
880+
url=client._url_from_endpoint(f"api_tokens/{token}", "v1"),
881+
content_type="application/json",
882+
status=201,
883+
json={
884+
"id": "5ddaad0c-5a0c-4674-beb5-1cd198d13360",
885+
"name": "myTokenName",
886+
"workspace_id": 42,
887+
"type": "personal_access_token",
888+
"status": "revoked",
889+
"created_at": "2023-05-20T12:40:55.662949Z",
890+
"last_used_at": "2023-05-24T12:40:55.662949Z",
891+
"expire_at": None,
892+
"revoked_at": "2023-05-27T12:40:55.662949Z",
893+
"member_id": 22015,
894+
"creator_id": 22015,
895+
"scopes": ["incidents:read", "scan"],
896+
},
897+
)
898+
899+
result = client.api_tokens(token)
900+
901+
assert mock_response.call_count == 1
902+
assert isinstance(result, ApiTokensResponse)
903+
904+
905+
@responses.activate
906+
def test_api_tokens_error(
907+
client: GGClient,
908+
):
909+
"""
910+
GIVEN a ggclient
911+
WHEN calling api_tokens with an invalid token
912+
THEN the method returns a Detail object containing the error detail
913+
"""
914+
mock_response = responses.get(
915+
url=client._url_from_endpoint("api_tokens/invalid", "v1"),
916+
content_type="application/json",
917+
status=400,
918+
json={
919+
"detail": "Not authorized",
920+
},
921+
)
922+
923+
result = client.api_tokens(token="invalid")
924+
925+
assert mock_response.call_count == 1
926+
assert isinstance(result, Detail)
927+
928+
870929
@responses.activate
871930
def test_create_honeytoken(
872931
client: GGClient,

tests/test_models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import pytest
44

55
from pygitguardian.models import (
6+
ApiTokensResponse,
7+
ApiTokensResponseSchema,
68
Detail,
79
DetailSchema,
810
Document,
@@ -63,6 +65,24 @@ def test_document_handle_surrogates(self):
6365
OrderedDict,
6466
{"detail": "hello", "status_code": 200},
6567
),
68+
(
69+
ApiTokensResponseSchema,
70+
ApiTokensResponse,
71+
{
72+
"id": "5ddaad0c-5a0c-4674-beb5-1cd198d13360",
73+
"name": "myTokenName",
74+
"workspace_id": 42,
75+
"type": "personal_access_token",
76+
"status": "revoked",
77+
"created_at": "2023-05-20T12:40:55.662949Z",
78+
"last_used_at": "2023-05-24T12:40:55.662949Z",
79+
"expire_at": None,
80+
"revoked_at": "2023-05-27T12:40:55.662949Z",
81+
"member_id": 22015,
82+
"creator_id": 22015,
83+
"scopes": ["incidents:read", "scan"],
84+
},
85+
),
6686
(MatchSchema, Match, {"match": "hello", "type": "hello"}),
6787
(
6888
MultiScanResultSchema,

0 commit comments

Comments
 (0)