cloudx-proxy 0.4.0__py3-none-any.whl → 0.4.2__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,208 @@
1
+ import os
2
+ import json
3
+ import subprocess
4
+ from pathlib import Path
5
+
6
+ def check_1password_cli() -> tuple:
7
+ """Check if 1Password CLI is installed and authenticated.
8
+
9
+ Returns:
10
+ tuple: (installed: bool, authenticated: bool, version: str)
11
+ """
12
+ try:
13
+ # Check if 1Password CLI is installed
14
+ result = subprocess.run(
15
+ ['op', '--version'],
16
+ capture_output=True,
17
+ text=True,
18
+ check=False
19
+ )
20
+
21
+ if result.returncode != 0:
22
+ return False, False, ""
23
+
24
+ version = result.stdout.strip()
25
+
26
+ # Check if 1Password CLI is authenticated
27
+ result = subprocess.run(
28
+ ['op', 'account', 'list'],
29
+ capture_output=True,
30
+ text=True,
31
+ check=False
32
+ )
33
+
34
+ if result.returncode != 0:
35
+ return True, False, version
36
+
37
+ return True, True, version
38
+
39
+ except FileNotFoundError:
40
+ return False, False, ""
41
+ except Exception:
42
+ return False, False, ""
43
+
44
+ def check_ssh_agent(agent_sock_path: str) -> bool:
45
+ """Check if 1Password SSH agent is running.
46
+
47
+ Args:
48
+ agent_sock_path: Path to the SSH agent socket
49
+
50
+ Returns:
51
+ bool: True if agent is running
52
+ """
53
+ try:
54
+ # Check if the socket file exists
55
+ if not os.path.exists(agent_sock_path):
56
+ return False
57
+
58
+ # Check if agent is active
59
+ env = os.environ.copy()
60
+ env['SSH_AUTH_SOCK'] = agent_sock_path
61
+
62
+ result = subprocess.run(
63
+ ['ssh-add', '-l'],
64
+ env=env,
65
+ capture_output=True,
66
+ text=True,
67
+ check=False
68
+ )
69
+
70
+ if "Could not open a connection to your authentication agent" in result.stderr:
71
+ return False
72
+
73
+ return True
74
+
75
+ except Exception:
76
+ return False
77
+
78
+ def list_ssh_keys() -> list:
79
+ """List SSH keys stored in 1Password.
80
+
81
+ Returns:
82
+ list: List of SSH key items
83
+ """
84
+ try:
85
+ # List SSH keys in 1Password using the correct command - confusingly, the category is "SSH Key" where the OUTPUT show "SSH_KEY"
86
+ result = subprocess.run(
87
+ ['op', 'item', 'list', '--categories', 'SSH Key', '--format=json'],
88
+ capture_output=True,
89
+ text=True,
90
+ check=False
91
+ )
92
+
93
+ if result.returncode != 0:
94
+ return []
95
+
96
+ items = json.loads(result.stdout)
97
+ return items
98
+
99
+ except Exception:
100
+ return []
101
+
102
+ def create_ssh_key(title: str, vault: str) -> tuple:
103
+ """Create a new SSH key in 1Password.
104
+
105
+ Args:
106
+ title: Title for the SSH key
107
+ vault: Vault ID to store the key in
108
+
109
+ Returns:
110
+ tuple: (success: bool, public_key: str, item_id: str)
111
+ """
112
+ try:
113
+ # Create a new SSH key in 1Password
114
+ result = subprocess.run(
115
+ [
116
+ 'op', 'item', 'create',
117
+ '--category=ssh-key', # and yet a different way to specify the category
118
+ f'--title={title}',
119
+ f'--vault={vault}'
120
+ ],
121
+ capture_output=True,
122
+ text=True,
123
+ check=False
124
+ )
125
+
126
+ if result.returncode != 0:
127
+ return False, "", ""
128
+
129
+ # Parse the output to extract the public key and item ID
130
+ output_lines = result.stdout.strip().split('\n')
131
+ item_id = ""
132
+ public_key = ""
133
+
134
+ for idx, line in enumerate(output_lines):
135
+ if line.startswith("ID:"):
136
+ item_id = line.split(":", 1)[1].strip()
137
+
138
+ # Check for "public key:" in the line
139
+ if "public key:" in line.lower():
140
+ public_key = line.split(":", 1)[1].strip()
141
+
142
+ # If we got the item ID but not the public key, try to get it separately
143
+ if item_id and not public_key:
144
+ result = subprocess.run(
145
+ ['op', 'item', 'get', item_id, '--fields', 'public key'],
146
+ capture_output=True,
147
+ text=True,
148
+ check=False
149
+ )
150
+
151
+ if result.returncode == 0:
152
+ public_key = result.stdout.strip()
153
+
154
+ return item_id and public_key, public_key, item_id
155
+
156
+ except Exception:
157
+ return False, "", ""
158
+
159
+ def get_vaults() -> list:
160
+ """Get list of available 1Password vaults.
161
+
162
+ Returns:
163
+ list: List of vault objects with 'id' and 'name' keys
164
+ """
165
+ try:
166
+ result = subprocess.run(
167
+ ['op', 'vault', 'list', '--format=json'],
168
+ capture_output=True,
169
+ text=True,
170
+ check=False
171
+ )
172
+
173
+ if result.returncode != 0:
174
+ return []
175
+
176
+ vaults = json.loads(result.stdout)
177
+ return vaults
178
+
179
+ except Exception:
180
+ return []
181
+
182
+ def save_public_key(public_key: str, key_path: str) -> bool:
183
+ """Save the public key to a file with proper permissions.
184
+
185
+ Args:
186
+ public_key: The public key content
187
+ key_path: Path to save the public key
188
+
189
+ Returns:
190
+ bool: True if successful
191
+ """
192
+ try:
193
+ # Create parent directories if needed
194
+ os.makedirs(os.path.dirname(key_path), exist_ok=True)
195
+
196
+ # Write the public key
197
+ with open(key_path, 'w') as f:
198
+ f.write(public_key)
199
+
200
+ # Set permissions (644) on Unix-like systems
201
+ if os.name != 'nt': # Not Windows
202
+ import stat
203
+ os.chmod(key_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IROTH | stat.S_IRGRP)
204
+
205
+ return True
206
+
207
+ except Exception:
208
+ return False
cloudx_proxy/_version.py CHANGED
@@ -17,5 +17,5 @@ __version__: str
17
17
  __version_tuple__: VERSION_TUPLE
