suite-py 1.41.3__py3-none-any.whl → 1.41.4__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.
@@ -1,180 +1,11 @@
1
- import base64
2
- import hashlib
3
- import logging
4
- import secrets
5
- import sys
6
- import threading
7
- import urllib
8
- import webbrowser
9
- from time import sleep
10
-
11
- import requests
12
- from flask import Flask, render_template, request
13
- from werkzeug.serving import make_server
14
-
15
- from suite_py.lib import logger
16
1
  from suite_py.lib import metrics
17
-
18
- # Global vars for comunication between flask thread and main cli function
19
- received_callback = None
20
- code = None
21
- error_message = None
22
- received_state = None
23
-
24
- # Global Flask App
25
- app = Flask(__name__, template_folder="templates")
26
- log = logging.getLogger("werkzeug")
27
- log.disabled = True
28
-
29
-
30
- @app.route("/callback")
31
- def callback():
32
- """# pylint: disable-next=missing-timeout
33
- The callback is invoked after a completed login attempt (succesful or otherwise).
34
- It sets global variables with the auth code or error messages, then sets the
35
- polling flag received_callback.
36
- :return:
37
- """
38
- global received_callback, code, error_message, received_state
39
- error_message = None
40
- code = None
41
- if "error" in request.args:
42
- error_message = request.args["error"] + ": " + request.args["error_description"]
43
- else:
44
- code = request.args["code"]
45
- received_state = request.args["state"]
46
- received_callback = True
47
- return render_template("login.html")
48
-
49
-
50
- class ServerThread(threading.Thread):
51
- """
52
- The Flask server is done this way to allow shutting down after a single request has been received.
53
- """
54
-
55
- def __init__(self, app):
56
- threading.Thread.__init__(self)
57
- self.srv = make_server("127.0.0.1", 5000, app)
58
- self.ctx = app.app_context()
59
- self.ctx.push()
60
-
61
- def run(self):
62
- # logger.debug('starting server')
63
- self.srv.serve_forever()
64
-
65
- def shutdown(self):
66
- self.srv.shutdown()
2
+ from suite_py.lib.handler.okta_handler import Okta
67
3
 
68
4
 
69
5
  class Login:
70
- def __init__(self, config):
71
-
72
- try:
73
- if not config.okta["client_id"] or not config.okta["base_url"]:
74
- self.usage()
75
- sys.exit(-1)
76
- except AttributeError:
77
- self.usage()
78
- sys.exit(-1)
79
-
80
- self._config = config
81
- self.okta_client_id = config.okta["client_id"]
82
- self.base_url = config.okta["base_url"]
83
- self.okta_scope = "openid"
84
- self.redirect_uri = "http://127.0.0.1:5000/callback"
85
-
86
- def usage(self):
87
- logger.warning("Unable to login: missing config")
88
- logger.warning(
89
- "please check docs: https://github.com/primait/suite_py/blob/master/README.md"
90
- )
91
-
92
- def url_encode_no_padding(self, byte_data):
93
- """
94
- Safe encoding handles + and /, and also replace = with nothing
95
- :param byte_data:
96
- :return:
97
- """
98
- return base64.urlsafe_b64encode(byte_data).decode("utf-8").replace("=", "")
99
-
100
- def generate_challenge(self, a_verifier):
101
- return self.url_encode_no_padding(hashlib.sha256(a_verifier.encode()).digest())
6
+ def __init__(self, config, tokens):
7
+ self._okta = Okta(config, tokens)
102
8
 
103
9
  @metrics.command("login")
104
10
  def run(self):
