locust-cloud 1.12.4__py3-none-any.whl → 1.13.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.
locust_cloud/auth.py DELETED
@@ -1,443 +0,0 @@
1
- import logging
2
- import os
3
- from datetime import UTC, datetime, timedelta
4
- from typing import Any, TypedDict, cast
5
-
6
- import locust.env
7
- import requests
8
- import werkzeug
9
- from flask import Blueprint, redirect, request, session, url_for
10
- from flask_login import UserMixin, login_required, login_user, logout_user
11
- from locust.html import render_template_from
12
- from locust_cloud import __version__
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- class Credentials(TypedDict):
18
- user_sub_id: str
19
- refresh_token: str
20
-
21
-
22
- class AuthUser(UserMixin):
23
- def __init__(self, user_sub_id: str):
24
- self.user_sub_id = user_sub_id
25
-
26
- def get_id(self):
27
- return self.user_sub_id
28
-
29
-
30
- def set_credentials(username: str, credentials: Credentials, response: werkzeug.wrappers.response.Response):
31
- if not credentials.get("user_sub_id"):
32
- return response
33
-
34
- user_sub_id = credentials["user_sub_id"]
35
- refresh_token = credentials["refresh_token"]
36
-
37
- response.set_cookie("username", username, expires=datetime.now(tz=UTC) + timedelta(days=365))
38
- response.set_cookie("user_token", refresh_token, expires=datetime.now(tz=UTC) + timedelta(days=365))
39
- response.set_cookie("user_sub_id", user_sub_id, expires=datetime.now(tz=UTC) + timedelta(days=365))
40
-
41
- return response
42
-
43
-
44
- def register_auth(environment: locust.env.Environment):
45
- environment.web_ui.app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "") + os.getenv("CUSTOMER_ID", "")
46
- environment.web_ui.app.debug = False
47
-
48
- web_base_path = environment.parsed_options.web_base_path
49
- auth_blueprint = Blueprint("locust_cloud_auth", __name__, url_prefix=web_base_path)
50
-
51
- def load_user(user_sub_id: str):
52
- username = request.cookies.get("username")
53
- refresh_token = request.cookies.get("user_token")
54
-
55
- if refresh_token:
56
- environment.web_ui.template_args["username"] = username
57
- return AuthUser(user_sub_id)
58
-
59
- return None
60
-
61
- environment.web_ui.login_manager.user_loader(load_user)
62
- environment.web_ui.auth_args = cast(
63
- Any,
64
- {
65
- "username_password_callback": f"{web_base_path}/authenticate",
66
- },
67
- )
68
-
69
- environment.web_ui.auth_args["auth_providers"] = []
70
- if environment.parsed_options.allow_signup:
71
- environment.web_ui.auth_args["auth_providers"].append(
72
- {"label": "Sign Up", "callback_url": f"{web_base_path}/signup"}
73
- )
74
- if environment.parsed_options.allow_forgot_password:
75
- environment.web_ui.auth_args["auth_providers"].append(
76
- {"label": "Forgot Password?", "callback_url": f"{web_base_path}/forgot-password"}
77
- )
78
-
79
- @auth_blueprint.route("/authenticate", methods=["POST"])
80
- def login_submit():
81
- username = request.form.get("username", "")
82
- password = request.form.get("password")
83
-
84
- try:
85
- auth_response = requests.post(
86
- f"{environment.parsed_options.deployer_url}/auth/login",
87
- json={"username": username, "password": password},
88
- headers={"X-Client-Version": __version__},
89
- )
90
-
91
- auth_response.raise_for_status()
92
-
93
- credentials = auth_response.json()
94
-
95
- if credentials.get("challenge_session"):
96
- session["challenge_session"] = credentials.get("challenge_session")
97
- session["username"] = username
98
-
99
- session["auth_error"] = ""
100
-
101
- return redirect(url_for("locust_cloud_auth.password_reset"))
102
- if os.getenv("CUSTOMER_ID", "") and credentials.get("customer_id") != os.getenv("CUSTOMER_ID", ""):
103
- session["auth_error"] = "Invalid login for this deployment"
104
- return redirect(url_for("locust.login"))
105
-
106
- if not credentials.get("user_sub_id"):
107
- session["auth_error"] = "Unknown error during authentication, check logs and/or contact support"
108
- return redirect(url_for("locust.login"))
109
-
110
- response = redirect(url_for("locust.index"))
111
- response = set_credentials(username, credentials, response)
112
- login_user(AuthUser(credentials["user_sub_id"]))
113
-
114
- return response
115
- except requests.exceptions.HTTPError as e:
116
- if e.response.status_code == 401:
117
- session["auth_error"] = "Invalid username or password"
118
- else:
119
- logger.error(f"Unknown response from auth: {e.response.status_code} {e.response.text}")
120
-
121
- session["auth_error"] = "Unknown error during authentication, check logs and/or contact support"
122
-
123
- return redirect(url_for("locust.login"))
124
-
125
- @auth_blueprint.route("/signup")
126
- def signup():
127
- if not environment.parsed_options.allow_signup:
128
- return redirect(url_for("locust.login"))
129
-
130
- if session.get("username"):
131
- sign_up_args = {
132
- "custom_form": {
133
- "inputs": [
134
- {
135
- "label": "Confirmation Code",
136
- "name": "confirmation_code",
137
- "is_required": True,
138
- },
139
- ],
140
- "callback_url": f"{web_base_path}/confirm-signup",
141
- "submit_button_text": "Confirm Email",
142
- },
143
- }
144
- else:
145
- sign_up_args = {
146
- "custom_form": {
147
- "inputs": [
148
- {
149
- "label": "Username",
150
- "name": "username",
151
- "is_required": True,
152
- "type": "email",
153
- },
154
- {
155
- "label": "Full Name",
156
- "name": "customer_name",
157
- "is_required": True,
158
- },
159
- {
160
- "label": "Password",
161
- "name": "password",
162
- "is_secret": True,
163
- "is_required": True,
164
- },
165
- {
166
- "label": "Access Code",
167
- "name": "access_code",
168
- "is_required": True,
169
- },
170
- {
171
- "label": "I consent to:\n\n1. Only test your own website/service or our example target\n\n2. Only use locust-cloud for its intended purpose: to load test other sites/services.\n\n3. Not attempt to circumvent your account limitations (e.g. max user count or max request count)\n\n4. Not use personal data (real names, addresses etc) in your tests.",
172
- "name": "consent",
173
- "default_value": False,
174
- "is_required": True,
175
- },
176
- ],
177
- "callback_url": f"{web_base_path}/create-account",
178
- "submit_button_text": "Sign Up",
179
- },
180
- }
181
-
182
- if session.get("auth_info"):
183
- sign_up_args["info"] = session["auth_info"]
184
- if session.get("auth_sign_up_error"):
185
- sign_up_args["error"] = session["auth_sign_up_error"]
186
-
187
- return render_template_from(
188
- "auth.html",
189
- auth_args=sign_up_args,
190
- )
191
-
192
- @auth_blueprint.route("/create-account", methods=["POST"])
193
- def create_account():
194
- if not environment.parsed_options.allow_signup:
195
- return redirect(url_for("locust.login"))
196
-
197
- session["auth_sign_up_error"] = ""
198
- session["auth_info"] = ""
199
-
200
- username = request.form.get("username", "")
201
- customer_name = request.form.get("customer_name", "")
202
- password = request.form.get("password")
203
- access_code = request.form.get("access_code")
204
-
205
- try:
206
- auth_response = requests.post(
207
- f"{environment.parsed_options.deployer_url}/auth/signup",
208
- json={"username": username, "password": password, "access_code": access_code},
209
- )
210
-
211
- auth_response.raise_for_status()
212
-
213
- session["user_sub_id"] = auth_response.json().get("user_sub_id")
214
- session["username"] = username
215
- session["customer_name"] = customer_name
216
- session["auth_info"] = (
217
- "Please check your email and enter the confirmation code. If you didn't get a code after one minute, you can [request a new one](/resend-code)"
218
- )
219
-
220
- return redirect(url_for("locust_cloud_auth.signup"))
221
- except requests.exceptions.HTTPError as e:
222
- message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
223
- session["auth_info"] = ""
224
- session["auth_sign_up_error"] = message
225
-
226
- return redirect(url_for("locust_cloud_auth.signup"))
227
-
228
- @auth_blueprint.route("/resend-code")
229
- def resend_code():
230
- if not session.get("username"):
231
- session["auth_sign_up_error"] = "An unexpected error occured. Please try again."
232
- return redirect(url_for("locust_cloud_auth.signup"))
233
-
234
- try:
235
- auth_response = requests.post(
236
- f"{environment.parsed_options.deployer_url}/auth/resend-confirmation",
237
- json={"username": session.get("username")},
238
- )
239
-
240
- auth_response.raise_for_status()
241
-
242
- session["auth_sign_up_error"] = ""
243
- session["auth_info"] = "Confirmation code sent, please check your email."
244
-
245
- return redirect(url_for("locust_cloud_auth.signup"))
246
- except requests.exceptions.HTTPError as e:
247
- message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
248
- session["auth_info"] = ""
249
- session["auth_sign_up_error"] = message
250
-
251
- return redirect(url_for("locust_cloud_auth.signup"))
252
-
253
- @auth_blueprint.route("/confirm-signup", methods=["POST"])
254
- def confirm_signup():
255
- if not environment.parsed_options.allow_signup:
256
- return redirect(url_for("locust.login"))
257
- if not session.get("user_sub_id"):
258
- session["auth_sign_up_error"] = "An unexpected error occured. Please try again."
259
- return redirect(url_for("locust_cloud_auth.signup"))
260
-
261
- session["auth_sign_up_error"] = ""
262
- confirmation_code = request.form.get("confirmation_code")
263
-
264
- try:
265
- auth_response = requests.post(
266
- f"{environment.parsed_options.deployer_url}/auth/confirm-signup",
267
- json={
268
- "username": session.get("username"),
269
- "customer_name": session.get("customer_name"),
270
- "user_sub_id": session["user_sub_id"],
271
- "confirmation_code": confirmation_code,
272
- },
273
- )
274
-
275
- auth_response.raise_for_status()
276
-
277
- session["username"] = None
278
- session["auth_info"] = "Account created successfully!"
279
- session["auth_sign_up_error"] = ""
280
-
281
- return redirect("https://docs.locust.cloud/")
282
- except requests.exceptions.HTTPError as e:
283
- message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
284
- session["auth_info"] = ""
285
- session["auth_sign_up_error"] = message
286
-
287
- return redirect(url_for("locust_cloud_auth.signup"))
288
-
289
- @auth_blueprint.route("/forgot-password")
290
- def forgot_password():
291
- if not environment.parsed_options.allow_forgot_password:
292
- return redirect(url_for("locust.login"))
293
-
294
- forgot_password_args = {
295
- "custom_form": {
296
- "inputs": [
297
- {
298
- "label": "Username",
299
- "name": "username",
300
- "is_required": True,
301
- "type": "email",
302
- },
303
- ],
304
- "callback_url": f"{web_base_path}/send-forgot-password",
305
- "submit_button_text": "Reset Password",
306
- },
307
- "info": "Enter your email and we will send a code to reset your password",
308
- }
309
-
310
- if session.get("auth_error"):
311
- forgot_password_args["error"] = session["auth_error"]
312
-
313
- return render_template_from("auth.html", auth_args=forgot_password_args)
314
-
315
- @auth_blueprint.route("/send-forgot-password", methods=["POST"])
316
- def send_forgot_password():
317
- if not environment.parsed_options.allow_forgot_password:
318
- return redirect(url_for("locust.login"))
319
-
320
- try:
321
- username = request.form.get("username", "")
322
-
323
- auth_response = requests.post(
324
- f"{environment.parsed_options.deployer_url}/auth/forgot-password",
325
- json={"username": username},
326
- )
327
-
328
- auth_response.raise_for_status()
329
-
330
- session["username"] = username
331
-
332
- return redirect(url_for("locust_cloud_auth.password_reset"))
333
- except requests.exceptions.HTTPError as e:
334
- message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
335
- session["auth_info"] = ""
336
- session["auth_error"] = message
337
-
338
- return redirect(url_for("locust_cloud_auth.forgot_password"))
339
-
340
- @auth_blueprint.route("/password-reset")
341
- def password_reset():
342
- if not environment.parsed_options.allow_forgot_password and not session.get("challenge_session"):
343
- return redirect(url_for("locust.login"))
344
-
345
- if session.get("challenge_session"):
346
- reset_password_args = {
347
- "custom_form": {
348
- "inputs": [
349
- {
350
- "label": "New Password",
351
- "name": "new_password",
352
- "is_required": True,
353
- "is_secret": True,
354
- },
355
- ],
356
- "callback_url": f"{web_base_path}/confirm-reset-password",
357
- "submit_button_text": "Set Password",
358
- },
359
- "info": "You must set a new password",
360
- }
361
- else:
362
- reset_password_args = {
363
- "custom_form": {
364
- "inputs": [
365
- {
366
- "label": "Confirmation Code",
367
- "name": "confirmation_code",
368
- "is_required": True,
369
- },
370
- {
371
- "label": "New Password",
372
- "name": "new_password",
373
- "is_required": True,
374
- "is_secret": True,
375
- },
376
- ],
377
- "callback_url": f"{web_base_path}/confirm-reset-password",
378
- "submit_button_text": "Reset Password",
379
- },
380
- "info": "Enter your the confirmation code that was sent to your email",
381
- }
382
-
383
- if session.get("auth_error"):
384
- reset_password_args["error"] = session["auth_error"]
385
-
386
- return render_template_from("auth.html", auth_args=reset_password_args)
387
-
388
- @auth_blueprint.route("/confirm-reset-password", methods=["POST"])
389
- def confirm_reset_password():
390
- if not environment.parsed_options.allow_forgot_password and not session.get("challenge_session"):
391
- return redirect(url_for("locust.login"))
392
-
393
- try:
394
- username = session["username"]
395
- confirmation_code = request.form.get("confirmation_code")
396
- new_password = request.form.get("new_password")
397
-
398
- auth_response = requests.post(
399
- f"{environment.parsed_options.deployer_url}/auth/password-reset",
400
- json={
401
- "username": username,
402
- "confirmation_code": confirmation_code,
403
- "new_password": new_password,
404
- "challenge_session": session.get("challenge_session"),
405
- },
406
- )
407
-
408
- auth_response.raise_for_status()
409
-
410
- session["username"] = ""
411
- session["auth_error"] = ""
412
-
413
- if session.get("challenge_session"):
414
- session["challenge_session"] = ""
415
-
416
- return redirect(url_for("locust_cloud_auth.password_reset_success"))
417
-
418
- session["auth_info"] = "Password reset successfully! Please login"
419
-
420
- return redirect(url_for("locust.login"))
421
- except requests.exceptions.HTTPError as e:
422
- message = e.response.json().get("Message", "An unexpected error occured. Please try again.")
423
- session["auth_info"] = ""
424
- session["auth_error"] = message
425
-
426
- return redirect(url_for("locust_cloud_auth.password_reset"))
427
-
428
- @auth_blueprint.route("/password-reset-success")
429
- def password_reset_success():
430
- return render_template_from(
431
- "auth.html",
432
- auth_args={
433
- "info": "Password successfully set! Please review the [documentation](https://docs.locust.cloud/) and start your first testrun! If you have already ran some tests, you may also [login](/login)"
434
- },
435
- )
436
-
437
- @auth_blueprint.route("/logout", methods=["POST"])
438
- @login_required
439
- def logout():
440
- logout_user()
441
- return redirect(url_for("locust.login"))
442
-
443
- environment.web_ui.app.register_blueprint(auth_blueprint)
@@ -1,141 +0,0 @@
1
- import logging
2
- import time
3
- from datetime import UTC, datetime
4
- from typing import Any
5
-
6
- import boto3
7
- import jwt
8
- import requests
9
- from botocore.credentials import RefreshableCredentials
10
- from botocore.session import Session as BotocoreSession
11
- from locust_cloud import __version__
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
-
16
- class CredentialError(Exception):
17
- """Custom exception for credential-related errors."""
18
-
19
- pass
20
-
21
-
22
- class CredentialManager:
23
- def __init__(
24
- self,
25
- lambda_url: str,
26
- username: str | None = None,
27
- password: str | None = None,
28
- user_sub_id: str | None = None,
29
- refresh_token: str | None = None,
30
- access_key: str | None = None,
31
- secret_key: str | None = None,
32
- ) -> None:
33
- self.lambda_url = lambda_url
34
- self.username = username
35
- self.password = password
36
- self.user_sub_id = user_sub_id
37
- self.refresh_token = refresh_token
38
-
39
- self.credentials = {
40
- "access_key": access_key,
41
- "secret_key": secret_key,
42
- }
43
- self.cognito_client_id_token: str = ""
44
- self.expiry_time: float = 0
45
-
46
- self.obtain_credentials()
47
-
48
- self.refreshable_credentials = RefreshableCredentials.create_from_metadata(
49
- metadata=self.get_current_credentials(),
50
- refresh_using=self.refresh_credentials,
51
- method="custom-refresh",
52
- )
53
-
54
- botocore_session = BotocoreSession()
55
- botocore_session._credentials = self.refreshable_credentials # type: ignore
56
- botocore_session.set_config_variable("signature_version", "v4")
57
-
58
- self.session = boto3.Session(botocore_session=botocore_session)
59
- logger.debug("Boto3 session created with RefreshableCredentials.")
60
-
61
- def obtain_credentials(self) -> None:
62
- payload = {}
63
- if self.username and self.password:
64
- payload = {"username": self.username, "password": self.password}
65
- elif self.user_sub_id and self.refresh_token:
66
- payload = {"user_sub_id": self.user_sub_id, "refresh_token": self.refresh_token}
67
- else:
68
- raise CredentialError("Insufficient credentials to obtain AWS session.")
69
-
70
- try:
71
- response = requests.post(
72
- f"{self.lambda_url}/auth/login",
73
- json=payload,
74
- headers={"X-Client-Version": __version__},
75
- )
76
- response.raise_for_status()
77
- data = response.json()
78
-
79
- token_key = next(
80
- (key for key in ["cognito_client_id_token", "id_token", "access_token"] if key in data), None
81
- )
82
-
83
- if not token_key:
84
- raise CredentialError("No valid token found in authentication response.")
85
-
86
- self.credentials = {
87
- "access_key": data.get("aws_access_key_id"),
88
- "secret_key": data.get("aws_secret_access_key"),
89
- "token": data.get("aws_session_token"),
90
- }
91
-
92
- token = data.get(token_key)
93
- if not token:
94
- raise CredentialError(f"Token '{token_key}' is missing in the authentication response.")
95
-
96
- decoded = jwt.decode(token, options={"verify_signature": False})
97
- self.expiry_time = decoded.get("exp", time.time() + 3600) - 60 # Refresh 1 minute before expiry
98
-
99
- self.cognito_client_id_token = token
100
-
101
- except requests.exceptions.HTTPError as http_err:
102
- response = http_err.response
103
- if response is None:
104
- raise CredentialError("Response was None?!") from http_err
105
-
106
- if response.status_code == 401:
107
- raise CredentialError("Incorrect username or password.") from http_err
108
- else:
109
- if js := response.json():
110
- if message := js.get("Message"):
111
- raise CredentialError(message)
112
- error_info = f"HTTP {response.status_code} {response.reason}"
113
- raise CredentialError(f"HTTP error occurred while obtaining credentials: {error_info}") from http_err
114
- except requests.exceptions.RequestException as req_err:
115
- raise CredentialError(f"Request exception occurred while obtaining credentials: {req_err}") from req_err
116
- except jwt.DecodeError as decode_err:
117
- raise CredentialError(f"Failed to decode JWT token: {decode_err}") from decode_err
118
- except KeyError as key_err:
119
- raise CredentialError(f"Missing expected key in authentication response: {key_err}") from key_err
120
-
121
- def refresh_credentials(self) -> dict[str, Any]:
122
- logger.debug("Refreshing credentials using refresh_credentials method.")
123
- self.obtain_credentials()
124
- return {
125
- "access_key": self.credentials.get("access_key"),
126
- "secret_key": self.credentials.get("secret_key"),
127
- "token": self.credentials.get("token"),
128
- "expiry_time": datetime.fromtimestamp(self.expiry_time, tz=UTC).isoformat(),
129
- }
130
-
131
- def get_current_credentials(self) -> dict[str, Any]:
132
- if not self.cognito_client_id_token:
133
- raise CredentialError("cognito_client_id_token not set in CredentialManager.")
134
-
135
- return {
136
- "access_key": self.credentials.get("access_key"),
137
- "secret_key": self.credentials.get("secret_key"),
138
- "token": self.credentials.get("token"),
139
- "expiry_time": datetime.fromtimestamp(self.expiry_time, tz=UTC).isoformat(),
140
- "cognito_client_id_token": self.cognito_client_id_token,
141
- }
locust_cloud/idle_exit.py DELETED
@@ -1,38 +0,0 @@
1
- import logging
2
- import sys
3
-
4
- import gevent
5
- import locust.env
6
- from locust import events
7
-
8
- logger = logging.getLogger(__name__)
9
-
10
-
11
- class IdleExit:
12
- def __init__(self, environment: locust.env.Environment):
13
- self.environment = environment
14
- self._destroy_task: gevent.Greenlet | None = None
15
- events.test_start.add_listener(self.on_locust_state_change)
16
- events.test_stop.add_listener(self.on_test_stop)
17
- events.quit.add_listener(self.on_locust_state_change)
18
-
19
- if not self.environment.parsed_options.autostart:
20
- self._destroy_task = gevent.spawn(self._destroy)
21
-
22
- def _destroy(self):
23
- gevent.sleep(1800)
24
- logger.info("Locust was detected as idle (no test running) for more than 30 minutes")
25
- self.environment.runner.quit()
26
-
27
- if self.environment.web_ui:
28
- self.environment.web_ui.greenlet.kill(timeout=5)
29
-
30
- if self.environment.web_ui.greenlet.started:
31
- sys.exit(1)
32
-
33
- def on_test_stop(self, **kwargs):
34
- self._destroy_task = gevent.spawn(self._destroy)
35
-
36
- def on_locust_state_change(self, **kwargs):
37
- if self._destroy_task:
38
- self._destroy_task.kill()