Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(security_logs): support added #149

Merged
merged 3 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 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, EnterpriseSecurityLogsResource
from .source_files.resource import SourceFilesResource
from .source_strings.resource import SourceStringsResource
from .storages.resource import StoragesResource
Expand Down Expand Up @@ -39,6 +40,8 @@
"ReportsResource",
"EnterpriseReportsResource",
"ScreenshotsResource",
"SecurityLogsResource",
"EnterpriseSecurityLogsResource",
"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"
114 changes: 114 additions & 0 deletions crowdin_api/api_resources/security_logs/resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from typing import Optional

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


class BaseSecurityLogsResource(BaseResource):
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,
limit: Optional[int] = None,
offset: Optional[int] = None,
page: Optional[int] = None,
andrii-bodnar marked this conversation as resolved.
Show resolved Hide resolved
event: Optional[SecurityLogEvent] = None,
ipAddress: Optional[str] = None,
andrii-bodnar marked this conversation as resolved.
Show resolved Hide resolved
):
"""
List User Security Logs

Link to documentaion:
https://developer.crowdin.com/api/v2/#operation/api.users.security-logs.getMany
"""

params = {"event": event, "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
),
)


class SecurityLogsResource(BaseSecurityLogsResource):
"""
Resource for Security Logs

Link to documentaion:
https://developer.crowdin.com/api/v2/#tag/Security-Logs
"""


class EnterpriseSecurityLogsResource(BaseSecurityLogsResource):
andrii-bodnar marked this conversation as resolved.
Show resolved Hide resolved
"""
Resource for Enterprise Security Logs

Link to documentation:
https://developer.crowdin.com/enterprise/api/v2/#tag/Security-Logs
"""

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,
limit: Optional[int] = None,
offset: Optional[int] = None,
page: Optional[int] = None,
andrii-bodnar marked this conversation as resolved.
Show resolved Hide resolved
event: Optional[SecurityLogEvent] = None,
ipAddress: Optional[str] = None,
userId: 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, "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,163 @@
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 (
EnterpriseSecurityLogsResource,
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,
"ipAddress": "127.0.0.1",
"offset": 0,
"limit": 10,
},
{
"event": SecurityLogEvent.LOGIN,
"ipAddress": "127.0.0.1",
"offset": 0,
"limit": 10,
},
),
(
{
"offset": 0,
"limit": 10,
},
{
"event": 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),
)


class TestEnterpriseSecurityLogsResource:

resource_class = EnterpriseSecurityLogsResource

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

@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,
"ipAddress": "127.0.0.1",
"userId": 1,
"offset": 0,
"limit": 10,
},
{
"event": SecurityLogEvent.LOGIN,
"ipAddress": "127.0.0.1",
"userId": 1,
"offset": 0,
"limit": 10,
},
),
(
{
"offset": 0,
"limit": 10,
},
{
"event": 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),
)
18 changes: 18 additions & 0 deletions crowdin_api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,24 @@ def screenshots(self) -> api_resources.ScreenshotsResource:
requester=self.get_api_requestor(), page_size=self.PAGE_SIZE
)

@property
def security_logs(self) -> Union[api_resources.SecurityLogsResource, api_resources.EnterpriseSecurityLogsResource]:
if self._is_enterprise_platform:
security_logs_class = api_resources.EnterpriseSecurityLogsResource
else:
security_logs_class = api_resources.SecurityLogsResource

if self.PROJECT_ID:
return security_logs_class(
requester=self.get_api_requestor(),
page_size=self.PAGE_SIZE,
project_id=self.PROJECT_ID,
)

return security_logs_class(
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", "EnterpriseSecurityLogsResource"),
("source_files", "SourceFilesResource"),
("source_strings", "SourceStringsResource"),
("storages", "StoragesResource"),
Expand Down
Loading