1password-secrets 0.0.1.dev218__tar.gz → 0.0.1.dev236__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.
- {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev236}/1password_secrets.egg-info/PKG-INFO +4 -4
- 1password_secrets-0.0.1.dev236/1password_secrets.egg-info/requires.txt +4 -0
- {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev236}/PKG-INFO +4 -4
- {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev236}/onepassword_secrets.py +170 -119
- 1password_secrets-0.0.1.dev218/1password_secrets.egg-info/requires.txt +0 -4
- {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev236}/1password_secrets.egg-info/SOURCES.txt +0 -0
- {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev236}/1password_secrets.egg-info/dependency_links.txt +0 -0
- {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev236}/1password_secrets.egg-info/entry_points.txt +0 -0
- {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev236}/1password_secrets.egg-info/top_level.txt +0 -0
- {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev236}/README.md +0 -0
- {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev236}/pyproject.toml +0 -0
- {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev236}/setup.cfg +0 -0
- {1password_secrets-0.0.1.dev218 → 1password_secrets-0.0.1.dev236}/setup.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: 1password-secrets
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev236
|
|
4
4
|
Summary: 1password-secrets is a set of utilities to sync 1password secrets.
|
|
5
5
|
Home-page: https://github.com/significa/fly-1password-secrets
|
|
6
6
|
Author: Significa
|
|
@@ -18,9 +18,9 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: python-dotenv==1.2.
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist: rich==
|
|
21
|
+
Requires-Dist: python-dotenv==1.2.4
|
|
22
|
+
Requires-Dist: requests==2.32.5
|
|
23
|
+
Requires-Dist: rich==15.0.0
|
|
24
24
|
Requires-Dist: inquirer==3.4.1
|
|
25
25
|
Dynamic: author
|
|
26
26
|
Dynamic: classifier
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: 1password-secrets
|
|
3
|
-
Version: 0.0.1.
|
|
3
|
+
Version: 0.0.1.dev236
|
|
4
4
|
Summary: 1password-secrets is a set of utilities to sync 1password secrets.
|
|
5
5
|
Home-page: https://github.com/significa/fly-1password-secrets
|
|
6
6
|
Author: Significa
|
|
@@ -18,9 +18,9 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.14
|
|
19
19
|
Requires-Python: >=3.10
|
|
20
20
|
Description-Content-Type: text/markdown
|
|
21
|
-
Requires-Dist: python-dotenv==1.2.
|
|
22
|
-
Requires-Dist:
|
|
23
|
-
Requires-Dist: rich==
|
|
21
|
+
Requires-Dist: python-dotenv==1.2.4
|
|
22
|
+
Requires-Dist: requests==2.32.5
|
|
23
|
+
Requires-Dist: rich==15.0.0
|
|
24
24
|
Requires-Dist: inquirer==3.4.1
|
|
25
25
|
Dynamic: author
|
|
26
26
|
Dynamic: classifier
|
|
@@ -12,20 +12,23 @@ from tempfile import NamedTemporaryFile
|
|
|
12
12
|
from typing import NoReturn
|
|
13
13
|
|
|
14
14
|
import inquirer
|
|
15
|
+
import requests
|
|
15
16
|
from dotenv import dotenv_values
|
|
16
17
|
from rich.console import Console
|
|
17
18
|
from rich.panel import Panel
|
|
18
19
|
from rich.table import Table
|
|
19
20
|
from rich.text import Text
|
|
20
|
-
from sgqlc.endpoint.http import HTTPEndpoint
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
FLY_MACHINES_API_ENDPOINT = "https://api.machines.dev/v1"
|
|
23
|
+
FLY_API_TIMEOUT_SECONDS = 30
|
|
23
24
|
DATE_FORMAT = "%Y/%m/%d %H:%M:%S"
|
|
24
25
|
DEFAULT_ENV_FILE_NAME = ".env"
|
|
25
26
|
ONE_PASSWORD_FILE_PATH_FIELD_NAME = "file_name" # noqa: S105
|
|
26
27
|
ONE_PASSWORD_NOTES_CONTENT_FIELD_NAME = "notesPlain" # noqa: S105
|
|
27
28
|
ONE_PASSWORD_SECURE_NOTE_CATEGORY = "Secure Note" # noqa: S105
|
|
28
29
|
DEFAULT_REMOTE_NAME = "origin"
|
|
30
|
+
REDACTED_PLACEHOLDER = "<REDACTED>" # noqa: S105
|
|
31
|
+
SENSITIVE_HEADER_NAMES = frozenset({"authorization", "proxy-authorization", "cookie", "set-cookie"})
|
|
29
32
|
|
|
30
33
|
console = Console()
|
|
31
34
|
|
|
@@ -64,6 +67,63 @@ def _setup_logger():
|
|
|
64
67
|
logger = _setup_logger()
|
|
65
68
|
|
|
66
69
|
|
|
70
|
+
def _redact_headers(headers) -> dict:
|
|
71
|
+
return {
|
|
72
|
+
name: (REDACTED_PLACEHOLDER if name.lower() in SENSITIVE_HEADER_NAMES else value)
|
|
73
|
+
for name, value in dict(headers or {}).items()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _format_command(command_args) -> str:
|
|
78
|
+
return " ".join((f'"{arg}"' if " " in arg else arg) for arg in command_args)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _debug_enabled() -> bool:
|
|
82
|
+
return logger.isEnabledFor(logging.DEBUG)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _debug_request(method, url, headers, json_body) -> None:
|
|
86
|
+
if not _debug_enabled():
|
|
87
|
+
return
|
|
88
|
+
|
|
89
|
+
lines = [f"Request: {method} {url}", "Request headers:"]
|
|
90
|
+
lines += [f" {name}: {value}" for name, value in _redact_headers(headers).items()]
|
|
91
|
+
|
|
92
|
+
if json_body is not None:
|
|
93
|
+
lines += ["Request body:", json.dumps(json_body, indent=2)]
|
|
94
|
+
|
|
95
|
+
logger.debug("\n".join(lines))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _debug_response(response) -> None:
|
|
99
|
+
if not _debug_enabled():
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
lines = [f"Response: {response.status_code}", "Response headers:"]
|
|
103
|
+
lines += [f" {name}: {value}" for name, value in _redact_headers(response.headers).items()]
|
|
104
|
+
lines += ["Response body:", response.text]
|
|
105
|
+
|
|
106
|
+
logger.debug("\n".join(lines))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _debug_command(command_args, output=None, returncode=None, redact_output=False) -> None:
|
|
110
|
+
if not _debug_enabled():
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
lines = [f"Running command: {_format_command(command_args)}"]
|
|
114
|
+
|
|
115
|
+
if returncode is not None:
|
|
116
|
+
lines.append(f"Exit code: {returncode}")
|
|
117
|
+
|
|
118
|
+
if output is not None:
|
|
119
|
+
if isinstance(output, bytes):
|
|
120
|
+
output = output.decode("utf-8", errors="replace")
|
|
121
|
+
|
|
122
|
+
lines += ["Output:", REDACTED_PLACEHOLDER if redact_output else output]
|
|
123
|
+
|
|
124
|
+
logger.debug("\n".join(lines))
|
|
125
|
+
|
|
126
|
+
|
|
67
127
|
def get_1password_env_file_item_id(title_substring, vault=None):
|
|
68
128
|
secure_notes = json.loads(
|
|
69
129
|
_run_1password_command(
|
|
@@ -129,9 +189,11 @@ def get_filename_from_1password(item_id, vault=None):
|
|
|
129
189
|
|
|
130
190
|
|
|
131
191
|
def get_fly_auth_token():
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
192
|
+
command_args = ["fly", "auth", "token", "--json"]
|
|
193
|
+
output = subprocess.check_output(command_args) # noqa: S603
|
|
194
|
+
_debug_command(command_args, output=output, returncode=0, redact_output=True)
|
|
195
|
+
|
|
196
|
+
return json.loads(output)["token"]
|
|
135
197
|
|
|
136
198
|
|
|
137
199
|
def _get_file_contents(filepath, raise_if_not_found=True):
|
|
@@ -160,132 +222,120 @@ def _boolean_prompt(prompt: str, default: bool = False) -> bool:
|
|
|
160
222
|
return answers["confirm"]
|
|
161
223
|
|
|
162
224
|
|
|
163
|
-
def
|
|
225
|
+
def _make_fly_api_request(
|
|
226
|
+
method,
|
|
227
|
+
path,
|
|
228
|
+
json_body=None,
|
|
229
|
+
token=None,
|
|
230
|
+
status_message="Communicating with Fly.io",
|
|
231
|
+
):
|
|
232
|
+
url = f"{FLY_MACHINES_API_ENDPOINT}{path}"
|
|
233
|
+
headers = {
|
|
234
|
+
"Authorization": f"Bearer {token or get_fly_auth_token()}",
|
|
235
|
+
"Content-Type": "application/json",
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
_debug_request(method, url, headers, json_body)
|
|
239
|
+
|
|
164
240
|
with console.status(f"[bold cyan]{status_message}...", spinner="dots"):
|
|
165
|
-
|
|
241
|
+
try:
|
|
242
|
+
response = requests.request(
|
|
243
|
+
method,
|
|
244
|
+
url,
|
|
245
|
+
headers=headers,
|
|
246
|
+
json=json_body,
|
|
247
|
+
timeout=FLY_API_TIMEOUT_SECONDS,
|
|
248
|
+
)
|
|
249
|
+
except requests.RequestException as error:
|
|
250
|
+
raise_error(f"Failed to reach the Fly API: {error}")
|
|
166
251
|
|
|
167
|
-
|
|
252
|
+
_debug_response(response)
|
|
168
253
|
|
|
169
|
-
|
|
254
|
+
if not response.ok:
|
|
255
|
+
try:
|
|
256
|
+
body = response.json()
|
|
257
|
+
except ValueError:
|
|
258
|
+
body = response.text
|
|
170
259
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
f"Fly response:\n{json.dumps(response, indent=2)}\n"
|
|
174
|
-
)
|
|
260
|
+
message = body.get("error") if isinstance(body, dict) else body
|
|
261
|
+
raise_error(f"Fly API error ({response.status_code}): {message or 'unknown error'}")
|
|
175
262
|
|
|
176
|
-
if response.
|
|
177
|
-
|
|
263
|
+
if not response.content:
|
|
264
|
+
return None
|
|
178
265
|
|
|
179
|
-
|
|
266
|
+
try:
|
|
267
|
+
return response.json()
|
|
268
|
+
except ValueError:
|
|
269
|
+
return None
|
|
180
270
|
|
|
181
271
|
|
|
182
272
|
def update_fly_secrets(app_id, secrets):
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
)
|
|
192
|
-
|
|
193
|
-
input: {
|
|
194
|
-
appId: $appId
|
|
195
|
-
replaceAll: $replaceAll
|
|
196
|
-
secrets: $secrets
|
|
197
|
-
}
|
|
198
|
-
) {
|
|
199
|
-
app {
|
|
200
|
-
name
|
|
201
|
-
}
|
|
202
|
-
release {
|
|
203
|
-
version
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
""",
|
|
208
|
-
{"appId": app_id, "secrets": secrets_input, "replaceAll": True},
|
|
209
|
-
status_message="Uploading secrets to Fly",
|
|
273
|
+
token = get_fly_auth_token()
|
|
274
|
+
|
|
275
|
+
current_secrets_response = (
|
|
276
|
+
_make_fly_api_request(
|
|
277
|
+
"GET",
|
|
278
|
+
f"/apps/{app_id}/secrets",
|
|
279
|
+
token=token,
|
|
280
|
+
status_message="Fetching current Fly secrets",
|
|
281
|
+
)
|
|
282
|
+
or {}
|
|
210
283
|
)
|
|
211
284
|
|
|
212
|
-
|
|
213
|
-
""
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
app(name: $appName){
|
|
218
|
-
secrets{
|
|
219
|
-
name
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
""",
|
|
224
|
-
{
|
|
225
|
-
"appName": app_id,
|
|
226
|
-
},
|
|
227
|
-
status_message="Fetching current Fly secrets",
|
|
228
|
-
)
|
|
285
|
+
secret_names_in_fly = {
|
|
286
|
+
secret["name"]
|
|
287
|
+
for secret in current_secrets_response.get("secrets") or []
|
|
288
|
+
if secret.get("name")
|
|
289
|
+
}
|
|
229
290
|
|
|
230
|
-
|
|
231
|
-
secret_names_in_fly = {secret["name"] for secret in get_secrets_response["app"]["secrets"]}
|
|
291
|
+
secrets_names_in_fly_only = secret_names_in_fly.difference(secrets.keys())
|
|
232
292
|
|
|
233
|
-
|
|
293
|
+
deletions = {}
|
|
234
294
|
|
|
235
295
|
if len(secrets_names_in_fly_only) > 0 and _boolean_prompt(
|
|
236
296
|
"The following secrets will be deleted from Fly: {}. Are you sure".format(
|
|
237
297
|
", ".join(sorted(secrets_names_in_fly_only))
|
|
238
298
|
)
|
|
239
299
|
):
|
|
240
|
-
|
|
241
|
-
"""
|
|
242
|
-
mutation(
|
|
243
|
-
$appId: ID!
|
|
244
|
-
$secretNames: [String!]!
|
|
245
|
-
) {
|
|
246
|
-
unsetSecrets(
|
|
247
|
-
input: {
|
|
248
|
-
appId: $appId
|
|
249
|
-
keys: $secretNames
|
|
250
|
-
}
|
|
251
|
-
){
|
|
252
|
-
release{
|
|
253
|
-
id
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
""",
|
|
258
|
-
{
|
|
259
|
-
"appId": app_id,
|
|
260
|
-
"secretNames": list(secrets_names_in_fly_only),
|
|
261
|
-
},
|
|
262
|
-
status_message="Removing deleted secrets from Fly",
|
|
263
|
-
)
|
|
300
|
+
deletions = dict.fromkeys(secrets_names_in_fly_only, None)
|
|
264
301
|
|
|
265
|
-
|
|
302
|
+
values = {**secrets, **deletions}
|
|
266
303
|
|
|
267
|
-
if
|
|
268
|
-
release_version = release.get("version", "unknown")
|
|
269
|
-
console.print(
|
|
270
|
-
f"[bold green]Releasing Fly app '{app_id}' version {release_version}[/bold green]"
|
|
271
|
-
)
|
|
272
|
-
else:
|
|
304
|
+
if not values:
|
|
273
305
|
console.print()
|
|
274
|
-
console.print("[dim]
|
|
275
|
-
|
|
276
|
-
|
|
306
|
+
console.print("[dim]No secret changes to push to Fly.[/dim]")
|
|
307
|
+
return
|
|
308
|
+
|
|
309
|
+
_make_fly_api_request(
|
|
310
|
+
"POST",
|
|
311
|
+
f"/apps/{app_id}/secrets",
|
|
312
|
+
json_body={"values": values},
|
|
313
|
+
token=token,
|
|
314
|
+
status_message="Uploading secrets to Fly",
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
console.print()
|
|
318
|
+
console.print(f"[bold green]Secrets staged on Fly app '{app_id}'[/bold green]")
|
|
319
|
+
|
|
320
|
+
if _boolean_prompt("Deploy secrets now"):
|
|
321
|
+
_deploy_fly_secrets(app_id)
|
|
277
322
|
|
|
278
323
|
|
|
279
324
|
def _deploy_fly_secrets(app_id):
|
|
280
|
-
"""Deploy secrets to a Fly app using the
|
|
325
|
+
"""Deploy secrets to a Fly app using the fly CLI."""
|
|
281
326
|
console.print()
|
|
282
327
|
console.print(f"[bold cyan]Deploying secrets to Fly app '{app_id}'...[/bold cyan]")
|
|
283
328
|
console.print()
|
|
284
329
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
330
|
+
command_args = ["fly", "secrets", "deploy", "-a", app_id]
|
|
331
|
+
_debug_command(command_args)
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
result = subprocess.run(command_args, check=False) # noqa: S603
|
|
335
|
+
except FileNotFoundError:
|
|
336
|
+
raise_error("The 'fly' CLI was not found. See https://fly.io/docs/flyctl/install/")
|
|
337
|
+
|
|
338
|
+
_debug_command(command_args, returncode=result.returncode)
|
|
289
339
|
|
|
290
340
|
if result.returncode != 0:
|
|
291
341
|
raise_error(f"Failed to deploy secrets (exit code {result.returncode})")
|
|
@@ -352,18 +402,16 @@ def _run_1password_command(*args, vault=None, json_output=True, status_message=N
|
|
|
352
402
|
if json_output:
|
|
353
403
|
command_args.extend(["--format", "json"])
|
|
354
404
|
|
|
355
|
-
logger.debug(
|
|
356
|
-
"Running command: {}".format(
|
|
357
|
-
" ".join((f'"{arg}"' if " " in arg else arg) for arg in command_args)
|
|
358
|
-
)
|
|
359
|
-
)
|
|
360
|
-
|
|
361
405
|
def run_command():
|
|
362
406
|
try:
|
|
363
|
-
|
|
407
|
+
output = subprocess.check_output(command_args) # noqa: S603
|
|
364
408
|
except subprocess.CalledProcessError as e:
|
|
409
|
+
_debug_command(command_args, returncode=e.returncode)
|
|
365
410
|
raise_error(f"1Password command failed with exit code {e.returncode}")
|
|
366
411
|
|
|
412
|
+
_debug_command(command_args, output=output, returncode=0)
|
|
413
|
+
return output
|
|
414
|
+
|
|
367
415
|
if status_message:
|
|
368
416
|
with console.status(f"[bold cyan]{status_message}...", spinner="dots"):
|
|
369
417
|
return run_command()
|
|
@@ -484,7 +532,10 @@ def edit_1password_fly_secrets(app_id, vault=None):
|
|
|
484
532
|
)
|
|
485
533
|
)
|
|
486
534
|
|
|
487
|
-
|
|
535
|
+
editor_command_args = ["code", "--wait", "--disable-extensions", file.name]
|
|
536
|
+
_debug_command(editor_command_args)
|
|
537
|
+
editor_output = subprocess.check_output(editor_command_args) # noqa: S603
|
|
538
|
+
_debug_command(editor_command_args, output=editor_output, returncode=0)
|
|
488
539
|
|
|
489
540
|
console.print("[green]Editor closed.[/green]")
|
|
490
541
|
console.print()
|
|
@@ -578,26 +629,23 @@ def _get_git_remote_name(remote=DEFAULT_REMOTE_NAME) -> tuple[str | None, str |
|
|
|
578
629
|
git_repository_regex = r"^(\w+)(:\/\/|@)([^\/:]+)[\/:]([^\/:]+)\/(.+).git$"
|
|
579
630
|
|
|
580
631
|
git_remote_url = None
|
|
632
|
+
command_args = ["git", "config", "--get", f"remote.{remote}.url"]
|
|
581
633
|
|
|
582
634
|
try:
|
|
583
635
|
git_remote_url = (
|
|
584
|
-
subprocess.check_output( # noqa: S603
|
|
585
|
-
[ # noqa: S607
|
|
586
|
-
"git",
|
|
587
|
-
"config",
|
|
588
|
-
"--get",
|
|
589
|
-
f"remote.{remote}.url",
|
|
590
|
-
]
|
|
591
|
-
)
|
|
636
|
+
subprocess.check_output(command_args) # noqa: S603
|
|
592
637
|
.decode("utf-8")
|
|
593
638
|
.strip()
|
|
594
639
|
)
|
|
640
|
+
_debug_command(command_args, output=git_remote_url, returncode=0)
|
|
595
641
|
|
|
596
642
|
except FileNotFoundError:
|
|
643
|
+
_debug_command(command_args, output="git not in the PATH")
|
|
597
644
|
return ("git not in the PATH", None)
|
|
598
645
|
|
|
599
646
|
except subprocess.CalledProcessError as error:
|
|
600
647
|
exit_code, _command = error.args
|
|
648
|
+
_debug_command(command_args, returncode=exit_code)
|
|
601
649
|
|
|
602
650
|
if exit_code == 1:
|
|
603
651
|
return (f"Either not in a git repository or remote {remote!r} is not set", None)
|
|
@@ -666,7 +714,10 @@ def main():
|
|
|
666
714
|
"--debug",
|
|
667
715
|
action=argparse.BooleanOptionalAction,
|
|
668
716
|
default=False,
|
|
669
|
-
help=
|
|
717
|
+
help=(
|
|
718
|
+
"run in debug mode, logging every command, HTTP request and response "
|
|
719
|
+
"(authorization headers and the fly auth token are redacted)"
|
|
720
|
+
),
|
|
670
721
|
)
|
|
671
722
|
|
|
672
723
|
parser.add_argument(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|