openrelik-api-client 0.2.3__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {openrelik_api_client-0.2.3 → openrelik_api_client-0.2.4}/PKG-INFO +3 -2
- {openrelik_api_client-0.2.3 → openrelik_api_client-0.2.4}/openrelik_api_client/api_client.py +61 -33
- openrelik_api_client-0.2.4/openrelik_api_client/folders.py +184 -0
- openrelik_api_client-0.2.4/openrelik_api_client/groups.py +190 -0
- {openrelik_api_client-0.2.3 → openrelik_api_client-0.2.4}/openrelik_api_client/workflows.py +16 -10
- {openrelik_api_client-0.2.3 → openrelik_api_client-0.2.4}/pyproject.toml +1 -1
- openrelik_api_client-0.2.3/openrelik_api_client/folders.py +0 -108
- {openrelik_api_client-0.2.3 → openrelik_api_client-0.2.4}/LICENSE +0 -0
- {openrelik_api_client-0.2.3 → openrelik_api_client-0.2.4}/README.md +0 -0
- {openrelik_api_client-0.2.3 → openrelik_api_client-0.2.4}/openrelik_api_client/__init__.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
|
-
Metadata-Version: 2.
|
1
|
+
Metadata-Version: 2.3
|
2
2
|
Name: openrelik-api-client
|
3
|
-
Version: 0.2.
|
3
|
+
Version: 0.2.4
|
4
4
|
Summary: API client that automatically handles token refresh
|
5
5
|
Author: Johan Berggren
|
6
6
|
Author-email: jberggren@gmail.com
|
@@ -9,6 +9,7 @@ Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: Programming Language :: Python :: 3.10
|
10
10
|
Classifier: Programming Language :: Python :: 3.11
|
11
11
|
Classifier: Programming Language :: Python :: 3.12
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
12
13
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
13
14
|
Requires-Dist: requests_toolbelt (>=1.0.0,<2.0.0)
|
14
15
|
Description-Content-Type: text/markdown
|
{openrelik_api_client-0.2.3 → openrelik_api_client-0.2.4}/openrelik_api_client/api_client.py
RENAMED
@@ -12,14 +12,17 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
-
import
|
16
|
-
from requests_toolbelt import MultipartEncoder
|
17
|
-
from requests.exceptions import RequestException
|
18
|
-
import requests
|
15
|
+
import math
|
19
16
|
import os
|
17
|
+
import tempfile
|
18
|
+
import time
|
20
19
|
from pathlib import Path
|
20
|
+
from typing import Any
|
21
21
|
from uuid import uuid4
|
22
|
-
|
22
|
+
|
23
|
+
import requests
|
24
|
+
from requests.exceptions import RequestException
|
25
|
+
from requests_toolbelt import MultipartEncoder
|
23
26
|
|
24
27
|
|
25
28
|
class APIClient:
|
@@ -72,6 +75,13 @@ class APIClient:
|
|
72
75
|
url = f"{self.base_url}{endpoint}"
|
73
76
|
return self.session.delete(url, **kwargs)
|
74
77
|
|
78
|
+
def get_config(self) -> dict[str, Any]:
|
79
|
+
"""Gets the current OpenRelik configuration."""
|
80
|
+
endpoint = f"{self.base_url}/config/system/"
|
81
|
+
response = self.session.get(endpoint)
|
82
|
+
response.raise_for_status()
|
83
|
+
return response.json()
|
84
|
+
|
75
85
|
def download_file(self, file_id: int, filename: str) -> str | None:
|
76
86
|
"""Downloads a file from OpenRelik.
|
77
87
|
|
@@ -92,8 +102,7 @@ class APIClient:
|
|
92
102
|
file.close()
|
93
103
|
return file.name
|
94
104
|
|
95
|
-
def upload_file(
|
96
|
-
self, file_path: str, folder_id: int) -> int | None:
|
105
|
+
def upload_file(self, file_path: str, folder_id: int) -> int | None:
|
97
106
|
"""Uploads a file to the server.
|
98
107
|
|
99
108
|
Args:
|
@@ -106,10 +115,13 @@ class APIClient:
|
|
106
115
|
Raise:
|
107
116
|
FileNotFoundError: if file_path is not found.
|
108
117
|
"""
|
118
|
+
MAX_CHUNK_RETRIES = 10 # Maximum number of retries for chunk upload
|
119
|
+
CHUNK_RETRY_INTERVAL = 0.5 # seconds
|
120
|
+
|
109
121
|
file_id = None
|
110
122
|
response = None
|
111
123
|
endpoint = "/files/upload"
|
112
|
-
chunk_size =
|
124
|
+
chunk_size = 10 * 1024 * 1024 # 10 MB
|
113
125
|
resumableTotalChunks = 0
|
114
126
|
resumableChunkNumber = 0
|
115
127
|
resumableIdentifier = uuid4().hex
|
@@ -128,30 +140,46 @@ class APIClient:
|
|
128
140
|
resumableTotalChunks = math.ceil(total_size / chunk_size)
|
129
141
|
while chunk := fh.read(chunk_size):
|
130
142
|
resumableChunkNumber += 1
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
143
|
+
retry_count = 0
|
144
|
+
while retry_count < MAX_CHUNK_RETRIES:
|
145
|
+
params = {
|
146
|
+
"resumableRelativePath": resumableFilename,
|
147
|
+
"resumableTotalSize": total_size,
|
148
|
+
"resumableCurrentChunkSize": len(chunk),
|
149
|
+
"resumableChunkSize": chunk_size,
|
150
|
+
"resumableChunkNumber": resumableChunkNumber,
|
151
|
+
"resumableTotalChunks": resumableTotalChunks,
|
152
|
+
"resumableIdentifier": resumableIdentifier,
|
153
|
+
"resumableFilename": resumableFilename,
|
154
|
+
"folder_id": folder_id,
|
155
|
+
}
|
156
|
+
encoder = MultipartEncoder(
|
157
|
+
{"file": (file_path.name, chunk, "application/octet-stream")}
|
158
|
+
)
|
159
|
+
headers = {"Content-Type": encoder.content_type}
|
160
|
+
response = self.session.post(
|
161
|
+
f"{self.base_url}{endpoint}",
|
162
|
+
headers=headers,
|
163
|
+
data=encoder.to_string(),
|
164
|
+
params=params,
|
165
|
+
)
|
166
|
+
if response.status_code == 200 or response.status_code == 201:
|
167
|
+
# Success, move to the next chunk
|
168
|
+
break
|
169
|
+
elif response.status_code == 503:
|
170
|
+
# Server has issue saving the chunk, retry the upload.
|
171
|
+
retry_count += 1
|
172
|
+
time.sleep(CHUNK_RETRY_INTERVAL)
|
173
|
+
elif response.status_code == 429:
|
174
|
+
# Rate limit exceeded, cancel the upload and raise an error.
|
175
|
+
raise RuntimeError("Upload failed, maximum retries exceeded")
|
176
|
+
else:
|
177
|
+
# Other errors, cancel the upload and raise an error.
|
178
|
+
raise RuntimeError("Upload failed")
|
179
|
+
|
153
180
|
if response and response.status_code == 201:
|
154
|
-
file_id = response.json().get(
|
181
|
+
file_id = response.json().get("id")
|
182
|
+
|
155
183
|
return file_id
|
156
184
|
|
157
185
|
|
@@ -171,7 +199,7 @@ class TokenRefreshSession(requests.Session):
|
|
171
199
|
if api_key:
|
172
200
|
self.headers["x-openrelik-refresh-token"] = api_key
|
173
201
|
|
174
|
-
def request(self, method, url, **kwargs):
|
202
|
+
def request(self, method: str, url: str, **kwargs: dict[str, Any]) -> requests.Response:
|
175
203
|
"""Intercepts the request to handle token expiration.
|
176
204
|
|
177
205
|
Args:
|
@@ -196,7 +224,7 @@ class TokenRefreshSession(requests.Session):
|
|
196
224
|
|
197
225
|
return response
|
198
226
|
|
199
|
-
def _refresh_token(self):
|
227
|
+
def _refresh_token(self) -> bool:
|
200
228
|
"""Refreshes the access token using the refresh token."""
|
201
229
|
refresh_url = f"{self.api_server_url}/auth/refresh"
|
202
230
|
try:
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# Copyright 2024 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
from typing import Any
|
16
|
+
|
17
|
+
from openrelik_api_client.api_client import APIClient
|
18
|
+
|
19
|
+
|
20
|
+
class FoldersAPI:
|
21
|
+
def __init__(self, api_client: APIClient):
|
22
|
+
super().__init__()
|
23
|
+
self.api_client = api_client
|
24
|
+
|
25
|
+
def create_root_folder(self, display_name: str) -> int | None:
|
26
|
+
"""Create a root folder.
|
27
|
+
|
28
|
+
Args:
|
29
|
+
display_name (str): Folder display name.
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
int: Folder ID for the new root folder, or None otherwise.
|
33
|
+
|
34
|
+
Raises:
|
35
|
+
HTTPError: If the API request failed.
|
36
|
+
"""
|
37
|
+
folder_id = None
|
38
|
+
endpoint = f"{self.api_client.base_url}/folders/"
|
39
|
+
params = {"display_name": display_name}
|
40
|
+
response = self.api_client.session.post(endpoint, json=params)
|
41
|
+
response.raise_for_status()
|
42
|
+
if response.status_code == 201:
|
43
|
+
folder_id = response.json().get("id")
|
44
|
+
return folder_id
|
45
|
+
|
46
|
+
def create_subfolder(self, folder_id: int, display_name: str) -> int | None:
|
47
|
+
"""Create a subfolder within the given folder ID.
|
48
|
+
|
49
|
+
Args:
|
50
|
+
folder_id: The ID of the parent folder.
|
51
|
+
display_name: The name of the subfolder to check.
|
52
|
+
|
53
|
+
Returns:
|
54
|
+
int: Folder ID for the new root folder, or None.
|
55
|
+
|
56
|
+
Raises:
|
57
|
+
HTTPError: If the API request failed.
|
58
|
+
"""
|
59
|
+
# Corrected: Use the passed folder_id in the endpoint URL
|
60
|
+
endpoint = f"{self.api_client.base_url}/folders/{folder_id}/folders/"
|
61
|
+
data = {"display_name": display_name}
|
62
|
+
response = self.api_client.session.post(endpoint, json=data)
|
63
|
+
response.raise_for_status()
|
64
|
+
# Corrected: assign to a different variable or use the return directly
|
65
|
+
new_folder_id = None
|
66
|
+
if response.status_code == 201:
|
67
|
+
new_folder_id = response.json().get("id")
|
68
|
+
return new_folder_id
|
69
|
+
|
70
|
+
def folder_exists(self, folder_id: int) -> bool:
|
71
|
+
"""Checks if a folder with the given ID exists.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
folder_id: The ID of the folder to check.
|
75
|
+
|
76
|
+
Returns:
|
77
|
+
True if the folder exists, False otherwise.
|
78
|
+
|
79
|
+
Raises:
|
80
|
+
HTTPError: If the API request failed.
|
81
|
+
"""
|
82
|
+
endpoint = f"{self.api_client.base_url}/folders/{folder_id}"
|
83
|
+
response = self.api_client.session.get(endpoint)
|
84
|
+
response.raise_for_status()
|
85
|
+
return response.status_code == 200
|
86
|
+
|
87
|
+
def update_folder(
|
88
|
+
self, folder_id: int, folder_data: dict[str, Any]
|
89
|
+
) -> dict[str, Any] | None:
|
90
|
+
"""Updates an existing folder.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
folder_id: The ID of the folder to update.
|
94
|
+
folder_data: The updated folder data.
|
95
|
+
|
96
|
+
Returns:
|
97
|
+
The updated folder data, or None.
|
98
|
+
|
99
|
+
Raises:
|
100
|
+
HTTPError: If the API request failed.
|
101
|
+
"""
|
102
|
+
endpoint = f"{self.api_client.base_url}/folders/{folder_id}"
|
103
|
+
response = self.api_client.session.patch(endpoint, json=folder_data)
|
104
|
+
response.raise_for_status()
|
105
|
+
return response.json()
|
106
|
+
|
107
|
+
def delete_folder(self, folder_id: int) -> bool:
|
108
|
+
"""Deletes an existing folder.
|
109
|
+
|
110
|
+
Args:
|
111
|
+
folder_id: The ID of the folder to update.
|
112
|
+
|
113
|
+
Returns:
|
114
|
+
True if the request was successful.
|
115
|
+
|
116
|
+
Raises:
|
117
|
+
HTTPError: If the API request failed.
|
118
|
+
"""
|
119
|
+
endpoint = f"{self.api_client.base_url}/folders/{folder_id}"
|
120
|
+
response = self.api_client.session.delete(endpoint)
|
121
|
+
response.raise_for_status()
|
122
|
+
return response.status_code == 204
|
123
|
+
|
124
|
+
def share_folder(
|
125
|
+
self,
|
126
|
+
folder_id: int,
|
127
|
+
user_names: list[str] | None = None,
|
128
|
+
group_names: list[str] | None = None,
|
129
|
+
user_ids: list[int] | None = None,
|
130
|
+
group_ids: list[int] | None = None,
|
131
|
+
user_role: str | None = None,
|
132
|
+
group_role: str | None = None,
|
133
|
+
) -> dict[str, Any] | None:
|
134
|
+
"""Shares a folder with specified users and/or groups.
|
135
|
+
|
136
|
+
The server expects all fields in the FolderShareRequest schema to be present.
|
137
|
+
If user_role or group_role are not provided, they default to "viewer".
|
138
|
+
|
139
|
+
Args:
|
140
|
+
folder_id: The ID of the folder to share.
|
141
|
+
user_names: A list of usernames to share the folder with.
|
142
|
+
group_names: A list of group names to share the folder with.
|
143
|
+
user_ids: A list of user IDs to share the folder with.
|
144
|
+
group_ids: A list of group IDs to share the folder with.
|
145
|
+
user_role: The role to assign to the specified users (e.g., "viewer", "editor").
|
146
|
+
Defaults to "viewer" if not provided.
|
147
|
+
group_role: The role to assign to the specified groups (e.g., "viewer", "editor").
|
148
|
+
Defaults to "viewer" if not provided.
|
149
|
+
|
150
|
+
Returns:
|
151
|
+
None if the sharing operation was successful. The server returns a null body
|
152
|
+
which is deserialized to None.
|
153
|
+
|
154
|
+
Raises:
|
155
|
+
requests.exceptions.HTTPError: If the API request failed (e.g., folder not found,
|
156
|
+
invalid role, permission denied).
|
157
|
+
"""
|
158
|
+
endpoint = f"{self.api_client.base_url}/folders/{folder_id}/roles"
|
159
|
+
|
160
|
+
# Ensure lists are not None for the payload
|
161
|
+
payload_user_ids = user_ids if user_ids is not None else []
|
162
|
+
payload_user_names = user_names if user_names is not None else []
|
163
|
+
payload_group_ids = group_ids if group_ids is not None else []
|
164
|
+
payload_group_names = group_names if group_names is not None else []
|
165
|
+
|
166
|
+
# Default roles to "viewer" if not provided, as the server schema requires these fields.
|
167
|
+
payload_user_role = user_role if user_role is not None else "viewer"
|
168
|
+
payload_group_role = group_role if group_role is not None else "viewer"
|
169
|
+
|
170
|
+
data = {
|
171
|
+
"user_ids": payload_user_ids,
|
172
|
+
"user_names": payload_user_names,
|
173
|
+
"group_ids": payload_group_ids,
|
174
|
+
"group_names": payload_group_names,
|
175
|
+
"user_role": payload_user_role,
|
176
|
+
"group_role": payload_group_role,
|
177
|
+
}
|
178
|
+
try:
|
179
|
+
response = self.api_client.session.post(endpoint, json=data)
|
180
|
+
response.raise_for_status()
|
181
|
+
return response.json()
|
182
|
+
except Exception as e:
|
183
|
+
print(f"An error occurred: {e.response.json()}")
|
184
|
+
return None
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
from typing import Any
|
16
|
+
|
17
|
+
from openrelik_api_client.api_client import APIClient
|
18
|
+
|
19
|
+
|
20
|
+
class GroupsAPI:
|
21
|
+
"""
|
22
|
+
Manages groups for Oppenrelik.
|
23
|
+
Provides functionalities for creating, retrieving, updating,
|
24
|
+
deleting groups, and managing group memberships.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(self, api_client: APIClient):
|
28
|
+
super().__init__()
|
29
|
+
self.api_client = api_client
|
30
|
+
self.groups_url = f"{self.api_client.base_url}/groups"
|
31
|
+
|
32
|
+
def create_group(self, name: str, description: str = "") -> dict[str, Any] | None:
|
33
|
+
"""
|
34
|
+
Creates a new group.
|
35
|
+
|
36
|
+
Args:
|
37
|
+
name: The name of the group.
|
38
|
+
description: An optional description for the group.
|
39
|
+
|
40
|
+
Returns:
|
41
|
+
A dictionary representing the newly created group if successful,
|
42
|
+
None otherwise.
|
43
|
+
|
44
|
+
Raises:
|
45
|
+
ValueError: If the group name is empty.
|
46
|
+
requests.exceptions.HTTPError: If the API request failed.
|
47
|
+
"""
|
48
|
+
if not name:
|
49
|
+
raise ValueError("Group name cannot be empty.")
|
50
|
+
|
51
|
+
endpoint = f"{self.groups_url}/"
|
52
|
+
data = {
|
53
|
+
"name": name,
|
54
|
+
"description": description,
|
55
|
+
}
|
56
|
+
response = self.api_client.session.post(endpoint, json=data)
|
57
|
+
response.raise_for_status()
|
58
|
+
if response.status_code == 201:
|
59
|
+
return response.json()
|
60
|
+
return None
|
61
|
+
|
62
|
+
def remove_group(self, group_name: str) -> bool:
|
63
|
+
"""
|
64
|
+
Removes a group by its name.
|
65
|
+
|
66
|
+
Args:
|
67
|
+
group_name: The name of the group to remove.
|
68
|
+
|
69
|
+
Returns:
|
70
|
+
True if the group was successfully removed.
|
71
|
+
|
72
|
+
Raises:
|
73
|
+
requests.exceptions.HTTPError: If the API request failed.
|
74
|
+
"""
|
75
|
+
endpoint = f"{self.groups_url}/{group_name}"
|
76
|
+
response = self.api_client.session.delete(endpoint)
|
77
|
+
response.raise_for_status()
|
78
|
+
return response.status_code == 204
|
79
|
+
|
80
|
+
def list_group_members(self, group_name: str) -> dict[str, Any] | None:
|
81
|
+
"""
|
82
|
+
Retrieves a group by its name.
|
83
|
+
|
84
|
+
Args:
|
85
|
+
group_name: The name of the group to retrieve.
|
86
|
+
|
87
|
+
Returns:
|
88
|
+
A dictionary representing the group if found, None otherwise.
|
89
|
+
|
90
|
+
Raises:
|
91
|
+
requests.exceptions.HTTPError: If the API request failed (e.g., 404 Not Found).
|
92
|
+
"""
|
93
|
+
endpoint = f"{self.groups_url}/{group_name}/users"
|
94
|
+
response = self.api_client.session.get(endpoint)
|
95
|
+
response.raise_for_status()
|
96
|
+
if response.status_code == 200:
|
97
|
+
return response.json()
|
98
|
+
return None
|
99
|
+
|
100
|
+
def list_groups(self) -> list[dict[str, Any]]:
|
101
|
+
"""
|
102
|
+
Lists all existing groups.
|
103
|
+
|
104
|
+
Returns:
|
105
|
+
A list of dictionaries, where each dictionary represents a group.
|
106
|
+
|
107
|
+
Raises:
|
108
|
+
requests.exceptions.HTTPError: If the API request failed.
|
109
|
+
"""
|
110
|
+
endpoint = f"{self.groups_url}/"
|
111
|
+
response = self.api_client.session.get(endpoint)
|
112
|
+
response.raise_for_status()
|
113
|
+
if response.status_code == 200:
|
114
|
+
return response.json()
|
115
|
+
return []
|
116
|
+
|
117
|
+
def delete_group(self, group_name: str) -> bool:
|
118
|
+
"""
|
119
|
+
Deletes a group by its name.
|
120
|
+
|
121
|
+
Args:
|
122
|
+
group_name: The name of the group to delete.
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
True if the group was successfully deleted.
|
126
|
+
|
127
|
+
Raises:
|
128
|
+
requests.exceptions.HTTPError: If the API request failed.
|
129
|
+
"""
|
130
|
+
endpoint = f"{self.groups_url}/{group_name}"
|
131
|
+
response = self.api_client.session.delete(endpoint)
|
132
|
+
response.raise_for_status()
|
133
|
+
return response.status_code == 204
|
134
|
+
|
135
|
+
def add_users_to_group(self, group_name: str, users: list[str]) -> bool:
|
136
|
+
"""
|
137
|
+
Adds a user to a group.
|
138
|
+
|
139
|
+
Args:
|
140
|
+
group_name: The name of the group.
|
141
|
+
users: The users to add.
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
List of users that were added.
|
145
|
+
|
146
|
+
Raises:
|
147
|
+
requests.exceptions.HTTPError: If the API request failed (e.g., group not found, user already member).
|
148
|
+
"""
|
149
|
+
endpoint = f"{self.groups_url}/{group_name}/users/"
|
150
|
+
response = self.api_client.session.post(endpoint, json=users)
|
151
|
+
response.raise_for_status()
|
152
|
+
return response.json()
|
153
|
+
|
154
|
+
def remove_users_from_group(self, group_name: int, users: list[str]) -> bool:
|
155
|
+
"""
|
156
|
+
Removes a user from a group.
|
157
|
+
|
158
|
+
Args:
|
159
|
+
group_name: The name of the group.
|
160
|
+
users: The users to remove.
|
161
|
+
|
162
|
+
Returns:
|
163
|
+
List of users that were removed.
|
164
|
+
|
165
|
+
Raises:
|
166
|
+
requests.exceptions.HTTPError: If the API request failed (e.g., group/user not found).
|
167
|
+
"""
|
168
|
+
endpoint = f"{self.groups_url}/{group_name}/users"
|
169
|
+
response = self.api_client.session.delete(endpoint, json=users)
|
170
|
+
response.raise_for_status()
|
171
|
+
return response.json()
|
172
|
+
|
173
|
+
def is_member(self, group_name: str, user_name: str) -> bool:
|
174
|
+
"""
|
175
|
+
Checks if a user is a member of a specific group.
|
176
|
+
|
177
|
+
Args:
|
178
|
+
group_name: The ID of the group.
|
179
|
+
user_name: The ID of the user.
|
180
|
+
|
181
|
+
Returns:
|
182
|
+
True if the user is a member, False otherwise.
|
183
|
+
"""
|
184
|
+
endpoint = f"{self.groups_url}/{group_name}/users/"
|
185
|
+
response = self.api_client.session.get(endpoint)
|
186
|
+
username = None
|
187
|
+
response.raise_for_status() # Raises HTTPError for 4xx/5xx
|
188
|
+
for user in response.json():
|
189
|
+
username = user["username"]
|
190
|
+
return user_name == username
|
@@ -13,6 +13,7 @@
|
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
15
|
import json
|
16
|
+
from typing import Any
|
16
17
|
|
17
18
|
from openrelik_api_client.api_client import APIClient
|
18
19
|
|
@@ -24,7 +25,8 @@ class WorkflowsAPI:
|
|
24
25
|
self.folders_url = f"{self.api_client.base_url}/folders"
|
25
26
|
|
26
27
|
def create_workflow(
|
27
|
-
|
28
|
+
self, folder_id: int, file_ids: list, template_id: int = None
|
29
|
+
) -> int | None:
|
28
30
|
"""Creates a new workflow.
|
29
31
|
|
30
32
|
Args:
|
@@ -40,15 +42,18 @@ class WorkflowsAPI:
|
|
40
42
|
"""
|
41
43
|
workflow_id = None
|
42
44
|
endpoint = f"{self.folders_url}/{folder_id}/workflows/"
|
43
|
-
data = {
|
44
|
-
|
45
|
+
data = {
|
46
|
+
"folder_id": folder_id,
|
47
|
+
"file_ids": file_ids,
|
48
|
+
"template_id": template_id,
|
49
|
+
}
|
45
50
|
response = self.api_client.session.post(endpoint, json=data)
|
46
51
|
response.raise_for_status()
|
47
52
|
if response.status_code == 200:
|
48
53
|
workflow_id = response.json().get("id")
|
49
54
|
return workflow_id
|
50
55
|
|
51
|
-
def get_workflow(self, folder_id: int, workflow_id: int):
|
56
|
+
def get_workflow(self, folder_id: int, workflow_id: int) -> dict[str, Any]:
|
52
57
|
"""Retrieves a workflow by ID.
|
53
58
|
|
54
59
|
Args:
|
@@ -67,7 +72,9 @@ class WorkflowsAPI:
|
|
67
72
|
if response.status_code == 200:
|
68
73
|
return response.json()
|
69
74
|
|
70
|
-
def update_workflow(
|
75
|
+
def update_workflow(
|
76
|
+
self, folder_id: int, workflow_id: int, workflow_data: dict
|
77
|
+
) -> dict[str, Any] | None:
|
71
78
|
"""Updates an existing workflow.
|
72
79
|
|
73
80
|
Args:
|
@@ -107,7 +114,7 @@ class WorkflowsAPI:
|
|
107
114
|
response.raise_for_status()
|
108
115
|
return response.status_code == 204
|
109
116
|
|
110
|
-
def run_workflow(self, folder_id: int, workflow_id: int):
|
117
|
+
def run_workflow(self, folder_id: int, workflow_id: int) -> dict[str, Any] | None:
|
111
118
|
"""Runs an existing workflow.
|
112
119
|
|
113
120
|
Args:
|
@@ -122,11 +129,10 @@ class WorkflowsAPI:
|
|
122
129
|
HTTPError: If the API request failed.
|
123
130
|
"""
|
124
131
|
workflow = None
|
125
|
-
endpoint =
|
126
|
-
f"{self.folders_url}/{folder_id}/workflows/{workflow_id}/run/")
|
132
|
+
endpoint = f"{self.folders_url}/{folder_id}/workflows/{workflow_id}/run/"
|
127
133
|
workflow = self.get_workflow(folder_id, workflow_id)
|
128
|
-
spec = json.loads(workflow.get(
|
129
|
-
data = {
|
134
|
+
spec = json.loads(workflow.get("spec_json"))
|
135
|
+
data = {"workflow_spec": spec}
|
130
136
|
response = self.api_client.session.post(endpoint, json=data)
|
131
137
|
response.raise_for_status()
|
132
138
|
if response.status_code == 200:
|
@@ -1,108 +0,0 @@
|
|
1
|
-
# Copyright 2024 Google LLC
|
2
|
-
#
|
3
|
-
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
-
# you may not use this file except in compliance with the License.
|
5
|
-
# You may obtain a copy of the License at
|
6
|
-
#
|
7
|
-
# https://www.apache.org/licenses/LICENSE-2.0
|
8
|
-
#
|
9
|
-
# Unless required by applicable law or agreed to in writing, software
|
10
|
-
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
-
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
-
# See the License for the specific language governing permissions and
|
13
|
-
# limitations under the License.
|
14
|
-
|
15
|
-
from typing import Any
|
16
|
-
|
17
|
-
from openrelik_api_client.api_client import APIClient
|
18
|
-
|
19
|
-
|
20
|
-
class FoldersAPI:
|
21
|
-
|
22
|
-
def __init__(self, api_client: APIClient):
|
23
|
-
super().__init__()
|
24
|
-
self.api_client = api_client
|
25
|
-
|
26
|
-
def create_root_folder(self, display_name: str) -> int | None:
|
27
|
-
"""Create a root folder.
|
28
|
-
|
29
|
-
Args:
|
30
|
-
display_name (str): Folder display name.
|
31
|
-
|
32
|
-
Returns:
|
33
|
-
int: Folder ID for the new root folder, or None otherwise.
|
34
|
-
|
35
|
-
Raises:
|
36
|
-
HTTPError: If the API request failed.
|
37
|
-
"""
|
38
|
-
folder_id = None
|
39
|
-
endpoint = f"{self.api_client.base_url}/folders/"
|
40
|
-
params = {"display_name": display_name}
|
41
|
-
response = self.api_client.session.post(endpoint, json=params)
|
42
|
-
response.raise_for_status()
|
43
|
-
if response.status_code == 201:
|
44
|
-
folder_id = response.json().get('id')
|
45
|
-
return folder_id
|
46
|
-
|
47
|
-
def create_subfolder(
|
48
|
-
self, folder_id: int, display_name: str) -> int | None:
|
49
|
-
"""Create a subfolder within the given folder ID.
|
50
|
-
|
51
|
-
Args:
|
52
|
-
folder_id: The ID of the parent folder.
|
53
|
-
display_name: The name of the subfolder to check.
|
54
|
-
|
55
|
-
Returns:
|
56
|
-
int: Folder ID for the new root folder, or None.
|
57
|
-
|
58
|
-
Raises:
|
59
|
-
HTTPError: If the API request failed.
|
60
|
-
"""
|
61
|
-
folder_id = None
|
62
|
-
endpoint = f"{self.api_client.base_url}/folders/{folder_id}/folders"
|
63
|
-
data = {"display_name": display_name}
|
64
|
-
response = self.api_client.session.post(endpoint, json=data)
|
65
|
-
response.raise_for_status()
|
66
|
-
if response.status_code == 201:
|
67
|
-
folder_id = response.json().get("id")
|
68
|
-
return folder_id
|
69
|
-
|
70
|
-
def folder_exists(self, folder_id: int) -> bool:
|
71
|
-
"""Checks if a folder with the given ID exists.
|
72
|
-
|
73
|
-
Args:
|
74
|
-
folder_id: The ID of the folder to check.
|
75
|
-
|
76
|
-
Returns:
|
77
|
-
True if the folder exists, False otherwise.
|
78
|
-
|
79
|
-
Raises:
|
80
|
-
HTTPError: If the API request failed.
|
81
|
-
"""
|
82
|
-
endpoint = f"{self.api_client.base_url}/folders/{folder_id}"
|
83
|
-
response = self.api_client.session.get(endpoint)
|
84
|
-
response.raise_for_status()
|
85
|
-
return response.status_code == 200
|
86
|
-
|
87
|
-
def update_folder(
|
88
|
-
self, folder_id: int, folder_data: dict[str, Any]
|
89
|
-
):
|
90
|
-
"""Updates an existing folder.
|
91
|
-
|
92
|
-
Args:
|
93
|
-
folder_id: The ID of the folder to update.
|
94
|
-
folder_data: The updated folder data.
|
95
|
-
|
96
|
-
Returns:
|
97
|
-
The updated folder data, or None.
|
98
|
-
|
99
|
-
Raises:
|
100
|
-
HTTPError: If the API request failed.
|
101
|
-
"""
|
102
|
-
endpoint = f"{self.api_client.base_url}/folders/{folder_id}"
|
103
|
-
response = self.api_client.session.patch(
|
104
|
-
endpoint,
|
105
|
-
json=folder_data
|
106
|
-
)
|
107
|
-
response.raise_for_status()
|
108
|
-
return response.json()
|
File without changes
|
File without changes
|
File without changes
|