105
- global received_callback
106
-
107
- # from https://auth0.com/docs/flows/add-login-using-the-authorization-code-flow-with-pkce
108
- # Step1: Create code verifier: Generate a code_verifier that will be sent to Auth0 to request tokens.
109
- verifier = self.url_encode_no_padding(secrets.token_bytes(32))
110
- # Step2: Create code challenge: Generate a code_challenge from the code_verifier that will be sent to Auth0 to request an authorization_code.
111
- challenge = self.generate_challenge(verifier)
112
- state = self.url_encode_no_padding(secrets.token_bytes(32))
113
- client_id = self.okta_client_id
114
- base_url = self.base_url
115
- scope = self.okta_scope
116
- redirect_uri = self.redirect_uri
117
- #
118
- # We generate a nonce (state) that is used to protect against attackers invoking the callback
119
- url = f"{base_url}/authorize?"
120
- url_parameters = {
121
- "scope": scope,
122
- "response_type": "code",
123
- "redirect_uri": redirect_uri,
124
- "client_id": client_id,
125
- "code_challenge": challenge.replace("=", ""),
126
- "code_challenge_method": "S256",
127
- "state": state,
128
- }
129
- url = url + urllib.parse.urlencode(url_parameters)
130
-
131
- # Step3: Authorize user: Request the user's authorization and redirect back to your app with an authorization_code.
132
- # Open the browser window to the login url
133
- # Start the server
134
- # Poll til the callback has been invoked
135
- received_callback = False
136
- logger.info(
137
- "A browser tab should've opened. If not manually navigate to: " + url
138
- )
139
- webbrowser.open(url)
140
- server = ServerThread(app)
141
- server.start()
142
- while not received_callback:
143
- sleep(1)
144
- server.shutdown()
145
-
146
- if state != received_state:
147
- logger.error(
148
- "Error: session replay or similar attack in progress. Please log out of all connections."
149
- )
150
- sys.exit(1)
151
-
152
- if error_message:
153
- logger.error("An error occurred:")
154
- logger.error(error_message)
155
- sys.exit(1)
156
-
157
- # Step4: Request tokens: Exchange your authorization_code and code_verifier for tokens.
158
- url = f"{base_url}/token"
159
- headers = {
160
- "content-type": "application/x-www-form-urlencoded",
161
- "Accept": "application/json",
162
- }
163
- body = {
164
- "grant_type": "authorization_code",
165
- "client_id": client_id,
166
- "code_verifier": verifier,
167
- "code": code,
168
- "redirect_uri": redirect_uri,
169
- }
170
- # pylint: disable-next=missing-timeout
171
- r = requests.post(url, headers=headers, data=body)
172
- data = r.json()
173
- logger.debug(data)
174
- if "id_token" in data:
175
- token = str(data["id_token"])
176
- else:
177
- logger.error("id_token not found in okta response")
178
- sys.exit(1)
179
- logger.info("login succeded")
180
- self._config.put_cache(f"{base_url}_token", token)
11
+ self._okta.login()
@@ -3,8 +3,7 @@ import sys
3
3
 
4
4
  from halo import Halo
5
5
 
6
- from suite_py.lib import logger
7
- from suite_py.lib import metrics
6
+ from suite_py.lib import logger, metrics
8
7
  from suite_py.lib.handler import git_handler as git
9
8
  from suite_py.lib.handler import prompt_utils
10
9
  from suite_py.lib.handler.captainhook_handler import CaptainHook
@@ -16,11 +15,11 @@ from suite_py.lib.symbol import CHECKMARK, CROSSMARK
16
15
 
17
16
 
18
17
  class MergePR:
19
- def __init__(self, project, config, tokens):
18
+ def __init__(self, project, captainhook: CaptainHook, config, tokens):
20
19
  self._project = project
21
20
  self._config = config
22
21
  self._youtrack = YoutrackHandler(config, tokens)
23
- self._captainhook = CaptainHook(config, tokens=tokens)
22
+ self._captainhook = captainhook
24
23
  self._git = GitHandler(project, config)
25
24
  self._github = GithubHandler(tokens)
26
25
  self._drone = DroneHandler(config, tokens, repo=project)
@@ -3,9 +3,7 @@ import sys
3
3
 
4
4
  from github import GithubException
5
5
 
6
- from suite_py.commands.ask_review import AskReview
7
- from suite_py.lib import logger
8
- from suite_py.lib import metrics
6
+ from suite_py.lib import logger, metrics
9
7
  from suite_py.lib.handler import prompt_utils
10
8
  from suite_py.lib.handler.git_handler import GitHandler, get_commit_logs
11
9
  from suite_py.lib.handler.github_handler import GithubHandler
@@ -13,7 +11,8 @@ from suite_py.lib.handler.youtrack_handler import YoutrackHandler
13
11
 
14
12
 
15
13
  class OpenPR:
16
- def __init__(self, project, config, tokens):
14
+ def __init__(self, ask_review, project, config, tokens):
15
+ self._ask_review = ask_review
17
16
  self._project = project
