prelude-sdk-beta 1447__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,269 @@
1
+ import configparser
2
+ import json
3
+ import os
4
+ from functools import wraps
5
+ from pathlib import Path
6
+
7
+ import requests
8
+
9
+
10
+ class Keychain:
11
+
12
+ def __init__(
13
+ self,
14
+ keychain_location: str | None = os.path.join(
15
+ Path.home(), ".prelude", "keychain.ini"
16
+ ),
17
+ ):
18
+ self.keychain_location = keychain_location
19
+ if self.keychain_location and not os.path.exists(self.keychain_location):
20
+ head, _ = os.path.split(Path(self.keychain_location))
21
+ Path(head).mkdir(parents=True, exist_ok=True)
22
+ open(self.keychain_location, "x").close()
23
+ self.configure_keychain("", "")
24
+
25
+ def read_keychain(self):
26
+ cfg = configparser.ConfigParser()
27
+ cfg.read(self.keychain_location)
28
+ return cfg
29
+
30
+ def configure_keychain(
31
+ self,
32
+ account,
33
+ handle,
34
+ hq="https://api.us1.preludesecurity.com",
35
+ oidc=None,
36
+ profile="default",
37
+ slug=None,
38
+ ):
39
+ cfg = self.read_keychain()
40
+ cfg[profile] = {"account": account, "handle": handle, "hq": hq}
41
+ if oidc:
42
+ cfg[profile]["oidc"] = oidc
43
+ if slug:
44
+ cfg[profile]["slug"] = slug
45
+ with open(self.keychain_location, "w") as f:
46
+ cfg.write(f)
47
+
48
+ def get_profile(self, profile="default") -> dict:
49
+ try:
50
+ cfg = self.read_keychain()
51
+ profile = next(s for s in cfg.sections() if s == profile)
52
+ return dict(cfg[profile].items())
53
+ except StopIteration:
54
+ raise Exception(
55
+ "Could not find profile %s for account in %s"
56
+ % (profile, self.keychain_location)
57
+ )
58
+
59
+
60
+ def exchange_token(
61
+ account: str, handle: str, hq: str, auth_flow: str, auth_params: dict
62
+ ):
63
+ """
64
+ Two token exchange auth flows:
65
+ 1) Password auth: auth_flow = "password", auth_params = {"password": "your_password"}
66
+ 2) Refresh token auth: auth_flow = "refresh", auth_params = {"refresh_token": "your_refresh_token"}
67
+ 3) Exchange an OIDC authorization code for tokens:
68
+ auth_flow = "oauth_code", auth_params = {"code": "your_authorization_code", "verifier": "your_verifier", "source": "cli"}
69
+ """
70
+ res = requests.post(
71
+ f"{hq}/iam/token",
72
+ headers=dict(account=account, _product="py-sdk"),
73
+ json=dict(auth_flow=auth_flow, handle=handle, **auth_params),
74
+ timeout=10,
75
+ )
76
+ if res.status_code == 401:
77
+ raise Exception("Error logging in: Unauthorized")
78
+ if not res.ok:
79
+ raise Exception("Error logging in: %s" % res.text)
80
+ return res.json()
81
+
82
+
83
+ class Account:
84
+
85
+ @staticmethod
86
+ def from_keychain(profile: str = "default", resolve_enums: bool = False):
87
+ """
88
+ Create an account object from a pre-configured profile in your keychain file
89
+ """
90
+ keychain = Keychain()
91
+ profile_items = keychain.get_profile(profile)
92
+ if any([item not in profile_items for item in ["account", "handle", "hq"]]):
93
+ raise ValueError(
94
+ "Please make sure you are using an up-to-date profile with the following fields: account, handle, hq"
95
+ )
96
+ return _Account(
97
+ account=profile_items["account"],
98
+ handle=profile_items["handle"],
99
+ hq=profile_items["hq"],
100
+ oidc=profile_items.get("oidc"),
101
+ profile=profile,
102
+ slug=profile_items.get("slug"),
103
+ resolve_enums=resolve_enums,
104
+ )
105
+
106
+ @staticmethod
107
+ def from_token(
108
+ account: str,
109
+ handle: str,
110
+ token: str | None = None,
111
+ refresh_token: str | None = None,
112
+ hq: str = "https://api.us1.preludesecurity.com",
113
+ oidc: str | None = None,
114
+ slug: str | None = None,
115
+ resolve_enums: bool = False,
116
+ ):
117
+ """
118
+ Create an account object from an access token or a refresh token
119
+ """
120
+ if not any([token, refresh_token]):
121
+ raise ValueError("Please provide either an access token or a refresh token")
122
+ if refresh_token:
123
+ res = exchange_token(
124
+ account, handle, hq, "refresh", dict(refresh_token=refresh_token)
125
+ )
126
+ token = res["token"]
127
+ return _Account(
128
+ account,
129
+ handle,
130
+ hq,
131
+ keychain_location=None,
132
+ oidc=oidc,
133
+ slug=slug,
134
+ token=token,
135
+ token_location=None,
136
+ resolve_enums=resolve_enums,
137
+ )
138
+
139
+
140
+ class _Account:
141
+
142
+ def __init__(
143
+ self,
144
+ account: str,
145
+ handle: str,
146
+ hq: str,
147
+ oidc: str | None = None,
148
+ profile: str | None = None,
149
+ slug: str | None = None,
150
+ token: str | None = None,
151
+ keychain_location: str | None = os.path.join(
152
+ Path.home(), ".prelude", "keychain.ini"
153
+ ),
154
+ token_location: str | None = os.path.join(
155
+ Path.home(), ".prelude", "tokens.json"
156
+ ),
157
+ resolve_enums: bool = False,
158
+ ):
159
+ if token is None and token_location is None:
160
+ raise ValueError(
161
+ "Please provide either an access token or a token location"
162
+ )
163
+
164
+ super().__init__()
165
+ self.account = account
166
+ self.handle = handle
167
+ self.headers = dict(account=account, _product="py-sdk")
168
+ self.hq = hq
169
+ self.keychain = Keychain(keychain_location)
170
+ self.oidc = oidc
171
+ self.profile = profile
172
+ self.slug = slug
173
+ self.token = token
174
+ self.token_location = token_location
175
+ self.resolve_enums = resolve_enums
176
+ if self.token_location and not os.path.exists(self.token_location):
177
+ head, _ = os.path.split(Path(self.token_location))
178
+ Path(head).mkdir(parents=True, exist_ok=True)
179
+ with open(self.token_location, "x") as f:
180
+ json.dump({}, f)
181
+ self.source = "cli" if self.oidc else "main"
182
+
183
+ @property
184
+ def token_key(self):
185
+ return f"{self.handle}/{self.oidc}" if self.oidc else self.handle
186
+
187
+ def _read_tokens(self):
188
+ with open(self.token_location, "r") as f:
189
+ return json.load(f)
190
+
191
+ def save_new_token(self, new_tokens):
192
+ existing_tokens = self._read_tokens()
193
+ if self.token_key not in existing_tokens:
194
+ existing_tokens[self.token_key] = dict()
195
+ existing_tokens[self.token_key][self.hq] = new_tokens
196
+ with open(self.token_location, "w") as f:
197
+ json.dump(existing_tokens, f)
198
+
199
+ def _verify(self):
200
+ if not self.token_location:
201
+ raise ValueError("Please provide a token location to continue")
202
+ if self.profile and not any([self.handle, self.account]):
203
+ raise ValueError(
204
+ "Please configure your %s profile to continue" % self.profile
205
+ )
206
+
207
+ def password_login(self, password, new_password=None):
208
+ self._verify()
209
+ tokens = exchange_token(
210
+ self.account,
211
+ self.handle,
212
+ self.hq,
213
+ "password_change" if new_password else "password",
214
+ dict(password=password, new_password=new_password),
215
+ )
216
+ self.save_new_token(tokens)
217
+ return tokens
218
+
219
+ def refresh_tokens(self):
220
+ self._verify()
221
+ existing_tokens = self._read_tokens().get(self.token_key, {}).get(self.hq, {})
222
+ if not (refresh_token := existing_tokens.get("refresh_token")):
223
+ raise Exception("No refresh token found, please login first to continue")
224
+ tokens = exchange_token(
225
+ self.account,
226
+ self.handle,
227
+ self.hq,
228
+ "refresh",
229
+ dict(refresh_token=refresh_token, source=self.source),
230
+ )
231
+ tokens = existing_tokens | tokens
232
+ self.save_new_token(tokens)
233
+ return tokens
234
+
235
+ def exchange_authorization_code(self, authorization_code: str, verifier: str):
236
+ self._verify()
237
+ tokens = exchange_token(
238
+ self.account,
239
+ self.handle,
240
+ self.hq,
241
+ "oauth_code",
242
+ dict(code=authorization_code, verifier=verifier, source=self.source),
243
+ )
244
+ existing_tokens = self._read_tokens().get(self.token_key, {}).get(self.hq, {})
245
+ tokens = existing_tokens | tokens
246
+ self.save_new_token(tokens)
247
+ return tokens
248
+
249
+ def get_token(self):
250
+ if self.token:
251
+ return self.token
252
+
253
+ tokens = self._read_tokens().get(self.token_key, {}).get(self.hq, {})
254
+ if "token" not in tokens:
255
+ raise Exception("Please login to continue")
256
+ return tokens["token"]
257
+
258
+ def update_auth_header(self):
259
+ self.headers |= dict(authorization=f"Bearer {self.get_token()}")
260
+
261
+
262
+ def verify_credentials(func):
263
+ @wraps(verify_credentials)
264
+ def handler(*args, **kwargs):
265
+ args[0].account.update_auth_header()
266
+ return func(*args, **kwargs)
267
+
268
+ handler.__wrapped__ = func
269
+ return handler