cloudx-proxy 0.3.15__py3-none-any.whl → 0.4.1__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.
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.3.15'
21
- __version_tuple__ = version_tuple = (0, 3, 15)
20
+ __version__ = version = '0.4.1'
21
+ __version_tuple__ = version_tuple = (0, 4, 1)
cloudx_proxy/cli.py CHANGED
@@ -52,7 +52,8 @@ def connect(instance_id: str, port: int, profile: str, region: str, ssh_key: str
52
52
  @click.option('--ssh-key', default='vscode', help='SSH key name to use (default: vscode)')
53
53
  @click.option('--ssh-config', help='SSH config file to use (default: ~/.ssh/vscode/config)')
54
54
  @click.option('--aws-env', help='AWS environment directory (default: ~/.aws, use name of directory in ~/.aws/aws-envs/)')
55
- def setup(profile: str, ssh_key: str, ssh_config: str, aws_env: str):
55
+ @click.option('--1password', 'use_1password', is_flag=True, help='Use 1Password SSH agent for SSH authentication')
56
+ def setup(profile: str, ssh_key: str, ssh_config: str, aws_env: str, use_1password: bool):
56
57
  """Set up AWS profile, SSH keys, and configuration for CloudX.
57
58
 
58
59
  This command will:
@@ -65,9 +66,16 @@ def setup(profile: str, ssh_key: str, ssh_config: str, aws_env: str):
65
66
  cloudx-proxy setup
66
67
  cloudx-proxy setup --profile myprofile --ssh-key mykey
67
68
  cloudx-proxy setup --ssh-config ~/.ssh/cloudx/config
69
+ cloudx-proxy setup --1password
68
70
  """
69
71
  try:
70
- setup = CloudXSetup(profile=profile, ssh_key=ssh_key, ssh_config=ssh_config, aws_env=aws_env)
72
+ setup = CloudXSetup(
73
+ profile=profile,
74
+ ssh_key=ssh_key,
75
+ ssh_config=ssh_config,
76
+ aws_env=aws_env,
77
+ use_1password=use_1password
78
+ )
71
79
 
72
80
  print("\n\033[1;95m=== cloudx-proxy Setup ===\033[0m\n")
73
81
 
cloudx_proxy/core.py CHANGED
@@ -91,9 +91,21 @@ class CloudXProxy:
91
91
  return False
92
92
 
93
93
  def push_ssh_key(self) -> bool:
94
- """Push SSH public key to instance via EC2 Instance Connect."""
94
+ """Push SSH public key to instance via EC2 Instance Connect.
95
+
96
+ Determines which SSH key to use (regular key or 1Password-managed key),
97
+ then pushes the correct public key to the instance.
98
+ """
95
99
  try:
96
- with open(self.ssh_key) as f:
100
+ # Check if file exists with .pub extension (could be a non-1Password key)
101
+ # or if the .pub extension is already part of self.ssh_key (because of 1Password integration)
102
+ key_path = self.ssh_key
103
+ if not key_path.endswith('.pub'):
104
+ key_path += '.pub'
105
+
106
+ self.log(f"Using public key: {key_path}")
107
+
108
+ with open(key_path) as f:
97
109
  public_key = f.read()
98
110
 
99
111
  self.ec2_connect.send_ssh_public_key(
cloudx_proxy/setup.py CHANGED
@@ -7,9 +7,11 @@ 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
- def __init__(self, profile: str = "vscode", ssh_key: str = "vscode", ssh_config: str = None, aws_env: str = None):
13
+ def __init__(self, profile: str = "vscode", ssh_key: str = "vscode", ssh_config: str = None,
14
+ aws_env: str = None, use_1password: bool = False):
13
15
  """Initialize cloudx-proxy setup.
14
16
 
15
17
  Args:
@@ -17,11 +19,14 @@ class CloudXSetup:
17
19
  ssh_key: SSH key name (default: "vscode")
18
20
  ssh_config: SSH config file path (default: None, uses ~/.ssh/vscode/config)
19
21
  aws_env: AWS environment directory (default: None)
22
+ use_1password: Use 1Password SSH agent for authentication (default: False)
20
23
  """
21
24
  self.profile = profile
22
25
  self.ssh_key = ssh_key
23
26
  self.aws_env = aws_env
27
+ self.use_1password = use_1password
24
28
  self.home_dir = str(Path.home())
29
+ self.onepassword_agent_sock = Path(self.home_dir) / ".1password" / "agent.sock"
25
30
 
26
31
  # Set up ssh config paths based on provided config or default
27
32
  if ssh_config:
@@ -151,6 +156,124 @@ class CloudXSetup:
151
156
  self.print_status(f"\033[1;91mError:\033[0m {str(e)}", False, 2)
152
157
  return False
153
158
 
159
+ def _check_1password_availability(self) -> bool:
160
+ """Check if 1Password CLI and SSH agent are available.
161
+
162
+ Returns:
163
+ bool: True if 1Password is available and configured
164
+ """
165
+ if not self.use_1password:
166
+ return False
167
+
168
+ self.print_status("Checking 1Password availability...")
169
+
170
+ # Use our helper function to check 1Password CLI
171
+ installed, authenticated, version = check_1password_cli()
172
+
173
+ if not installed:
174
+ self.print_status("1Password CLI not found. Please install it from https://1password.com/downloads/command-line/", False, 2)
175
+ return False
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)
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
195
+
196
+ def _create_1password_key(self) -> bool:
197
+ """Create a new SSH key in 1Password.
198
+
199
+ Returns:
200
+ bool: True if successful
201
+ """
202
+ try:
203
+ # Get vaults to determine where to store the key
204
+ vaults = get_vaults()
205
+ if not vaults:
206
+ self.print_status("No 1Password vaults found", False, 2)
207
+ return False
208
+
209
+ # Display available vaults
210
+ self.print_status("Creating a new SSH key in 1Password", None, 2)
211
+ print("\n\033[96mAvailable 1Password vaults:\033[0m")
212
+ for i, vault in enumerate(vaults):
213
+ print(f" {i+1}. {vault['name']}")
214
+
215
+ # Let user select vault
216
+ vault_num = self.prompt("Select vault number to store SSH key", "1")
217
+ try:
218
+ vault_idx = int(vault_num) - 1
219
+ if vault_idx < 0 or vault_idx >= len(vaults):
220
+ self.print_status("Invalid vault number", False, 2)
221
+ return False
222
+ selected_vault = vaults[vault_idx]['id']
223
+ except ValueError:
224
+ self.print_status("Invalid input", False, 2)
225
+ return False
226
+
227
+ # Create a title for the 1Password item
228
+ ssh_key_title = f"cloudX SSH Key - {self.ssh_key}"
229
+
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)
254
+
255
+ if not success:
256
+ self.print_status("Failed to create SSH key in 1Password", False, 2)
257
+ return False
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)
271
+ return True
272
+
273
+ except Exception as e:
274
+ self.print_status(f"Error creating key in 1Password: {str(e)}", False, 2)
275
+ return False
276
+
154
277
  def setup_ssh_key(self) -> bool:
