mantis_api_client 5.5.0__py3-none-any.whl

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.
@@ -0,0 +1,186 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (c) 2016-2021 AMOSSYS
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #
24
+ import argparse
25
+ import sys
26
+ from typing import List
27
+
28
+ import argcomplete
29
+ from omegaconf import OmegaConf
30
+ from rich_argparse import RichHelpFormatter
31
+
32
+ from mantis_api_client.cli_parser.account_parser import add_account_parser
33
+ from mantis_api_client.cli_parser.attack_parser import add_attack_parser
34
+ from mantis_api_client.cli_parser.basebox_parser import add_basebox_parser
35
+ from mantis_api_client.cli_parser.dataset_parser import add_dataset_parser
36
+ from mantis_api_client.cli_parser.lab_parser import add_lab_parser
37
+ from mantis_api_client.cli_parser.labs_parser import add_labs_parser
38
+ from mantis_api_client.cli_parser.log_collector_parser import add_log_collector_parser
39
+ from mantis_api_client.cli_parser.scenario_parser import add_scenario_parser
40
+ from mantis_api_client.cli_parser.signature_parser import add_signature_parser
41
+ from mantis_api_client.cli_parser.topology_parser import add_topology_parser
42
+ from mantis_api_client.cli_parser.user_parser import add_user_parser
43
+ from mantis_api_client.cli_parser.bas_parser import add_bas_parser
44
+ from mantis_api_client.config import mantis_api_client_config
45
+ from mantis_api_client.oidc import initialize_oidc_client
46
+ from mantis_api_client.utils import colored
47
+
48
+
49
+ def create_mantis_cli_parser() -> argparse.ArgumentParser:
50
+ parser = argparse.ArgumentParser(formatter_class=RichHelpFormatter)
51
+
52
+ # Config file argument
53
+ parser.add_argument("--config", help="Configuration file")
54
+
55
+ # Common debug argument
56
+ parser.add_argument(
57
+ "-d",
58
+ "--debug",
59
+ action="store_true",
60
+ dest="debug_mode",
61
+ help="Activate debug mode (default: %(default)s)",
62
+ )
63
+
64
+ subparsers = parser.add_subparsers()
65
+
66
+ # --------------------
67
+ # --- Scenario API options (bas)
68
+ # --------------------
69
+
70
+ add_bas_parser(root_parser=parser, subparsers=subparsers)
71
+
72
+ # --------------------
73
+ # --- Scenario API options (attack)
74
+ # --------------------
75
+
76
+ add_attack_parser(root_parser=parser, subparsers=subparsers)
77
+
78
+ # --------------------
79
+ # --- Scenario API options (basebox)
80
+ # --------------------
81
+
82
+ add_basebox_parser(root_parser=parser, subparsers=subparsers)
83
+
84
+ # --------------------
85
+ # --- Scenario API options (dataset)
86
+ # --------------------
87
+
88
+ add_dataset_parser(root_parser=parser, subparsers=subparsers)
89
+
90
+ # --------------------
91
+ # --- Scenario API options (scenario)
92
+ # --------------------
93
+
94
+ add_scenario_parser(root_parser=parser, subparsers=subparsers)
95
+
96
+ # --------------------
97
+ # --- Scenario API options (topology)
98
+ # --------------------
99
+
100
+ add_topology_parser(root_parser=parser, subparsers=subparsers)
101
+
102
+ # --------------------
103
+ # --- Scenario API options (log_collector)
104
+ # --------------------
105
+
106
+ add_log_collector_parser(root_parser=parser, subparsers=subparsers)
107
+
108
+ # --------------------
109
+ # --- Scenario API options (signature)
110
+ # --------------------
111
+
112
+ add_signature_parser(root_parser=parser, subparsers=subparsers)
113
+
114
+ # --------------------
115
+ # --- User API options
116
+ # --------------------
117
+
118
+ add_user_parser(root_parser=parser, subparsers=subparsers)
119
+
120
+ # -------------------
121
+ # --- Labs subparser
122
+ # -------------------
123
+
124
+ add_labs_parser(root_parser=parser, subparsers=subparsers)
125
+
126
+ # -------------------
127
+ # --- Lab subparser
128
+ # -------------------
129
+
130
+ add_lab_parser(root_parser=parser, subparsers=subparsers)
131
+
132
+ # -------------------
133
+ # --- login options
134
+ # -------------------
135
+
136
+ add_account_parser(root_parser=parser, subparsers=subparsers)
137
+
138
+ return parser
139
+
140
+
141
+ def handle_command_line(command_line_args: List[str]) -> None:
142
+ try:
143
+ initialize_oidc_client()
144
+ except Exception as e:
145
+ print(colored(str(e), "white", "on_red"))
146
+ sys.exit(1)
147
+
148
+ parser = create_mantis_cli_parser()
149
+
150
+ argcomplete.autocomplete(parser)
151
+
152
+ parser_defaults = {
153
+ k: mantis_api_client_config[k]
154
+ for k in mantis_api_client_config
155
+ if not OmegaConf.is_missing(mantis_api_client_config, k)
156
+ }
157
+ parser.set_defaults(
158
+ func=lambda _: parser.print_help(), **parser_defaults
159
+ ) # all arguments must be set at same time
160
+
161
+ args, left_argv = parser.parse_known_args(command_line_args)
162
+
163
+ # Parse remaining args from command line (overriding potential config file
164
+ # parameters)
165
+ args = parser.parse_args(left_argv, args)
166
+
167
+ # If in a 'lab' namespace, we set specific environment variables
168
+ # to allow remote access to the running lab
169
+ if hasattr(args, "set_lab"):
170
+ args.set_lab(args)
171
+
172
+ args.func(args)
173
+
174
+ if hasattr(args, "set_lab"):
175
+ args.unset_lab(args)
176
+
177
+ sys.exit(0)
178
+
179
+
180
+ def main() -> None:
181
+ command_line_args = sys.argv[1:]
182
+ handle_command_line(command_line_args)
183
+
184
+
185
+ if __name__ == "__main__":
186
+ main()
@@ -0,0 +1,302 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright (c) 2016-2021 AMOSSYS
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in all
14
+ # copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ # SOFTWARE.
23
+ #
24
+ import json
25
+ import sys
26
+ from pathlib import Path
27
+ from typing import Any
28
+ from typing import Callable
29
+ from typing import Dict
30
+ from typing import Optional
31
+ from typing import TypeVar
32
+ from typing import Union
33
+
34
+ from mantis_authz import MantisOpenID
35
+ from mantis_authz.config import authz_config
36
+ from pydantic import BaseModel
37
+ from pydantic import Field
38
+ from pydantic import ValidationError
39
+ from rich.prompt import Confirm
40
+ from ruamel.yaml import YAML
41
+
42
+ from mantis_api_client.config import mantis_api_client_config
43
+
44
+ RT = TypeVar("RT")
45
+
46
+ oidc_client = None
47
+
48
+
49
+ class ConfigurationError(ValueError):
50
+ pass
51
+
52
+
53
+ class Profile(BaseModel):
54
+ domain: str
55
+ refresh_token: str
56
+ default_workspace: Optional[str] = None
57
+
58
+
59
+ class Session(BaseModel):
60
+ active_profile: Optional[str] = None
61
+ profiles: Dict[str, Profile] = Field(default_factory=dict)
62
+
63
+ def get_active_profile(self, raise_exc: bool = True) -> Optional[Profile]:
64
+ if self.active_profile is None:
65
+ if raise_exc:
66
+ raise ConfigurationError(
67
+ "Not authenticated, you need to execute 'mantis account login"
68
+ )
69
+ return None
70
+ return self.profiles[self.active_profile]
71
+
72
+ def set_active_profile(self, name: str) -> None:
73
+ if name not in self.profiles:
74
+ raise ValueError(f"Unknown profile '{name}'")
75
+ self.active_profile = name
76
+
77
+
78
+ class ConfigFileManager:
79
+ def __init__(self, path: Union[str, Path]) -> None:
80
+ if isinstance(path, str):
81
+ path = Path(path)
82
+ if path.suffix in (".yaml", ".yml"):
83
+ klass = YAML()
84
+ loader = klass.load
85
+ dumper = klass.dump
86
+ # elif path.suffix == ".json":
87
+ # loader = json.load
88
+ # dumper = json.dump
89
+ else:
90
+ raise ValueError(f"Unknown file suffix '{path.suffix}'")
91
+ self.loader = loader
92
+ self.dumper = dumper
93
+ self.path = path
94
+
95
+ def load(self) -> Dict:
96
+ return self.loader(self.path) or {}
97
+
98
+ def dump(self, obj) -> None:
99
+ self.dumper(obj, self.path)
100
+
101
+
102
+ class OIDCClient:
103
+ """
104
+ Provide a wrapper arround OIDC client providing token refreshing for API
105
+ calls.
106
+ """
107
+
108
+ def __init__(self) -> None:
109
+ try:
110
+ self._session = self._parse_session_from_config()
111
+ except FileNotFoundError:
112
+ self._session = Session()
113
+ self.set_context()
114
+ self._oidc: Optional[MantisOpenID] = self._get_oidc()
115
+ self._access_token: Optional[str] = None
116
+
117
+ def get_active_profile_domain(self, **kwargs) -> Optional[str]:
118
+ active_profile = self._session.get_active_profile(**kwargs)
119
+ if active_profile is None:
120
+ return None
121
+ return active_profile.domain
122
+
123
+ def get_default_workspace(self, **kwargs) -> Optional[str]:
124
+ active_profile = self._session.get_active_profile(**kwargs)
125
+ if active_profile is None:
126
+ return None
127
+ return active_profile.default_workspace
128
+
129
+ @staticmethod
130
+ def _parse_session_from_config() -> Session:
131
+ obj = ConfigFileManager(mantis_api_client_config.config_file).load()
132
+ try:
133
+ session = Session(
134
+ active_profile=obj.get("active_profile"),
135
+ profiles=obj.get("profiles", {}),
136
+ )
137
+ except ValidationError:
138
+ exit("Configuration format is incorrect.\nRun `mantis init` again")
139
+ if (
140
+ session.active_profile is not None
141
+ and session.active_profile not in session.profiles
142
+ ):
143
+ raise ConfigurationError(
144
+ "Unknown default profile '{}'".format(obj["active_profile"])
145
+ )
146
+ return session
147
+
148
+ def _store_session_to_config(self) -> None:
149
+ cfgmgr = ConfigFileManager(mantis_api_client_config.config_file)
150
+ cfgmgr.dump(self._session.dict())
151
+
152
+ def _get_oidc(self, domain: Optional[str] = None) -> Optional[MantisOpenID]:
153
+ mantis_api_client_config.config_file.touch(exist_ok=True)
154
+
155
+ if domain is None:
156
+ # return early when no session is available
157
+ if self._session.active_profile is None:
158
+ return None
159
+ active_profile = self._session.get_active_profile()
160
+ assert active_profile is not None
161
+ domain = active_profile.domain
162
+ else:
163
+ self.set_context(profile=domain)
164
+ return MantisOpenID(
165
+ server_url=mantis_api_client_config.oidc.server_url,
166
+ realm_name=mantis_api_client_config.oidc.realm,
167
+ client_id=mantis_api_client_config.oidc.client_id,
168
+ )
169
+
170
+ def set_context(self, profile: Optional[str] = None) -> None:
171
+ if profile is None:
172
+ profile = self._session.active_profile
173
+ if profile is None: # Production configuration
174
+ profile = "mantis-platform.io"
175
+ if profile:
176
+ # prod mode
177
+ mantis_domain = profile
178
+ app_base_url = f"https://app.{mantis_domain}"
179
+ id_base_url = f"https://id.{mantis_domain}"
180
+ mantis_api_client_config.update(
181
+ {
182
+ "dataset_api_url": f"{app_base_url}/api/dataset",
183
+ "dataset_web_url": f"{app_base_url}/datasets/resources",
184
+ "scenario_api_url": f"{app_base_url}/api/scenario",
185
+ "user_api_url": f"{app_base_url}/api/backoffice",
186
+ }
187
+ )
188
+ mantis_api_client_config.oidc.server_url = id_base_url
189
+ del app_base_url, id_base_url
190
+ authz_config.use_permissions = True
191
+
192
+ def configure_profile(
193
+ self,
194
+ domain: Optional[str] = None,
195
+ token: Optional[str] = None,
196
+ workspace_id: Optional[str] = None,
197
+ ) -> None:
198
+ profile_name = domain
199
+ if profile_name is None:
200
+ # remove profile
201
+ if self._session.active_profile is not None:
202
+ self._session.profiles.pop(self._session.active_profile, None)
203
+ else:
204
+ assert domain is not None and token is not None
205
+ profile = Profile(
206
+ domain=domain,
207
+ refresh_token=token,
208
+ default_workspace=workspace_id,
209
+ )
210
+ self._session.profiles[profile_name] = profile
211
+ self._oidc = self._get_oidc(domain)
212
+ self._session.active_profile = profile_name
213
+ self.set_context(profile_name)
214
+ self._store_session_to_config()
215
+
216
+ def get_active_tokens(self) -> Dict:
217
+ active_profile = self._session.get_active_profile()
218
+ assert active_profile is not None and self._oidc is not None
219
+ return self._oidc.refresh_token(active_profile.refresh_token)
220
+
221
+ def token(self, domain: str, *args, **kwargs):
222
+ oidc = self._get_oidc(domain)
223
+ assert oidc is not None
224
+ return oidc.token(*args, **kwargs)
225
+
226
+ def auth_url(self, domain: str, *args, **kwargs):
227
+ oidc = self._get_oidc(domain)
228
+ assert oidc is not None
229
+ return oidc.auth_url(*args, **kwargs)
230
+
231
+
232
+ def authorize(func: Callable[..., RT]) -> Callable[..., RT]:
233
+ def authorize_wrapper(route: str, **kwargs: Any) -> Any:
234
+ tokens = get_oidc_client().get_active_tokens()
235
+ access_token = tokens["access_token"]
236
+ kwargs.setdefault("headers", {}).update(
237
+ {"Authorization": f"Bearer {access_token}"}
238
+ )
239
+ return func(route, **kwargs)
240
+
241
+ if authz_config.use_permissions:
242
+ return authorize_wrapper
243
+ return func
244
+
245
+
246
+ def _migrate_legacy_session_file() -> None:
247
+ config_file = mantis_api_client_config.config_file
248
+ legacy_config_file = config_file.parent / "session.json"
249
+ if legacy_config_file.exists() and not config_file.exists():
250
+ print(f"Legacy configuration file found at `{legacy_config_file}`")
251
+ confirm = Confirm.ask("Do you want to convert it automatically ?", default=True)
252
+ if confirm:
253
+ try:
254
+ with legacy_config_file.open() as f:
255
+ legacy_config = json.load(f)
256
+ with config_file.open("w") as f:
257
+ YAML().dump(_convert_legacy_config_file(legacy_config), f)
258
+ legacy_config_file.unlink()
259
+ except Exception:
260
+ if Confirm.ask(
261
+ "Migration failed. You should remove the configuration file. Proceed ?",
262
+ default=True,
263
+ ):
264
+ legacy_config_file.unlink()
265
+ else:
266
+ print(f"Migration completed. New configuration file is `{config_file}`")
267
+
268
+
269
+ def _convert_legacy_config_file(legacy_config: Dict) -> Dict:
270
+ return {
271
+ "active_profile": legacy_config.pop("active_profile")[11:],
272
+ "profiles": {
273
+ domain[11:]: {"domain": domain[11:], **conf}
274
+ for domain, conf in legacy_config.items()
275
+ },
276
+ }
277
+
278
+
279
+ def initialize_oidc_client() -> None:
280
+ global oidc_client
281
+ mantis_api_client_config.config_file.parent.mkdir(parents=True, exist_ok=True)
282
+ _migrate_legacy_session_file()
283
+ oidc_client = OIDCClient()
284
+
285
+
286
+ def get_oidc_client() -> OIDCClient:
287
+ if oidc_client is None:
288
+ try:
289
+ initialize_oidc_client()
290
+ except Exception as e:
291
+ from mantis_api_client.utils import colored
292
+
293
+ print(colored(str(e), "white", "on_red"))
294
+ sys.exit(1)
295
+
296
+ assert oidc_client is not None
297
+ return oidc_client
298
+
299
+
300
+ # Call OIDCClient initialization once here, to prevent some internal initialization issues
301
+ # like authz_config.use_permissions not being set properly
302
+ get_oidc_client()