18
18
  version_tuple: VERSION_TUPLE
19
19
 
20
- __version__ = version = '0.4.0'
21
- __version_tuple__ = version_tuple = (0, 4, 0)
20
+ __version__ = version = '0.4.2'
21
+ __version_tuple__ = version_tuple = (0, 4, 2)
cloudx_proxy/setup.py CHANGED
@@ -7,6 +7,7 @@ from pathlib import Path
7
7
  from typing import Optional, Tuple
8
8
  import boto3
9
9
  from botocore.exceptions import ClientError
10
+ from ._1password import check_1password_cli, check_ssh_agent, list_ssh_keys, create_ssh_key, get_vaults, save_public_key
10
11
 
11
12
  class CloudXSetup:
12
13
  def __init__(self, profile: str = "vscode", ssh_key: str = "vscode", ssh_config: str = None,
@@ -166,107 +167,51 @@ class CloudXSetup:
166
167
 
167
168
  self.print_status("Checking 1Password availability...")
168
169
 
169
- # Check if 1Password CLI is installed
170
- try:
171
- result = subprocess.run(
172
- ['op', '--version'],
173
- capture_output=True,
174
- text=True,
175
- check=False
176
- )
177
- if result.returncode != 0:
178
- self.print_status("1Password CLI not found. Please install it from https://1password.com/downloads/command-line/", False, 2)
179
- return False
180
-
181
- self.print_status(f"1Password CLI {result.stdout.strip()} installed", True, 2)
182
-
183
- # Check if 1Password SSH agent is running
184
- agent_sock_exists = self.onepassword_agent_sock.exists()
185
- if not agent_sock_exists:
186
- self.print_status("1Password SSH agent socket not found at ~/.1password/agent.sock", False, 2)
187
- self.print_status("Please ensure 1Password SSH agent is enabled in 1Password settings", None, 2)
188
- return False
189
-
190
- self.print_status("1Password SSH agent socket found", True, 2)
191
-
192
- # Check if agent is active by trying to list identities
193
- result = subprocess.run(
194
- ['ssh-add', '-l'],
195
- env={**os.environ, 'SSH_AUTH_SOCK': str(self.onepassword_agent_sock)},
196
- capture_output=True,
197
- text=True,
198
- check=False
199
- )
200
-
201
- if "Could not open a connection to your authentication agent" in result.stderr:
202
- self.print_status("1Password SSH agent is not running", False, 2)
203
- return False
204
-
205
- self.print_status("1Password SSH agent is running", True, 2)
206
-
207
- # Check if 1Password CLI is authenticated
208
- result = subprocess.run(
209
- ['op', 'account', 'list'],
210
- capture_output=True,
211
- text=True,
212
- check=False
213
- )
214
-
215
- if result.returncode != 0:
216
- self.print_status("1Password CLI is not authenticated. Run 'op signin' first.", False, 2)
217
- return False
218
-
219
- self.print_status("1Password CLI is authenticated", True, 2)
220
- return True
221
-
222
- except FileNotFoundError:
170
+ # Use our helper function to check 1Password CLI
171
+ installed, authenticated, version = check_1password_cli()
172
+
173
+ if not installed:
223
174
  self.print_status("1Password CLI not found. Please install it from https://1password.com/downloads/command-line/", False, 2)
224
175
  return False
225
- except Exception as e:
226
- self.print_status(f"Error checking 1Password: {str(e)}", False, 2)
176
+
177
+ self.print_status(f"1Password CLI {version} installed", True, 2)
178
+
179
+ if not authenticated:
180
+ self.print_status("1Password CLI is not authenticated. Run 'op signin' first.", False, 2)
227
181
  return False
182
+
183
+ self.print_status("1Password CLI is authenticated", True, 2)
184
+
185
+ # Check if 1Password SSH agent is running
186
+ agent_running = check_ssh_agent(str(self.onepassword_agent_sock))
187
+
188
+ if not agent_running:
189
+ self.print_status("1Password SSH agent is not running", False, 2)
190
+ self.print_status("Please ensure 1Password SSH agent is enabled in 1Password settings", None, 2)
191
+ return False
192
+
193
+ self.print_status("1Password SSH agent is running", True, 2)
194
+ return True
228
195
 
229
- def _store_key_in_1password(self) -> bool:
230
- """Store the SSH key in 1Password and offer to remove it from the filesystem.
196
+ def _create_1password_key(self) -> bool:
197
+ """Create a new SSH key in 1Password.
231
198
 
232
199
  Returns:
233
200
  bool: True if successful
234
201
  """
235
202
  try:
236
- # If key exists on filesystem, offer to store it in 1Password
237
- key_exists = self.ssh_key_file.exists() and (self.ssh_key_file.with_suffix('.pub')).exists()
238
-
239
- if not key_exists:
240
- self.print_status("No SSH key found to store in 1Password", False, 2)
241
- return False
242
-
243
- # Ask if user wants to store the key in 1Password
244
- store_in_1password = self.prompt("Would you like to store this SSH key in 1Password?", "Y").lower() == "y"
245
- if not store_in_1password:
246
- return True
247
-
248
- # Get vault to store the key in
249
- result = subprocess.run(
250
- ['op', 'vault', 'list', '--format=json'],
251
- capture_output=True,
252
- text=True,
253
- check=True
254
- )
255
-
256
- if result.returncode != 0:
257
- self.print_status("Error listing 1Password vaults", False, 2)
258
- return False
259
-
260
- vaults = json.loads(result.stdout)
203
+ # Get vaults to determine where to store the key
204
+ vaults = get_vaults()
261
205
  if not vaults:
262
206
  self.print_status("No 1Password vaults found", False, 2)
263
207
  return False
264
-
208
+
265
209
  # Display available vaults
210
+ self.print_status("Creating a new SSH key in 1Password", None, 2)
266
211
  print("\n\033[96mAvailable 1Password vaults:\033[0m")
267
212
  for i, vault in enumerate(vaults):
268
213
  print(f" {i+1}. {vault['name']}")
269
-
214
+
270
215
  # Let user select vault
271
216
  vault_num = self.prompt("Select vault number to store SSH key", "1")
272
217
  try:
@@ -278,46 +223,55 @@ class CloudXSetup:
278
223
  except ValueError:
279
224
  self.print_status("Invalid input", False, 2)
280
225
  return False
281
-
282
- # Read private key
283
- private_key = self.ssh_key_file.read_text()
284
226
 
285
227
  # Create a title for the 1Password item
286
228
  ssh_key_title = f"cloudX SSH Key - {self.ssh_key}"
287
229
 
288
- # Create item in 1Password
289
- # Format: op item create --category=sshkey --title="..." --vault="..." "privateKey=<private key>"
290
- result = subprocess.run(
291
- [
292
- 'op', 'item', 'create',
293
- '--category=sshkey',
294
- f'--title={ssh_key_title}',
295
- f'--vault={selected_vault}',
296
- f'privateKey={private_key}'
297
- ],
298
- capture_output=True,
299
- text=True,
300
- check=False
301
- )
302
-
303
- if result.returncode != 0:
304
- self.print_status(f"Error storing SSH key in 1Password: {result.stderr}", False, 2)
305
- return False
230
+ # Check if a key with this title already exists in 1Password
231
+ ssh_keys = list_ssh_keys()
232
+ existing_key = next((key for key in ssh_keys if key['title'] == ssh_key_title), None)
233
+
234
+ if existing_key:
235
+ self.print_status(f"SSH key '{ssh_key_title}' already exists in 1Password", True, 2)
236
+ # Get the public key
237
+ result = subprocess.run(
238
+ ['op', 'item', 'get', existing_key['id'], '--fields', 'public key'],
239
+ capture_output=True,
240
+ text=True,
241
+ check=False
242
+ )
243
+
244
+ if result.returncode == 0:
245
+ public_key = result.stdout.strip()
246
+ # Save it to the expected location
247
+ if save_public_key(public_key, f"{self.ssh_key_file}.pub"):
248
+ self.print_status(f"Saved existing public key to {self.ssh_key_file}.pub", True, 2)
249
+ return True
250
+ else:
251
+ # Create a new SSH key in 1Password
252
+ self.print_status(f"Creating new SSH key '{ssh_key_title}' in 1Password...", None, 2)
253
+ success, public_key, item_id = create_ssh_key(ssh_key_title, selected_vault)
306
254
 
307
- self.print_status("SSH key stored in 1Password successfully", True, 2)
308
-
309
- # Ask if user wants to remove private key from filesystem
310
- remove_private_key = self.prompt("Would you like to remove the private key from the filesystem? (for security)", "Y").lower() == "y"
311
- if remove_private_key:
312
- # We should only remove the private key, keep the public key
313
- self.ssh_key_file.unlink()
314
- self.print_status("Private key removed from filesystem", True, 2)
315
- self.print_status("\033[93mImportant: If you need to recover this key, it's now only available in 1Password\033[0m", None, 2)
255
+ if not success:
256
+ self.print_status("Failed to create SSH key in 1Password", False, 2)
257
+ return False
316
258
 
259
+ self.print_status("SSH key created successfully in 1Password", True, 2)
260
+
261
+ # Save the public key to the expected location
262
+ if save_public_key(public_key, f"{self.ssh_key_file}.pub"):
263
+ self.print_status(f"Saved public key to {self.ssh_key_file}.pub", True, 2)
264
+ return True
265
+ else:
266
+ self.print_status(f"Failed to save public key to {self.ssh_key_file}.pub", False, 2)
267
+ return False
268
+
269
+ # Remind user to enable the key in 1Password SSH agent
270
+ self.print_status("\033[93mImportant: Make sure the key is enabled in 1Password's SSH agent settings\033[0m", None, 2)
317
271
  return True
318
272
 
319
273
  except Exception as e:
320
- self.print_status(f"Error storing key in 1Password: {str(e)}", False, 2)
274
+ self.print_status(f"Error creating key in 1Password: {str(e)}", False, 2)
321
275
  return False
322
276
 
323
277
  def setup_ssh_key(self) -> bool:
@@ -334,21 +288,8 @@ class CloudXSetup:
334
288
  if op_available:
335
289
  self.print_status("Using 1Password SSH agent for authentication", True, 2)
336
290
 
337
- # Generate a key if it doesn't exist
338
- key_exists = self.ssh_key_file.exists() and (self.ssh_key_file.with_suffix('.pub')).exists()
339
- if not key_exists:
340
- self.print_status(f"No SSH key found for '{self.ssh_key}'", None, 2)
341
- create_key = self.prompt("Would you like to create a new SSH key?", "Y").lower() == "y"
342
- if create_key:
343
- # We'll continue to the standard key creation flow
344
- pass
345
- else:
346
- # User doesn't want a key, so we'll skip and just rely on 1Password
347
- return True
348
- else:
349
- # Key exists, offer to store it in 1Password
350
- self._store_key_in_1password()
351
- return True
291
+ # Always prefer to create keys in 1Password
292
+ return self._create_1password_key()
352
293
  else:
353
294
  proceed = self.prompt("1Password integration not available. Continue with standard SSH key setup?", "Y").lower() != "n"
354
295
  if not proceed:
@@ -393,14 +334,8 @@ class CloudXSetup:
393
334
  self.ssh_key_file.with_suffix('.pub').chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IROTH | stat.S_IRGRP) # 644 permissions
394
335
  self.print_status("Set key file permissions", True, 2)
395
336
 
396
- # For both standard setup and 1Password when a key needs to be created,
397
- # we'll use the same code to generate the key
337
+ # Standard key generation successful
398
338
  self.print_status("Key generated successfully", True, 2)
399
-
400
- # If using 1Password, offer to store the key there
401
- if self.use_1password:
402
- self._store_key_in_1password()
403
-
404
339
  return True
405
340
 
406
341
  except Exception as e:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cloudx-proxy
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: SSH proxy command to connect VSCode with Cloud9/CloudX instance using AWS Systems Manager
5
5
  Author-email: easytocloud <info@easytocloud.com>
6
6
  License: MIT License
@@ -0,0 +1,12 @@
1
+ cloudx_proxy/_1password.py,sha256=uxyCfVvO1eQrOfYRojst_LN2DV4fIwxM5moaQTn3wQY,5853
2
+ cloudx_proxy/__init__.py,sha256=ZZ2O_m9OFJm18AxMSuYJt4UjSuSqyJlYRaZMoets498,61
3
+ cloudx_proxy/_version.py,sha256=_F8vLxUxrAtC2alXNPGVa9l3P6_vLpQAzemS6QlnPGQ,511
4
+ cloudx_proxy/cli.py,sha256=kdrZydxL94BJrv6NnjIcceRqhoonBzMIx4vfm1Wl7qc,4104
5
+ cloudx_proxy/core.py,sha256=RF3bX5MQiokRKjYEPnfWdKywGdtoVUvV2xZqm9uOl1g,8135
6
+ cloudx_proxy/setup.py,sha256=jvv7ibJQ8svyjYYeVKwGa70L7RV2W7yS7JXEvKed3wI,33339
7
+ cloudx_proxy-0.4.2.dist-info/LICENSE,sha256=i7P2OR4zsJYsMWcCUDe_B9ZfGi9bU0K5I2nKfDrW_N8,1068
8
+ cloudx_proxy-0.4.2.dist-info/METADATA,sha256=YAHtMfsqZ1aDk1FryZhgV-Q_vr4GcdBO_mzs-gbEpK8,14037
9
+ cloudx_proxy-0.4.2.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
10
+ cloudx_proxy-0.4.2.dist-info/entry_points.txt,sha256=HGt743N2lVlKd7O1qWq3C0aEHyS5PjPnxzDHh7hwtSg,54
11
+ cloudx_proxy-0.4.2.dist-info/top_level.txt,sha256=2wtEote1db21j-VvkCJFfT-dLlauuG5indjggYh3xDg,13
12
+ cloudx_proxy-0.4.2.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- cloudx_proxy/__init__.py,sha256=ZZ2O_m9OFJm18AxMSuYJt4UjSuSqyJlYRaZMoets498,61
2
- cloudx_proxy/_version.py,sha256=l5eo51MdCumDFCp44TFT1JH8yCDo1krag-GJubLxnVo,511
3
- cloudx_proxy/cli.py,sha256=kdrZydxL94BJrv6NnjIcceRqhoonBzMIx4vfm1Wl7qc,4104
4
- cloudx_proxy/core.py,sha256=RF3bX5MQiokRKjYEPnfWdKywGdtoVUvV2xZqm9uOl1g,8135
5
- cloudx_proxy/setup.py,sha256=OGJszBaTzw94pB5B61h2i3fWath44DBRshfKYvlLu6U,36024
6
- cloudx_proxy-0.4.0.dist-info/LICENSE,sha256=i7P2OR4zsJYsMWcCUDe_B9ZfGi9bU0K5I2nKfDrW_N8,1068
7
- cloudx_proxy-0.4.0.dist-info/METADATA,sha256=XC0WQBkIzpUuMnb_sJkQgqT7RKPJnHHYq9XFwB1a2U8,14037
8
- cloudx_proxy-0.4.0.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
9
- cloudx_proxy-0.4.0.dist-info/entry_points.txt,sha256=HGt743N2lVlKd7O1qWq3C0aEHyS5PjPnxzDHh7hwtSg,54
10
- cloudx_proxy-0.4.0.dist-info/top_level.txt,sha256=2wtEote1db21j-VvkCJFfT-dLlauuG5indjggYh3xDg,13
11
- cloudx_proxy-0.4.0.dist-info/RECORD,,