Skip to content

Commit

Permalink
feat(api): Security Logs methods support (#149)
Browse files Browse the repository at this point in the history
  • Loading branch information
OmAximani0 authored Feb 8, 2024
1 parent 5b2c756 commit 627bcb6
Show file tree
Hide file tree
Showing 7 changed files with 331 additions and 0 deletions.
2 changes: 2 additions & 0 deletions crowdin_api/api_resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .projects.resource import ProjectsResource
from .reports.resource import ReportsResource, EnterpriseReportsResource
from .screenshots.resource import ScreenshotsResource
from .security_logs.resource import SecurityLogsResource
from .source_files.resource import SourceFilesResource
from .source_strings.resource import SourceStringsResource
from .storages.resource import StoragesResource
Expand Down Expand Up @@ -39,6 +40,7 @@
"ReportsResource",
"EnterpriseReportsResource",
"ScreenshotsResource",
"SecurityLogsResource",
"SourceFilesResource",
"SourceStringsResource",
"StoragesResource",
Expand Down
1 change: 1 addition & 0 deletions crowdin_api/api_resources/security_logs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__pdoc__ = {"tests": False}
26 changes: 26 additions & 0 deletions crowdin_api/api_resources/security_logs/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from enum import Enum


class SecurityLogEvent(Enum):
LOGIN = "login"
PASSWORD_SET = "password.set"
PASSWORD_CHANGE = "password.change"
EMAIL_CHANGE = "email.change"
LOGIN_CHANGE = "login.change"
PERSONAL_TOKEN_ISSUED = "personal_token.issued"
PERSONAL_TOKEN_REVOKED = "personal_token.revoked"
MFA_ENABLED = "mfa.enabled"
MFA_DISABLED = "mfa.disabled"
SESSION_REVOKE = "session.revoke"
SESSION_REVOKE_ALL = "session.revoke_all"
SSO_CONNECT = "sso.connect"
SSO_DISCONNECT = "sso.disconnect"
USER_REMOVE = "user.remove"
APPLICATION_CONNECTED = "application.connected"
APPLICATION_DISCONNECTED = "application.disconnected"
WEBAUTHN_CREATED = "webauthn.created"
WEBAUTHN_DELETED = "webauthn.deleted"
TRUSTED_DEVICE_REMOVE = "trusted_device.remove"
TRUSTED_DEVICE_REMOVE_ALL = "trusted_device.remove_all"
DEVICE_VERIFICATION_ENABLED = "device_verification.enabled"
DEVICE_VERIFICATION_DISABLED = "device_verification.disabled"
122 changes: 122 additions & 0 deletions crowdin_api/api_resources/security_logs/resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
from datetime import datetime
from typing import Optional

from crowdin_api.api_resources.abstract.resources import BaseResource
from crowdin_api.api_resources.security_logs.enums import SecurityLogEvent


class SecurityLogsResource(BaseResource):
"""
Resource for Security Logs
Link to documentaion:
https://developer.crowdin.com/api/v2/#tag/Security-Logs
Link to documentation for enterprise:
https://developer.crowdin.com/enterprise/api/v2/#tag/Security-Logs
"""

def get_user_security_logs_path(
self, userId: int, securityLogId: Optional[int] = None
):
if securityLogId is not None:
return f"/users/{userId}/security-logs/{securityLogId}"
return f"/users/{userId}/security-logs"

def list_user_security_logs(
self,
userId: int,
event: Optional[SecurityLogEvent] = None,
createdAfter: Optional[datetime] = None,
createdBefore: Optional[datetime] = None,
ipAddress: Optional[str] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
page: Optional[int] = None,
):
"""
List User Security Logs
Link to documentaion:
https://developer.crowdin.com/api/v2/#operation/api.users.security-logs.getMany
"""

params = {
"event": event,
"createdAfter": createdAfter,
"createdBefore": createdBefore,
"ipAddress": ipAddress,
}
params.update(self.get_page_params(page=page, offset=offset, limit=limit))

return self._get_entire_data(
method="get",
path=self.get_user_security_logs_path(userId=userId),
params=params,
)

def get_user_security_log(self, userId: int, securityLogId: int):
"""
Get User Security Log
Link to documentation:
https://developer.crowdin.com/api/v2/#operation/api.users.security-logs.get
"""

return self.requester.request(
method="get",
path=self.get_user_security_logs_path(
userId=userId, securityLogId=securityLogId
),
)

def get_organization_security_logs_path(self, securityLogId: Optional[int] = None):
if securityLogId is not None:
return f"/security-logs/{securityLogId}"
return "/security-logs"

def list_organization_security_logs(
self,
event: Optional[SecurityLogEvent] = None,
createdAfter: Optional[datetime] = None,
createdBefore: Optional[datetime] = None,
ipAddress: Optional[str] = None,
userId: Optional[int] = None,
limit: Optional[int] = None,
offset: Optional[int] = None,
page: Optional[int] = None,
):
"""
List Organization Security Logs
Link to documentation:
https://developer.crowdin.com/enterprise/api/v2/#operation/api.security-logs.getMany
"""

params = {
"event": event,
"createdAfter": createdAfter,
"createdBefore": createdBefore,
"ipAddress": ipAddress,
"userId": userId,
}
params.update(self.get_page_params(page=page, offset=offset, limit=limit))

return self._get_entire_data(
method="get",
path=self.get_organization_security_logs_path(),
params=params,
)

def get_organization_security_log(self, securityLogId: int):
"""
Get Organization Security Log
Link to documentaion:
https://developer.crowdin.com/enterprise/api/v2/#operation/api.security-logs.get
"""

return self.requester.request(
method="get",
path=self.get_organization_security_logs_path(securityLogId=securityLogId),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
from datetime import datetime
from unittest import mock

import pytest
from crowdin_api.api_resources.security_logs.enums import SecurityLogEvent
from crowdin_api.api_resources.security_logs.resource import SecurityLogsResource
from crowdin_api.requester import APIRequester


class TestSecurityLogsResource:
resource_class = SecurityLogsResource

def get_resource(self, base_absolut_url):
return self.resource_class(requester=APIRequester(base_url=base_absolut_url))

@pytest.mark.parametrize(
"incoming_data, path",
(
({"userId": 1}, "/users/1/security-logs"),
({"userId": 1, "securityLogId": 2}, "/users/1/security-logs/2"),
),
)
def test_get_user_security_logs_path(self, incoming_data, path, base_absolut_url):
resource = self.get_resource(base_absolut_url)
assert resource.get_user_security_logs_path(**incoming_data) == path

@pytest.mark.parametrize(
"in_params, request_params",
(
(
{
"event": SecurityLogEvent.LOGIN,
"createdAfter": datetime(year=2024, month=1, day=1, hour=12),
"createdBefore": datetime(year=2024, month=1, day=30, hour=12),
"ipAddress": "127.0.0.1",
"offset": 0,
"limit": 10,
},
{
"event": SecurityLogEvent.LOGIN,
"createdAfter": datetime(year=2024, month=1, day=1, hour=12),
"createdBefore": datetime(year=2024, month=1, day=30, hour=12),
"ipAddress": "127.0.0.1",
"offset": 0,
"limit": 10,
},
),
(
{
"offset": 0,
"limit": 10,
},
{
"event": None,
"createdAfter": None,
"createdBefore": None,
"ipAddress": None,
"offset": 0,
"limit": 10,
},
),
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
def test_list_user_security_logs(
self, m_request, in_params, request_params, base_absolut_url
):
m_request.return_value = "response"

resource = self.get_resource(base_absolut_url)
assert resource.list_user_security_logs(userId=1, **in_params) == "response"
m_request.assert_called_once_with(
method="get",
path=resource.get_user_security_logs_path(userId=1),
params=request_params,
)

@mock.patch("crowdin_api.requester.APIRequester.request")
def test_get_user_security_log(self, m_request, base_absolut_url):
m_request.return_value = "response"

resource = self.get_resource(base_absolut_url)
assert resource.get_user_security_log(userId=1, securityLogId=2) == "response"
m_request.assert_called_once_with(
method="get",
path=resource.get_user_security_logs_path(userId=1, securityLogId=2),
)

@pytest.mark.parametrize(
"incoming_data, path",
(
({}, "/security-logs"),
({"securityLogId": 1}, "/security-logs/1"),
),
)
def test_get_organization_security_logs_path(
self, incoming_data, path, base_absolut_url
):
resource = self.get_resource(base_absolut_url)
assert resource.get_organization_security_logs_path(**incoming_data) == path

@pytest.mark.parametrize(
"in_params, request_params",
(
(
{
"event": SecurityLogEvent.LOGIN,
"createdAfter": datetime(year=2024, month=1, day=1, hour=12),
"createdBefore": datetime(year=2024, month=1, day=30, hour=12),
"ipAddress": "127.0.0.1",
"userId": 1,
"offset": 0,
"limit": 10,
},
{
"event": SecurityLogEvent.LOGIN,
"createdAfter": datetime(year=2024, month=1, day=1, hour=12),
"createdBefore": datetime(year=2024, month=1, day=30, hour=12),
"ipAddress": "127.0.0.1",
"userId": 1,
"offset": 0,
"limit": 10,
},
),
(
{
"offset": 0,
"limit": 10,
},
{
"event": None,
"createdAfter": None,
"createdBefore": None,
"ipAddress": None,
"userId": None,
"offset": 0,
"limit": 10,
},
),
),
)
@mock.patch("crowdin_api.requester.APIRequester.request")
def test_list_organization_security_logs(
self, m_request, in_params, request_params, base_absolut_url
):
m_request.return_value = "response"

resource = self.get_resource(base_absolut_url)
assert resource.list_organization_security_logs(**in_params) == "response"
m_request.assert_called_once_with(
method="get",
path=resource.get_organization_security_logs_path(),
params=request_params,
)

@mock.patch("crowdin_api.requester.APIRequester.request")
def test_get_organization_security_log(self, m_request, base_absolut_url):
m_request.return_value = "response"

resource = self.get_resource(base_absolut_url)
assert resource.get_organization_security_log(securityLogId=1) == "response"
m_request.assert_called_once_with(
method="get",
path=resource.get_organization_security_logs_path(securityLogId=1),
)
13 changes: 13 additions & 0 deletions crowdin_api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,19 @@ def screenshots(self) -> api_resources.ScreenshotsResource:
requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
)

@property
def security_logs(self) -> api_resources.SecurityLogsResource:
if self.PROJECT_ID:
return api_resources.SecurityLogsResource(
requester=self.get_api_requestor(),
page_size=self.PAGE_SIZE,
project_id=self.PROJECT_ID,
)

return api_resources.SecurityLogsResource(
requester=self.get_api_requestor(), page_size=self.PAGE_SIZE,
)

@property
def source_files(self) -> api_resources.SourceFilesResource:
if self.PROJECT_ID:
Expand Down
2 changes: 2 additions & 0 deletions crowdin_api/tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ def test_api_requestor(self, m_APIRequester):
("projects", "ProjectsResource"),
("reports", "ReportsResource"),
("screenshots", "ScreenshotsResource"),
("security_logs", "SecurityLogsResource"),
("source_files", "SourceFilesResource"),
("source_strings", "SourceStringsResource"),
("storages", "StoragesResource"),
Expand Down Expand Up @@ -200,6 +201,7 @@ class TestCrowdinClientEnterprise:
("projects", "ProjectsResource"),
("reports", "EnterpriseReportsResource"),
("screenshots", "ScreenshotsResource"),
("security_logs", "SecurityLogsResource"),
("source_files", "SourceFilesResource"),
("source_strings", "SourceStringsResource"),
("storages", "StoragesResource"),
Expand Down

0 comments on commit 627bcb6

Please sign in to comment.