155
278
  """Set up SSH key pair.
156
279
 
@@ -158,6 +281,21 @@ class CloudXSetup:
158
281
  bool: True if key was set up successfully
159
282
  """
160
283
  self.print_header("SSH Key Configuration")
284
+
285
+ # Check 1Password integration if requested
286
+ if self.use_1password:
287
+ op_available = self._check_1password_availability()
288
+ if op_available:
289
+ self.print_status("Using 1Password SSH agent for authentication", True, 2)
290
+
291
+ # Always prefer to create keys in 1Password
292
+ return self._create_1password_key()
293
+ else:
294
+ proceed = self.prompt("1Password integration not available. Continue with standard SSH key setup?", "Y").lower() != "n"
295
+ if not proceed:
296
+ return False
297
+ self.use_1password = False # Fallback to standard setup
298
+
161
299
  self.print_status(f"Checking SSH key '{self.ssh_key}' configuration...")
162
300
 
163
301
  try:
@@ -196,6 +334,8 @@ class CloudXSetup:
196
334
  self.ssh_key_file.with_suffix('.pub').chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IROTH | stat.S_IRGRP) # 644 permissions
197
335
  self.print_status("Set key file permissions", True, 2)
198
336
 
337
+ # Standard key generation successful
338
+ self.print_status("Key generated successfully", True, 2)
199
339
  return True
