Skip to content

Commit 3651e38

Browse files
authored
[Feature] Google Workspace API Support
2 parents 69fc736 + fb3afc7 commit 3651e38

File tree

19 files changed

+558
-30
lines changed

19 files changed

+558
-30
lines changed

.github/workflows/tests.yaml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ jobs:
1919
run: PYTHONPATH=. pytest --cov-report term-missing --cov=src tests/UnitTests/test_*.py
2020

2121
e2e-tests:
22+
needs: unit-tests # Run e2e test only if unit tests passed
2223
runs-on: ubuntu-latest
2324
steps:
2425
- uses: actions/checkout@v4
@@ -43,4 +44,10 @@ jobs:
4344
AZURE_AD_TENANT_ID: ${{ secrets.AZURE_AD_TENANT_ID }}
4445
AZURE_AD_CLIENT_ID: ${{ secrets.AZURE_AD_CLIENT_ID }}
4546
AZURE_AD_SECRET_VALUE: ${{ secrets.AZURE_AD_SECRET_VALUE }}
47+
GOOGLE_PRIVATE_KEY_ID: ${{ secrets.GOOGLE_PRIVATE_KEY_ID }}
48+
GOOGLE_PRIVATE_KEY: ${{ secrets.GOOGLE_PRIVATE_KEY }}
49+
GOOGLE_CLIENT_EMAIL: ${{ secrets.GOOGLE_CLIENT_EMAIL }}
50+
GOOGLE_CLIENT_ID: ${{ secrets.GOOGLE_CLIENT_ID }}
51+
GOOGLE_CERT_URL: ${{ secrets.GOOGLE_CERT_URL }}
52+
GOOGLE_DELEGATED_ACCOUNT: ${{ secrets.GOOGLE_DELEGATED_ACCOUNT }}
4653
run: python -m unittest

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.idea
2+
.venv
3+
__pycache__
4+
.coverage
5+
.DS_Store

README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,49 @@ For dockerhub audit logs, use type `dockerhub` with the below parameters.
254254
| additional_fields | Additional custom fields to add to the logs before sending to logzio | Optional | - |
255255

256256