18
17
  self._config = config
19
18
  self._tokens = tokens
@@ -115,7 +114,7 @@ class OpenPR:
115
114
  return
116
115
 
117
116
  if prompt_utils.ask_confirm("Do you want to insert reviewers?"):
118
- AskReview(self._project, self._config, self._tokens).run()
117
+ self._ask_review.run()
119
118
  elif youtrack_id:
120
119
  self._detect_and_move_new_pr_with_reviewers(youtrack_id, pr)
121
120
 
@@ -3,17 +3,15 @@ import sys
3
3
 
4
4
  import requests
5
5
 
6
- from suite_py.lib import logger
7
- from suite_py.lib import metrics
8
- from suite_py.lib.handler.captainhook_handler import CaptainHook
6
+ from suite_py.lib import logger, metrics
9
7
 
10
8
 
11
9
  class ProjectLock:
12
- def __init__(self, project, env, action, config, tokens):
10
+ def __init__(self, project, env, action, captainhook):
13
11
  self._project = project
14
12
  self._env = _parse_env(env)
15
13
  self._action = action
16
- self._captainhook = CaptainHook(config, tokens=tokens)
14
+ self._captainhook = captainhook
17
15
 
18
16
  @metrics.command("manage-project-lock")
19
17
  def run(self):
@@ -6,11 +6,9 @@ import semver
6
6
  from halo import Halo
7
7
 
8
8
  from suite_py.commands import common
9
- from suite_py.lib import logger
10
- from suite_py.lib import metrics
9
+ from suite_py.lib import logger, metrics
11
10
  from suite_py.lib.handler import git_handler as git
12
11
  from suite_py.lib.handler import prompt_utils
13
- from suite_py.lib.handler.captainhook_handler import CaptainHook
14
12
  from suite_py.lib.handler.changelog_handler import ChangelogHandler
15
13
  from suite_py.lib.handler.drone_handler import DroneHandler
16
14
  from suite_py.lib.handler.git_handler import GitHandler
@@ -21,7 +19,7 @@ from suite_py.lib.handler.youtrack_handler import YoutrackHandler
21
19
 
22
20
  class Release:
23
21
  # pylint: disable=too-many-instance-attributes
24
- def __init__(self, action, project, config, tokens, flags=None):
22
+ def __init__(self, action, project, captainhook, config, tokens, flags=None):
25
23
  self._action = action
26
24
  self._project = project
27
25
  self._flags = flags
@@ -29,7 +27,7 @@ class Release:
29
27
  self._tokens = tokens
30
28
  self._changelog_handler = ChangelogHandler()
31
29
  self._youtrack = YoutrackHandler(config, tokens)
32
- self._captainhook = CaptainHook(config)
30
+ self._captainhook = captainhook
33
31
  self._github = GithubHandler(tokens)
34
32
  self._repo = self._github.get_repo(project)
35
33
  self._git = GitHandler(project, config)
@@ -5,8 +5,7 @@ import sys
5
5
 
6
6
  import yaml
7
7
 
8
- from suite_py.lib import logger
9
- from suite_py.lib import metrics
8
+ from suite_py.lib import logger, metrics
10
9
  from suite_py.lib.handler import prompt_utils
11
10
  from suite_py.lib.handler.vault_handler import VaultHandler
12
11
 
@@ -1,7 +1,6 @@
1
1
  import sys
2
2
 
3
- from suite_py.lib import logger
4
- from suite_py.lib import metrics
3
+ from suite_py.lib import logger, metrics
5
4
  from suite_py.lib.handler import prompt_utils
6
5
  from suite_py.lib.tokens import Tokens
7
6
 
@@ -1,16 +1,15 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  from halo import Halo
3
3
 
4
- from suite_py.lib import logger
5
- from suite_py.lib import metrics
4
+ from suite_py.lib import logger, metrics
6
5
  from suite_py.lib.handler.captainhook_handler import CaptainHook
7
6
  from suite_py.lib.symbol import CHECKMARK, CROSSMARK
8
7
 
9
8
 
10
9
  class Status:
11
- def __init__(self, project, config):
10
+ def __init__(self, project, captainhook: CaptainHook):
12
11
  self._project = project
13
- self._captainhook = CaptainHook(config)
12
+ self._captainhook = captainhook
14
13
 