200
340
 
201
341
  except Exception as e:
@@ -206,6 +346,75 @@ class CloudXSetup:
206
346
  return True
207
347
  return False
208
348
 
349
+ def _build_proxy_command(self) -> str:
350
+ """Build the ProxyCommand with appropriate parameters.
351
+
352
+ Returns:
353
+ str: The complete ProxyCommand string
354
+ """
355
+ proxy_command = "uvx cloudx-proxy connect %h %p"
356
+ if self.profile != "vscode":
357
+ proxy_command += f" --profile {self.profile}"
358
+ if self.aws_env:
359
+ proxy_command += f" --aws-env {self.aws_env}"
360
+ if self.ssh_key != "vscode":
361
+ proxy_command += f" --ssh-key {self.ssh_key}"
362
+
363
+ return proxy_command
364
+
365
+ def _build_auth_config(self) -> str:
366
+ """Build the authentication configuration block.
367
+
368
+ Returns:
369
+ str: SSH config authentication section
370
+ """
371
+ if self.use_1password:
372
+ # When using 1Password:
373
+ # 1. Set IdentityAgent to the 1Password socket
374
+ # 2. Set IdentityFile to the PUBLIC key (.pub) to limit key search
375
+ # 3. Set IdentitiesOnly to yes to avoid using ssh-agent keys
376
+ return f""" IdentityAgent {self.onepassword_agent_sock}
377
+ IdentityFile {self.ssh_key_file}.pub
378
+ IdentitiesOnly yes
379
+ """
380
+ else:
381
+ # Standard SSH key configuration
382
+ return f""" IdentityFile {self.ssh_key_file}
383
+ IdentitiesOnly yes
384
+ """
385
+
386
+ def _build_host_config(self, cloudx_env: str, hostname: str, instance_id: str, include_proxy: bool = True) -> str:
387
+ """Build a host configuration block.
388
+
389
+ Args:
390
+ cloudx_env: CloudX environment
391
+ hostname: Hostname for the instance
392
+ instance_id: EC2 instance ID (None for wildcard entries)
393
+ include_proxy: Whether to include the ProxyCommand (default: True)
394
+
395
+ Returns:
396
+ str: Complete host configuration block
397
+ """
398
+ host_pattern = hostname if hostname else "*"
399
+ host_entry = f"""
400
+ Host cloudx-{cloudx_env}-{host_pattern}
401
+ """
402
+ # Add HostName only for specific hosts, not for wildcard entries
403
+ if instance_id:
404
+ host_entry += f""" HostName {instance_id}
405
+ """
406
+ host_entry += """ User ec2-user
407
+ """
408
+ # Add authentication configuration
409
+ host_entry += self._build_auth_config()
410
+
411
+ # Add proxy command if requested
412
+ if include_proxy:
413
+ host_entry += f""" ProxyCommand {self._build_proxy_command()}
414
+ """
415
+
416
+ return host_entry
417
+
209
418
  def _add_host_entry(self, cloudx_env: str, instance_id: str, hostname: str, current_config: str) -> bool:
210
419
  """Add settings to a specific host entry.
211
420
 
@@ -219,25 +428,8 @@ class CloudXSetup:
219
428
  bool: True if settings were added successfully
220
429
  """
221
430
  try:
222
- # Build host entry with all settings
223
- proxy_command = "uvx cloudx-proxy connect %h %p"
224
- if self.profile != "vscode":
225
- proxy_command += f" --profile {self.profile}"
226
- if self.aws_env:
227
- proxy_command += f" --aws-env {self.aws_env}"
228
- if self.ssh_key != "vscode":
229
- proxy_command += f" --ssh-key {self.ssh_key}"
230
-
231
- host_entry = f"""
232
- Host cloudx-{cloudx_env}-{hostname}
233
- HostName {instance_id}
234
- User ec2-user
235
- """
236
- host_entry += f""" IdentityFile {self.ssh_key_file}
237
- IdentitiesOnly yes
238
- """
239
- host_entry += f""" ProxyCommand {proxy_command}
240
- """
431
+ # Generate the host entry using the consolidated helper method
432
+ host_entry = self._build_host_config(cloudx_env, hostname, instance_id)
241
433
 
242
434
  # Append host entry
243
435
  with open(self.ssh_config_file, 'a') as f:
@@ -367,30 +559,18 @@ Host cloudx-{cloudx_env}-{hostname}
367
559
 
368
560
  # Create base config
369
561
  self.print_status(f"Creating new config for cloudx-{cloudx_env}-*", None, 2)
370
- # Build ProxyCommand with only non-default parameters
371
- # We don't need to include ssh_config here as SSH will handle that through the config
372
- proxy_command = "uvx cloudx-proxy connect %h %p"
373
- if self.profile != "vscode":
374
- proxy_command += f" --profile {self.profile}"
375
- if self.aws_env:
376
- proxy_command += f" --aws-env {self.aws_env}"
377
- if self.ssh_key != "vscode":
378
- proxy_command += f" --ssh-key {self.ssh_key}"
379
-
562
+
380
563
  # Ensure control directory exists with proper permissions
381
564
  if not self._ensure_control_dir():
382
565
  return False
383
566
 
384
- # Build base configuration
385
- base_config = f"""# cloudx-proxy SSH Configuration
386
- Host cloudx-{cloudx_env}-*
387
- User ec2-user
388
- """
389
- # Add key configuration
390
- base_config += f""" IdentityFile {self.ssh_key_file}
391
- IdentitiesOnly yes
392
-
393
- """
567
+ # Build base configuration with wildcard hostname pattern
568
+ # Start with a header comment
569
+ base_config = "# cloudx-proxy SSH Configuration\n"
570
+
571
+ # Add base host pattern with wildcard
572
+ base_config += self._build_host_config(cloudx_env, None, None, include_proxy=True)
573
+
394
574
  # Add SSH multiplexing configuration
395
575
  control_path = "~/.ssh/control/%r@%h:%p"
396
576
  if platform.system() == 'Windows':
@@ -402,9 +582,6 @@ Host cloudx-{cloudx_env}-*
402
582
  ControlPath {control_path}
403
583
  ControlPersist 4h
404
584
 