257+
</details>
258+
<details>
259+
<summary>
260+
<span><a href="./src/apis/google/README.md#google-workspace-activities">Google Workspace Activity</a></span>
261+
</summary>
262+
263+
For Google Workspace Activity, use type `google_activity` with the below parameters.
264+
265+
266+
## Configuration Options
267+
| Parameter Name | Description | Required/Optional | Default |
268+
|-----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------|-----------------------------------------|
269+
| name | Name of the API (custom name) | Optional | `Google Workspace` |
270+
| google_ws_sa_file_name | The name of the service account credentials file. **Required unless** `google_ws_sa_file_path` is set. | Required* | `""` |
271+
| google_ws_sa_file_path | The path to the service account credentials file. **Required unless** `google_ws_sa_file_name` is set. Use this if mounting the file to a different path than the default. | Optional* | `./src/shared/<google_ws_sa_file_name>` |
272+
| google_ws_delegated_account | The email of the user for which the application is requesting delegated access | Required | - |
273+
| application_name | Specifies the [Google Workspace application](https://developers.google.com/workspace/admin/reports/reference/rest/v1/activities/list#applicationname) to fetch activity data from (e.g., `saml`, `user_accounts`, `login`, `admin`, `groups`, etc). | Required | - |
274+
| user_key | The unique ID of the user to fetch activity data for | Optional | `all` |
275+
| additional_fields | Additional custom fields to add to the logs before sending to logzio | Optional | - |
276+
| days_back_fetch | The amount of days to fetch back in the first request | Optional | 1 (day) |
277+
| scrape_interval | Time interval to wait between runs (unit: `minutes`) | Optional | 1 (minute) |
278+
279+
280+
</details>
281+
<details>
282+
<summary>
283+
<span><a href="./src/apis/google/README.md#google-workspace-general">Google Workspace General API</a></span>
284+
</summary>
285+
286+
For structuring custom general Google Workspace API calls use type `google_workspace` API with the parameters below.
287+
288+
| Parameter Name | Description | Required/Optional | Default |
289+
|-----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------|--------------------------------------------------------------------|
290+
| name | Name of the API (custom name) | Optional | `Google Workspace` |
291+
| google_ws_sa_file_name | The name of the service account credentials file. **Required unless** `google_ws_sa_file_path` is set. | Required* | `""` |
292+
| google_ws_sa_file_path | The path to the service account credentials file. **Required unless** `google_ws_sa_file_name` is set. Use this if mounting the file to a different path than the default. | Optional* | `./src/shared/<google_ws_sa_file_name>` |
293+
| google_ws_delegated_account | The email of the user for which the application is requesting delegated access | Required | - |
294+
| scopes | The OAuth 2.0 scopes that you might need to request to access Google APIs | Optional | `["https://www.googleapis.com/auth/admin.reports.audit.readonly"]` |
295+
| data_request | Nest here any detail relevant to the data request. (Options in [General API](../general/README.md)) | Required | - |
296+
| additional_fields | Additional custom fields to add to the logs before sending to logzio | Optional | - |
297+
| days_back_fetch | The amount of days to fetch back in the first request | Optional | 1 (day) |
298+
| scrape_interval | Time interval to wait between runs (unit: `minutes`) | Optional | 1 (minute) |
299+
257300
</details>
258301

259302

@@ -312,6 +355,11 @@ docker stop -t 30 logzio-api-fetcher
312355
```
313356

314357
## Changelog:
358+
- **2.0.0**:
359+
- Add Google Workspace Support
360+
- Add option to configure multiple Logz.io outputs.
361+
- Bug fix for Cloudflare `next_url` to be optional.
362+
- **Note:** No breaking changes, major version bump is made to align with semantic versioning in future releases.
315363
- **0.3.1**:
316364
- Handle internal logger configuration in memory instead of disk
317365
- **0.3.0**:

e2e_tests/apis/test_e2e_google.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import json
2+
import os
3+
import time
4+
import unittest
5+
from os.path import abspath, dirname
6+
7+
from e2e_tests.api_e2e_test import ApiE2ETest
8+
from e2e_tests.utils.config_utils import update_json_tokens
9+
10+
11+
class TestGoogleE2E(ApiE2ETest):
12+
def module_specific_setup(self):
13+
secrets_map = {
14+
"private_key_id": "GOOGLE_PRIVATE_KEY_ID",
15+
"private_key": "GOOGLE_PRIVATE_KEY",
16+
"client_email": "GOOGLE_CLIENT_EMAIL",
17+
"client_id": "GOOGLE_CLIENT_ID",
18+
"client_x509_cert_url": "GOOGLE_CERT_URL",
19+
}
20+
curr_path = abspath(dirname(__file__))
21+
creds_path = f"{curr_path}/testdata/google_sa_creds.json"
22+
creds_path_temp = f"{curr_path}/testdata/google_sa_creds_temp.json"
23+
os.environ["GOOGLE_SA_CREDS"] = creds_path_temp
24+
os.environ["GOOGLE_PRIVATE_KEY"] = os.getenv("GOOGLE_PRIVATE_KEY").encode().decode("unicode_escape")
25+
update_json_tokens(creds_path, secrets_map)
26+
27+
def test_google_data_in_logz(self):
28+
secrets_map = {
29+
"apis.0.google_ws_delegated_account": "GOOGLE_DELEGATED_ACCOUNT",
30+
"logzio.token": "LOGZIO_SHIPPING_TOKEN",
31+
"apis.0.additional_fields.type": "TEST_TYPE",
32+
"apis.0.google_ws_sa_file_path": "GOOGLE_SA_CREDS"
33+
}
34+
curr_path = abspath(dirname(__file__))
35+
config_path = f"{curr_path}/testdata/google_api_conf.yaml"
36+
self.run_main_program(config_path=config_path, secrets_map=secrets_map)
37+
time.sleep(120)
38+
logs = self.search_logs(f"type:{self.test_type}")
39+
self.assertTrue(logs)
40+
41+
42+
if __name__ == '__main__':
43+
unittest.main()
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
apis:
2+
- name: google login
3+
type: google_activity
4+
google_ws_sa_file_path: ./google_sa_creds_temp.json
5+
google_ws_delegated_account: [email protected]
6+
application_name: login
7+
additional_fields:
8+
type: google_activity_shipping_test
9+
days_back_fetch: 7
10+
scrape_interval: 5
11+
12+
logzio:
13+
url: https://listener.logz.io:8071
14+
token: token
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"type": "service_account",
3+
"project_id": "core-998",
4+
"private_key_id": "<<GOOGLE_PRIVATE_KEY_ID>>",
5+
"private_key": "<<GOOGLE_PRIVATE_KEY>>",
6+
"client_email": "<<GOOGLE_CLIENT_EMAIL>>",
7+
"client_id": "<<GOOGLE_CLIENT_ID>>",
8+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
9+
"token_uri": "https://oauth2.googleapis.com/token",
10+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
11+
"client_x509_cert_url": "<<GOOGLE_CERT_URL>>",
12+
"universe_domain": "googleapis.com"
13+
}

e2e_tests/utils/config_utils.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import json
12
import os
23
import yaml
34
import glob
@@ -13,17 +14,13 @@ def validate_config_tokens(secrets_map):
1314
if os.getenv(env_var) is None:
1415
raise EnvironmentError(f"{env_var} environment variable is missing")
1516

16-
17-
def update_config_tokens(file_path, secrets_map):
17+
def _replace_key_with_env_var(secrets_map, content):
1818
"""
19-
Updates the tokens in the given file based on the provided token updates.
20-
:param file_path: Path to the configuration file.
21-
:param secrets_map: Dictionary of token updates.
22-
:return: Path to the temporary configuration file.
19+
Replaces the keys in the content with the corresponding environment variable values.
20+
:param secrets_map: Dictionary of keys and corresponding environment variable.
21+
:param content: The content to be updated.
22+
:return: Updated content.
2323
"""
24-
with open(file_path, "r") as conf:
25-
content = yaml.safe_load(conf)
26-
2724
for key, env_var in secrets_map.items():
2825
value = os.getenv(env_var)
2926
print(f"Updating {key} with {env_var}={value}")
@@ -41,6 +38,19 @@ def update_config_tokens(file_path, secrets_map):
4138
d[int(keys[-1])] = value
4239
else:
4340
d[keys[-1]] = value
41+
return content
42+
43+
def update_config_tokens(file_path, secrets_map):
44+
"""
45+
Updates the tokens in the given yaml file based on the provided token updates.
46+
:param file_path: Path to the configuration file.
47+
:param secrets_map: Dictionary of token updates.
48+
:return: Path to the temporary configuration file.
49+
"""
50+
with open(file_path, "r") as conf:
51+
content = yaml.safe_load(conf)
52+
53+
content = _replace_key_with_env_var(secrets_map, content)
4454

4555
path, ext = file_path.rsplit(".", 1)
4656
temp_test_path = f"{path}_temp.{ext}"
@@ -50,6 +60,25 @@ def update_config_tokens(file_path, secrets_map):
5060

5161
return temp_test_path
5262

63+
def update_json_tokens(file_path, secrets_map):
64+
"""
65+
Updates the tokens in the given json file based on the provided token updates.
66+
:param file_path: Path to the configuration file.
67+
:param secrets_map: Dictionary of token updates.
68+
:return: Path to the temporary configuration file.
69+
"""
70+
with open(file_path, "r") as conf:
71+
content = json.loads(conf.read())
72+
73+
content = _replace_key_with_env_var(secrets_map, content)
74+
75+
path, ext = file_path.rsplit(".", 1)
76+
temp_test_path = f"{path}_temp.{ext}"
77+
78+
with open(temp_test_path, "w") as file:
79+
json.dump(content, file)
80+
81+
return temp_test_path
5382

5483
def delete_temp_files():
5584
"""

requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
pydantic~=2.10.6
22
pyyaml~=6.0.2
33
requests~=2.32.3
4+
google~=3.0.0
5+
google-auth~=2.38.0

src/apis/azure/AzureGraph.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from src.apis.general.StopPaginationSettings import StopPaginationSettings
99

1010

11-
DATE_FROM_END_PATTERN = re.compile(r'\S+$')
12-
11+
DATE_FROM_END_PATTERN = re.compile(r'(\S+)$')
12+
DATE_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
1313

1414
logger = logging.getLogger(__name__)
1515

@@ -69,12 +69,6 @@ def send_request(self):
6969
data = super().send_request()
7070

7171
# Add 1s to the time we took from the response to avoid duplicates
72-
org_date = re.search(DATE_FROM_END_PATTERN, self.data_request.url).group(0)
73-
try:
74-
org_date = datetime.strptime(org_date, "%Y-%m-%dT%H:%M:%SZ")
75-
org_date_plus_second = (org_date + timedelta(seconds=1)).strftime("%Y-%m-%dT%H:%M:%SZ")
76-
self.data_request.url = self._replace_url_date(org_date_plus_second)
77-
except ValueError:
78-
logger.error(f"Failed to parse API {self.name} date in URL: {self.data_request.url}")
72+
self.data_request.add_seconds_to_url_date_filter(1, DATE_FORMAT, DATE_FROM_END_PATTERN)
7973

8074
return data

src/apis/cloudflare/Cloudflare.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
DATE_FILTER_PARAMETER = "since="
1212
FIND_DATE_PATTERN = re.compile(r'since=(\S+?)(?:&|$)')
13+
DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%fZ"
1314

1415
logger = logging.getLogger(__name__)
1516

@@ -61,21 +62,13 @@ def _initialize_url_date(self):
6162
self.url += f"?since={self._generate_start_fetch_date()}"
6263

6364
def _generate_start_fetch_date(self):
64-
return (datetime.now(UTC) - timedelta(days=self.days_back_fetch)).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
65+
return (datetime.now(UTC) - timedelta(days=self.days_back_fetch)).strftime(DATE_FORMAT)
6566

6667
def send_request(self):
6768
data = super().send_request()
6869

6970
# Add 1 second to a known date filter to avoid duplicates in the logs
7071
if DATE_FILTER_PARAMETER in self.url:
71-
try:
72-
org_date = re.search(FIND_DATE_PATTERN, self.url).group(1)
73-
org_date_date = datetime.strptime(org_date, "%Y-%m-%dT%H:%M:%S.%fZ")
74-
org_date_plus_second = (org_date_date + timedelta(seconds=1)).strftime("%Y-%m-%dT%H:%M:%S.%fZ")
75-
self.url = self.url.replace(org_date, org_date_plus_second)
76-
except IndexError:
77-
logger.error(f"Failed to add 1s to the {self.name} api 'since' filter value, on url {self.url}")
78-
except ValueError:
79-
logger.error(f"Failed to parse API {self.name} date in URL: {self.url}")
72+
self.add_seconds_to_url_date_filter(1, DATE_FORMAT, FIND_DATE_PATTERN)
8073

8174
return data

0 commit comments

Comments
 (0)