15
14
  @metrics.command("status")
16
15
  def run(self):
@@ -2,6 +2,8 @@
2
2
  import requests
3
3
 
4
4
  from suite_py.lib.handler.github_handler import GithubHandler
5
+ from suite_py.lib.handler.okta_handler import Okta
6
+ from suite_py.__version__ import __version__
5
7
 
6
8
 
7
9
  class UnauthorizedError(Exception):
@@ -10,14 +12,12 @@ class UnauthorizedError(Exception):
10
12
 
11
13
 
12
14
  class CaptainHook:
13
- def __init__(self, config, tokens=None):
15
+ _okta: Okta
16
+
17
+ def __init__(self, config, okta: Okta, tokens=None):
14
18
  self._baseurl = config.user["captainhook_url"]
15
19
  self._timeout = config.user["captainhook_timeout"]
16
- self._okta_base_url = config.okta["base_url"]
17
- self._okta_token = config.get_cache(f"{self._okta_base_url}_token")
18
- self._headers = {
19
- "Authorization": f"Bearer {self._okta_token}",
20
- }
20
+ self._okta = okta
21
21
 
22
22
  if tokens is not None:
23
23
  self._github = GithubHandler(tokens)
@@ -64,7 +64,7 @@ class CaptainHook:
64
64
  def send_post_request(self, endpoint, data=None, json=None):