405
- """
406
- # Add ProxyCommand
407
- base_config += f""" ProxyCommand {proxy_command}
408
585
  """
409
586
 
410
587
  # If file exists, append the new config, otherwise create it
@@ -421,12 +598,9 @@ Host cloudx-{cloudx_env}-*
421
598
  self.ssh_config_file.chmod(stat.S_IRUSR | stat.S_IWUSR) # 600 permissions (owner read/write)
422
599
  self.print_status("Set config file permissions to 600", True, 2)
423
600
 
424
- # Add specific host entry
601
+ # Add specific host entry using the consolidated helper method
425
602
  self.print_status(f"Adding host entry for cloudx-{cloudx_env}-{hostname}", None, 2)
426
- host_entry = f"""
427
- Host cloudx-{cloudx_env}-{hostname}
428
- HostName {instance_id}
429
- """
603
+ host_entry = self._build_host_config(cloudx_env, hostname, instance_id, include_proxy=False)
430
604
  with open(self.ssh_config_file, 'a') as f:
431
605
  f.write(host_entry)
432
606
  self.print_status("Host entry added", True, 2)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: cloudx-proxy
3
- Version: 0.3.15
3
+ Version: 0.4.1
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,11 @@
1
+ cloudx_proxy/__init__.py,sha256=ZZ2O_m9OFJm18AxMSuYJt4UjSuSqyJlYRaZMoets498,61
2
+ cloudx_proxy/_version.py,sha256=yF2DwGUoQKNnLhAbpZX8kCQKjw77EZzhRk7_OTftets,511
3
+ cloudx_proxy/cli.py,sha256=kdrZydxL94BJrv6NnjIcceRqhoonBzMIx4vfm1Wl7qc,4104
4
+ cloudx_proxy/core.py,sha256=RF3bX5MQiokRKjYEPnfWdKywGdtoVUvV2xZqm9uOl1g,8135
5
+ cloudx_proxy/setup.py,sha256=jvv7ibJQ8svyjYYeVKwGa70L7RV2W7yS7JXEvKed3wI,33339
6
+ cloudx_proxy-0.4.1.dist-info/LICENSE,sha256=i7P2OR4zsJYsMWcCUDe_B9ZfGi9bU0K5I2nKfDrW_N8,1068
7
+ cloudx_proxy-0.4.1.dist-info/METADATA,sha256=wFL6lV-shLLfI6cn8H5ZItIF944-TrX67LJC0Ml-muc,14037
8
+ cloudx_proxy-0.4.1.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
9
+ cloudx_proxy-0.4.1.dist-info/entry_points.txt,sha256=HGt743N2lVlKd7O1qWq3C0aEHyS5PjPnxzDHh7hwtSg,54
10
+ cloudx_proxy-0.4.1.dist-info/top_level.txt,sha256=2wtEote1db21j-VvkCJFfT-dLlauuG5indjggYh3xDg,13
11
+ cloudx_proxy-0.4.1.dist-info/RECORD,,
@@ -1,11 +0,0 @@
1
- cloudx_proxy/__init__.py,sha256=ZZ2O_m9OFJm18AxMSuYJt4UjSuSqyJlYRaZMoets498,61
2
- cloudx_proxy/_version.py,sha256=HSn5cGZLA_vnXREa9sRtSYCA5Ii9CJlQbi1YMzsfUGM,513
3
- cloudx_proxy/cli.py,sha256=tP-Ftf2VNKtaiu26x5nDKCMZOPKoa4XCNDd5-okoE4s,3827
4
- cloudx_proxy/core.py,sha256=iHloywyiDcWRXzFxgX0TdcOPHujW2u83WUMllk4m9Es,7588
5
- cloudx_proxy/setup.py,sha256=zZMMGAAQMqXlfVJ8_M4f5-SEcYqsFQ82kQv18ikdta0,25409
6
- cloudx_proxy-0.3.15.dist-info/LICENSE,sha256=i7P2OR4zsJYsMWcCUDe_B9ZfGi9bU0K5I2nKfDrW_N8,1068
7
- cloudx_proxy-0.3.15.dist-info/METADATA,sha256=qOT0s-RuqhXnU69PHxWELyeBKNgluZyHpO-R6O5d2q8,14038
8
- cloudx_proxy-0.3.15.dist-info/WHEEL,sha256=jB7zZ3N9hIM9adW7qlTAyycLYW9npaWKLRzaoVcLKcM,91
9
- cloudx_proxy-0.3.15.dist-info/entry_points.txt,sha256=HGt743N2lVlKd7O1qWq3C0aEHyS5PjPnxzDHh7hwtSg,54
10
- cloudx_proxy-0.3.15.dist-info/top_level.txt,sha256=2wtEote1db21j-VvkCJFfT-dLlauuG5indjggYh3xDg,13
11
- cloudx_proxy-0.3.15.dist-info/RECORD,,