65
65
  r = requests.post(
66
66
  f"{self._baseurl}{endpoint}",
67
- headers=self._headers,
67
+ headers=self._headers(),
68
68
  data=data,
69
69
  json=json,
70
70
  timeout=self._timeout,
@@ -75,7 +75,7 @@ class CaptainHook:
75
75
  def send_put_request(self, endpoint, data=None, json=None):
76
76
  r = requests.put(
77
77
  f"{self._baseurl}{endpoint}",
78
- headers=self._headers,
78
+ headers=self._headers(),
79
79
  data=data,
80
80
  json=json,
81
81
  timeout=self._timeout,
@@ -86,7 +86,7 @@ class CaptainHook:
86
86
  def send_get_request(self, endpoint):
87
87
  r = requests.get(
88
88
  f"{self._baseurl}{endpoint}",
89
- headers=self._headers,
89
+ headers=self._headers(),
90
90
  timeout=(2, self._timeout),
91
91
  )
92
92
 
@@ -103,3 +103,10 @@ class CaptainHook:
103
103
 
104
104
  def _get_user(self):
105
105
  return self._github.get_user().login
106
+
107
+ def _headers(self):
108
+ id_token = self._okta.get_id_token()
109
+ return {
110
+ "User-Agent": f"suite-py/{__version__}",
111
+ "Authorization": f"Bearer {id_token}",
112
+ }
@@ -12,7 +12,8 @@ from suite_py.lib.handler.captainhook_handler import CaptainHook
12
12
 
13
13
  # Collects metrics and sends them to datadog through captainhook
14
14
  class Metrics:
15
- def __init__(self, config: Config):
15
+ def __init__(self, captainhook: CaptainHook, config: Config):
16
+ self._captainhook = captainhook
16
17
  self._config = config
17
18
 
18
19
  # Emits the command_executed metric
@@ -37,8 +38,11 @@ class Metrics:
37
38
  metrics = []
38
39
 
39
40
  logger.debug(f"creating metric: {metric}")
40
- metrics.append(metric)
41
- self._config.put_cookie("metrics", metrics)
41
+ if self._config.user.get("disable_metrics_creation", False):
42
+ logger.debug("skipping metric creation")
43
+ else:
44
+ metrics.append(metric)
45
+ self._config.put_cookie("metrics", metrics)
42
46
 
43
47
  # Upload metrics in a detached background process
44
48
  def async_upload(self):
@@ -69,8 +73,6 @@ class Metrics:
69
73
  self.upload()
70
74
 
71
75
  def upload(self):
72
- captainhook = CaptainHook(self._config)
73
-
74
76
  metrics = self._config.get_cookie("metrics", [])
75
77
  # Prevent double uploads
76
78
  #
@@ -79,7 +81,7 @@ class Metrics:
79
81
  self._config.put_cookie("metrics", [])
80
82
  try:
81
83
  if len(metrics) != 0:
82
- captainhook.send_metrics(metrics)
84
+ self._captainhook.send_metrics(metrics)
83
85
  except Exception:
84
86
  # Upload failed, try again later
85
87
  self._config.put_cookie("metrics", metrics)
@@ -0,0 +1,81 @@
1
+ import time
2
+ import typing
3
+
4
+ from suite_py.lib import logger, oauth
5
+ from suite_py.lib.config import Config
6
+ from suite_py.lib.tokens import Tokens
7
+
8
+ _SCOPE = "openid offline_access"
9
+
10
+
11
+ class Okta:
12
+ def __init__(self, config: Config, tokens: Tokens) -> None:
13
+ self._config = config
14
+ self._tokens = tokens
15
+
16
+ def login(self):
17
+ res = oauth.authorization_code_flow(
18
+ self._config.okta["client_id"],
19
+ self._config.okta["base_url"],
20
+ _SCOPE,
21
+ )
22
+
23
+ self._update_tokens(res)
24
+
25
+ def get_id_token(self) -> str:
26
+ """
27
+ Returns an id_token, performing a token refresh if needed
28
+ Raises on an invalid or missing refresh token
29
+ """
30
+ return self._get_id_token() or self._refresh()
31
+
32
+ def _refresh(self) -> str:
33
+ logger.debug("Refreshing id_token")
34
+
35
+ refresh_token = self._get_refresh_token()
36
+ if not isinstance(refresh_token, str):
37
+ raise Exception(
38
+ "Invalid okta refresh token. Try logging in with `suite_py login`"
39
+ )
40
+ res = oauth.do_refresh_token(
41
+ self._config.okta["client_id"],
42
+ self._config.okta["base_url"],
43
+ _SCOPE,
44
+ refresh_token,
45
+ )
46
+
47
+ return self._update_tokens(res)
48
+
49
+ def _update_tokens(self, tokens: oauth.OAuthTokenResponse) -> str:
50
+ if not tokens.id_token:
51
+ raise Exception("Okta didn't return a new id_token. This shouldn't happen.")
52
+ if not tokens.refresh_token:
53
+ raise Exception(
54
+ "Okta didn't return a new refresh_token. This shouldn't happen."
55
+ )
56
+
57
+ self._set_refresh_token(tokens.refresh_token)
58
+
59
+ expires_at = time.time() + tokens.expires_in
60
+ self._set_id_token(tokens.id_token, expires_at)
61
+
62
+ return tokens.id_token
63
+
64
+ def _get_refresh_token(self) -> typing.Optional[str]:
65
+ return self._tokens.okta().get("refresh_token", None)
66
+
67
+ def _set_refresh_token(self, token: str):
68
+ okta = self._tokens.okta()
69
+ okta["refresh_token"] = token
70
+ self._tokens.edit("okta", okta)
71
+
72
+ def _get_id_token(self) -> typing.Optional[str]:
73
+ (token, expiration) = self._tokens.okta().get("id_token", (None, None))
74
+ if token is not None and time.time() < expiration:
75
+ return token
76
+ return None
77
+
78
+ def _set_id_token(self, token: str, expiration: float):
79
+ okta = self._tokens.okta()
80
+ okta["id_token"] = (token, expiration)
81
+ self._tokens.edit("okta", okta)
suite_py/lib/logger.py CHANGED
@@ -1,5 +1,6 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  import logging
3
+
3
4
  import logzero
4
5
  from logzero import logger as _logger
5
6
 
suite_py/lib/metrics.py CHANGED
@@ -1,6 +1,8 @@
1
1
  # -*- encoding: utf-8 -*-
2
2
  import functools
3
3
  from suite_py.lib.config import Config
4
+
5
+ from suite_py.lib.handler.captainhook_handler import CaptainHook
4
6
  from suite_py.lib.handler.metrics_handler import Metrics
5
7
 
6
8
  _metrics_handler = None
@@ -15,9 +17,9 @@ def _metrics() -> Metrics:
15
17
  )
16
18
 
17
19
 
18
- def setup(config: Config):
20
+ def setup(config: Config, captainhook: CaptainHook):
19
21
  global _metrics_handler
20
- _metrics_handler = Metrics(config)
22
+ _metrics_handler = Metrics(config=config, captainhook=captainhook)
21
23
 
22
24
 
23
25
  def command_executed(command):