illumio-pylo 0.3.11__py3-none-any.whl → 0.3.12__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.
@@ -9,7 +9,7 @@ import paramiko
9
9
  import illumio_pylo as pylo
10
10
  import click
11
11
  from illumio_pylo.API.CredentialsManager import get_all_credentials, create_credential_in_file, CredentialFileEntry, \
12
- create_credential_in_default_file, \
12
+ create_credential_in_default_file, delete_credential_from_file, \
13
13
  get_credentials_from_file, encrypt_api_key_with_paramiko_ssh_key_chacha20poly1305, \
14
14
  decrypt_api_key_with_paramiko_ssh_key_chacha20poly1305, get_supported_keys_from_ssh_agent, is_encryption_available
15
15
 
@@ -42,15 +42,42 @@ def fill_parser(parser: argparse.ArgumentParser):
42
42
  create_parser.add_argument('--verify-ssl', required=False, type=bool, default=None,
43
43
  help='Verify SSL')
44
44
 
45
+ update_parser = sub_parser.add_parser('update', help='Update a credential')
46
+ update_parser.add_argument('--name', required=False, type=str, default=None,
47
+ help='Name of the credential to update')
48
+
49
+ update_parser.add_argument('--fqdn', required=False, type=str, default=None,
50
+ help='FQDN of the PCE')
51
+ update_parser.add_argument('--port', required=False, type=int, default=None,
52
+ help='Port of the PCE')
53
+ update_parser.add_argument('--org', required=False, type=int, default=None,
54
+ help='Organization ID')
55
+ update_parser.add_argument('--api-user', required=False, type=str, default=None,
56
+ help='API user')
57
+ update_parser.add_argument('--verify-ssl', required=False, type=bool, default=None,
58
+ help='Verify SSL')
59
+
60
+ # Delete sub-command
61
+ delete_parser = sub_parser.add_parser('delete', help='Delete a credential')
62
+ delete_parser.add_argument('--name', required=False, type=str, default=None,
63
+ help='Name of the credential to delete')
64
+ delete_parser.add_argument('--yes', '-y', action='store_true', default=False,
65
+ help='Skip confirmation prompt')
66
+
67
+ # Web editor sub-command
68
+ web_editor_parser = sub_parser.add_parser('web-editor', help='Start web-based credential editor')
69
+ web_editor_parser.add_argument('--host', required=False, type=str, default='127.0.0.1',
70
+ help='Host to bind the web server to')
71
+ web_editor_parser.add_argument('--port', required=False, type=int, default=5000,
72
+ help='Port to bind the web server to')
73
+
45
74
 
46
75
  def __main(args, **kwargs):
47
76
  if args['sub_command'] == 'list':
48
77
  table = PrettyTable(field_names=["Name", "URL", "API User", "Originating File"])
49
- # all should be left justified
50
78
  table.align = "l"
51
79
 
52
80
  credentials = get_all_credentials()
53
- # sort credentials by name
54
81
  credentials.sort(key=lambda x: x.name)
55
82
 
56
83
  for credential in credentials:
@@ -153,6 +180,116 @@ def __main(args, **kwargs):
153
180
 
154
181
  print("OK! ({})".format(file_path))
155
182
 
183
+ elif args['sub_command'] == 'update':
184
+ # if name is not provided, prompt for it
185
+ if args['name'] is None:
186
+ wanted_name = click.prompt('> Input a Profile Name to update (ie: prod-pce)', type=str)
187
+ args['name'] = wanted_name
188
+
189
+ # find the credential by name
190
+ wanted_name = args['name']
191
+ found_profile = get_credentials_from_file(wanted_name, fail_with_an_exception=False)
192
+ if found_profile is None:
193
+ print("Cannot find a profile named '{}'".format(wanted_name))
194
+ print("Available profiles:")
195
+ credentials = get_all_credentials()
196
+ for credential in credentials:
197
+ print(" - {}".format(credential.name))
198
+ sys.exit(1)
199
+
200
+ print("Found profile '{}' to update in file '{}'".format(found_profile.name, found_profile.originating_file))
201
+
202
+ if args['fqdn'] is not None:
203
+ found_profile.fqdn = args['fqdn']
204
+ if args['port'] is not None:
205
+ found_profile.port = args['port']
206
+ if args['org'] is not None:
207
+ found_profile.org_id = args['org']
208
+ if args['api_user'] is not None:
209
+ found_profile.api_user = args['api_user']
210
+ if args['verify_ssl'] is not None:
211
+ found_profile.verify_ssl = args['verify_ssl']
212
+
213
+ # ask if user wants to update API key
214
+ update_api_key = click.prompt('> Do you want to update the API key? Y/N', type=bool)
215
+ if update_api_key:
216
+ api_key = click.prompt('> New API Key', hide_input=True)
217
+ found_profile.api_key = api_key
218
+ print()
219
+
220
+ if is_encryption_available():
221
+ encrypt_api_key = click.prompt('> Encrypt API (requires an SSH agent running and an RSA or Ed25519 key added to them) ? Y/N', type=bool)
222
+ if encrypt_api_key:
223
+ print("Available keys (ECDSA NISTPXXX keys and a few others are not supported and will be filtered out):")
224
+ ssh_keys = get_supported_keys_from_ssh_agent()
225
+
226
+ # display a table of keys
227
+ print_keys(keys=ssh_keys, display_index=True)
228
+ print()
229
+
230
+ index_of_selected_key = click.prompt('> Select key by ID#', type=click.IntRange(0, len(ssh_keys)-1))
231
+ selected_ssh_key = ssh_keys[index_of_selected_key]
232
+ print("Selected key: {} | {} | {}".format(selected_ssh_key.get_name(),
233
+ selected_ssh_key.get_fingerprint().hex(),
234
+ selected_ssh_key.comment))
235
+ print(" * encrypting API key with selected key (you may be prompted by your SSH agent for confirmation or PIN code) ...", flush=True, end="")
236
+ # encrypted_api_key = encrypt_api_key_with_paramiko_ssh_key_fernet(ssh_key=selected_ssh_key, api_key=found_profile.api_key)
237
+ encrypted_api_key = encrypt_api_key_with_paramiko_ssh_key_chacha20poly1305(ssh_key=selected_ssh_key, api_key=found_profile.api_key)
238
+ print("OK!")
239
+ print(" * trying to decrypt the encrypted API key...", flush=True, end="")
240
+ decrypted_api_key = decrypt_api_key_with_paramiko_ssh_key_chacha20poly1305(encrypted_api_key_payload=encrypted_api_key)
241
+ if decrypted_api_key != found_profile.api_key:
242
+ raise pylo.PyloEx("Decrypted API key does not match original API key")
243
+ print("OK!")
244
+ found_profile.api_key = encrypted_api_key
245
+
246
+ credentials_data: CredentialFileEntry = {
247
+ "name": found_profile.name,
248
+ "fqdn": found_profile.fqdn,
249
+ "port": found_profile.port,
250
+ "org_id": found_profile.org_id,
251
+ "api_user": found_profile.api_user,
252
+ "verify_ssl": found_profile.verify_ssl,
253
+ "api_key": found_profile.api_key
254
+ }
255
+
256
+ print("* Updating credential in file '{}'...".format(found_profile.originating_file), flush=True, end="")
257
+ create_credential_in_file(file_full_path=found_profile.originating_file, data=credentials_data, overwrite_existing_profile=True)
258
+ print("OK!")
259
+
260
+ elif args['sub_command'] == 'delete':
261
+ # if name is not provided, prompt for it
262
+ wanted_name = args['name']
263
+ if wanted_name is None:
264
+ wanted_name = click.prompt('> Input a Profile Name to delete (ie: prod-pce)', type=str)
265
+
266
+ # find the credential by name
267
+ found_profile = get_credentials_from_file(wanted_name, fail_with_an_exception=False)
268
+ if found_profile is None:
269
+ print("Cannot find a profile named '{}'".format(wanted_name))
270
+ print("Available profiles:")
271
+ credentials = get_all_credentials()
272
+ for credential in credentials:
273
+ print(" - {}".format(credential.name))
274
+ sys.exit(1)
275
+
276
+ print("Found profile '{}' in file '{}'".format(found_profile.name, found_profile.originating_file))
277
+ print(" - FQDN: {}".format(found_profile.fqdn))
278
+ print(" - Port: {}".format(found_profile.port))
279
+ print(" - Org ID: {}".format(found_profile.org_id))
280
+ print(" - API User: {}".format(found_profile.api_user))
281
+
282
+ # Confirm deletion unless --yes flag is provided
283
+ if not args['yes']:
284
+ confirm = click.prompt('> Are you sure you want to delete this credential? Y/N', type=bool)
285
+ if not confirm:
286
+ print("Deletion cancelled.")
287
+ sys.exit(0)
288
+
289
+ print("* Deleting credential...", flush=True, end="")
290
+ delete_credential_from_file(profile_name=found_profile.name, file_path=found_profile.originating_file)
291
+ print("OK!")
292
+
156
293
  elif args['sub_command'] == 'test':
157
294
  print("* Profile Tester command")
158
295
  wanted_name = args['name']
@@ -180,6 +317,9 @@ def __main(args, **kwargs):
180
317
  connector.objects_label_dimension_get()
181
318
  print("OK!")
182
319
 
320
+ elif args['sub_command'] == 'web-editor':
321
+ run_web_editor(host=args['host'], port=args['port'])
322
+
183
323
  else:
184
324
  raise pylo.PyloEx("Unknown sub-command '{}'".format(args['sub_command']))
185
325
 
@@ -189,7 +329,7 @@ command_object = Command(command_name, __main, fill_parser, credentials_manager_
189
329
 
190
330
  def print_keys(keys: list[paramiko.AgentKey], display_index=True) -> None:
191
331
 
192
- column_properties = [ # (name, width)
332
+ column_properties = [
193
333
  ("ID#", 4),
194
334
  ("Type", 20),
195
335
  ("Fingerprint", 40),
@@ -214,3 +354,238 @@ def print_keys(keys: list[paramiko.AgentKey], display_index=True) -> None:
214
354
  table.add_row(display_values)
215
355
 
216
356
  print(table)
357
+
358
+
359
+ def run_web_editor(host: str = '127.0.0.1', port: int = 5000) -> None:
360
+ """Start the Flask web server for credential management."""
361
+ try:
362
+ from flask import Flask, jsonify, request, send_from_directory
363
+ except ImportError:
364
+ print("Flask is not installed. Please install it with: pip install flask")
365
+ sys.exit(1)
366
+
367
+ # Determine paths for static files
368
+ current_dir = os.path.dirname(os.path.abspath(__file__))
369
+ web_editor_dir = os.path.join(current_dir, 'ui/credential_manager_ui')
370
+ # That directory should contain index.html, error if not
371
+ if not os.path.exists(os.path.join(web_editor_dir, 'index.html')):
372
+ print("Cannot find web editor static files in expected location: {}".format(web_editor_dir))
373
+ sys.exit(1)
374
+
375
+ app = Flask(__name__, static_folder=web_editor_dir)
376
+
377
+ # Serve static files
378
+ @app.route('/')
379
+ def index():
380
+ return send_from_directory(web_editor_dir, 'index.html')
381
+
382
+ @app.route('/static/<path:filename>')
383
+ def serve_static(filename):
384
+ return send_from_directory(web_editor_dir, filename)
385
+
386
+ # API: List all credentials
387
+ @app.route('/api/credentials', methods=['GET'])
388
+ def api_list_credentials():
389
+ credentials = get_all_credentials()
390
+ credentials.sort(key=lambda x: x.name)
391
+ result = []
392
+ for cred in credentials:
393
+ result.append({
394
+ 'name': cred.name,
395
+ 'fqdn': cred.fqdn,
396
+ 'port': cred.port,
397
+ 'org_id': cred.org_id,
398
+ 'api_user': cred.api_user,
399
+ 'verify_ssl': cred.verify_ssl,
400
+ 'originating_file': cred.originating_file
401
+ })
402
+ return jsonify(result)
403
+
404
+ # API: Get a single credential
405
+ @app.route('/api/credentials/<name>', methods=['GET'])
406
+ def api_get_credential(name):
407
+ found_profile = get_credentials_from_file(name, fail_with_an_exception=False)
408
+ if found_profile is None:
409
+ return jsonify({'error': 'Credential not found'}), 404
410
+ return jsonify({
411
+ 'name': found_profile.name,
412
+ 'fqdn': found_profile.fqdn,
413
+ 'port': found_profile.port,
414
+ 'org_id': found_profile.org_id,
415
+ 'api_user': found_profile.api_user,
416
+ 'verify_ssl': found_profile.verify_ssl,
417
+ 'originating_file': found_profile.originating_file
418
+ })
419
+
420
+ # API: Create a new credential
421
+ @app.route('/api/credentials', methods=['POST'])
422
+ def api_create_credential():
423
+ data = request.get_json()
424
+ if not data:
425
+ return jsonify({'error': 'No data provided'}), 400
426
+
427
+ required_fields = ['name', 'fqdn', 'port', 'org_id', 'api_user', 'api_key']
428
+ for field in required_fields:
429
+ if field not in data:
430
+ return jsonify({'error': f'Missing required field: {field}'}), 400
431
+
432
+ # Check if credential already exists
433
+ credentials = get_all_credentials()
434
+ for credential in credentials:
435
+ if credential.name == data['name']:
436
+ return jsonify({'error': f"A credential named '{data['name']}' already exists"}), 400
437
+
438
+ credentials_data: CredentialFileEntry = {
439
+ "name": data['name'],
440
+ "fqdn": data['fqdn'],
441
+ "port": int(data['port']),
442
+ "org_id": int(data['org_id']),
443
+ "api_user": data['api_user'],
444
+ "verify_ssl": data.get('verify_ssl', True),
445
+ "api_key": data['api_key']
446
+ }
447
+
448
+ # Handle encryption if requested
449
+ if data.get('encrypt') and data.get('ssh_key_index') is not None:
450
+ if is_encryption_available():
451
+ try:
452
+ ssh_keys = get_supported_keys_from_ssh_agent()
453
+ key_index = int(data['ssh_key_index'])
454
+ if 0 <= key_index < len(ssh_keys):
455
+ selected_ssh_key = ssh_keys[key_index]
456
+ encrypted_api_key = encrypt_api_key_with_paramiko_ssh_key_chacha20poly1305(
457
+ ssh_key=selected_ssh_key, api_key=data['api_key'])
458
+ # Verify encryption
459
+ decrypted_api_key = decrypt_api_key_with_paramiko_ssh_key_chacha20poly1305(
460
+ encrypted_api_key_payload=encrypted_api_key)
461
+ if decrypted_api_key != data['api_key']:
462
+ return jsonify({'error': 'Encryption verification failed'}), 500
463
+ credentials_data["api_key"] = encrypted_api_key
464
+ except Exception as e:
465
+ return jsonify({'error': f'Encryption failed: {str(e)}'}), 500
466
+
467
+ try:
468
+ if data.get('use_current_workdir'):
469
+ file_path = create_credential_in_file(file_full_path=os.getcwd(), data=credentials_data)
470
+ else:
471
+ file_path = create_credential_in_default_file(data=credentials_data)
472
+ return jsonify({'success': True, 'file_path': file_path})
473
+ except Exception as e:
474
+ return jsonify({'error': str(e)}), 500
475
+
476
+ # API: Update a credential
477
+ @app.route('/api/credentials/<name>', methods=['PUT'])
478
+ def api_update_credential(name):
479
+ data = request.get_json()
480
+ if not data:
481
+ return jsonify({'error': 'No data provided'}), 400
482
+
483
+ found_profile = get_credentials_from_file(name, fail_with_an_exception=False)
484
+ if found_profile is None:
485
+ return jsonify({'error': 'Credential not found'}), 404
486
+
487
+ # Update fields if provided
488
+ if 'fqdn' in data:
489
+ found_profile.fqdn = data['fqdn']
490
+ if 'port' in data:
491
+ found_profile.port = int(data['port'])
492
+ if 'org_id' in data:
493
+ found_profile.org_id = int(data['org_id'])
494
+ if 'api_user' in data:
495
+ found_profile.api_user = data['api_user']
496
+ if 'verify_ssl' in data:
497
+ found_profile.verify_ssl = data['verify_ssl']
498
+ if 'api_key' in data and data['api_key']:
499
+ found_profile.api_key = data['api_key']
500
+
501
+ # Handle encryption if requested
502
+ if data.get('encrypt') and data.get('ssh_key_index') is not None:
503
+ if is_encryption_available():
504
+ try:
505
+ ssh_keys = get_supported_keys_from_ssh_agent()
506
+ key_index = int(data['ssh_key_index'])
507
+ if 0 <= key_index < len(ssh_keys):
508
+ selected_ssh_key = ssh_keys[key_index]
509
+ encrypted_api_key = encrypt_api_key_with_paramiko_ssh_key_chacha20poly1305(
510
+ ssh_key=selected_ssh_key, api_key=found_profile.api_key)
511
+ decrypted_api_key = decrypt_api_key_with_paramiko_ssh_key_chacha20poly1305(
512
+ encrypted_api_key_payload=encrypted_api_key)
513
+ if decrypted_api_key != found_profile.api_key:
514
+ return jsonify({'error': 'Encryption verification failed'}), 500
515
+ found_profile.api_key = encrypted_api_key
516
+ except Exception as e:
517
+ return jsonify({'error': f'Encryption failed: {str(e)}'}), 500
518
+
519
+ credentials_data: CredentialFileEntry = {
520
+ "name": found_profile.name,
521
+ "fqdn": found_profile.fqdn,
522
+ "port": found_profile.port,
523
+ "org_id": found_profile.org_id,
524
+ "api_user": found_profile.api_user,
525
+ "verify_ssl": found_profile.verify_ssl,
526
+ "api_key": found_profile.api_key
527
+ }
528
+
529
+ try:
530
+ create_credential_in_file(file_full_path=found_profile.originating_file,
531
+ data=credentials_data, overwrite_existing_profile=True)
532
+ return jsonify({'success': True})
533
+ except Exception as e:
534
+ return jsonify({'error': str(e)}), 500
535
+
536
+ # API: Test a credential
537
+ @app.route('/api/credentials/<name>/test', methods=['POST'])
538
+ def api_test_credential(name):
539
+ found_profile = get_credentials_from_file(name, fail_with_an_exception=False)
540
+ if found_profile is None:
541
+ return jsonify({'error': 'Credential not found'}), 404
542
+
543
+ try:
544
+ connector = pylo.APIConnector.create_from_credentials_object(found_profile)
545
+ connector.objects_label_dimension_get()
546
+ return jsonify({'success': True, 'message': 'Connection successful'})
547
+ except Exception as e:
548
+ return jsonify({'error': str(e)}), 500
549
+
550
+ # API: Delete a credential
551
+ @app.route('/api/credentials/<name>', methods=['DELETE'])
552
+ def api_delete_credential(name):
553
+ found_profile = get_credentials_from_file(name, fail_with_an_exception=False)
554
+ if found_profile is None:
555
+ return jsonify({'error': 'Credential not found'}), 404
556
+
557
+ try:
558
+ delete_credential_from_file(profile_name=found_profile.name, file_path=found_profile.originating_file)
559
+ return jsonify({'success': True, 'message': f"Credential '{name}' deleted successfully"})
560
+ except Exception as e:
561
+ return jsonify({'error': str(e)}), 500
562
+
563
+ # API: Get SSH keys for encryption
564
+ @app.route('/api/ssh-keys', methods=['GET'])
565
+ def api_get_ssh_keys():
566
+ if not is_encryption_available():
567
+ return jsonify({'available': False, 'keys': []})
568
+
569
+ try:
570
+ ssh_keys = get_supported_keys_from_ssh_agent()
571
+ keys_list = []
572
+ for i, key in enumerate(ssh_keys):
573
+ keys_list.append({
574
+ 'index': i,
575
+ 'type': key.get_name(),
576
+ 'fingerprint': key.get_fingerprint().hex(),
577
+ 'comment': key.comment
578
+ })
579
+ return jsonify({'available': True, 'keys': keys_list})
580
+ except Exception as e:
581
+ return jsonify({'available': False, 'error': str(e), 'keys': []})
582
+
583
+ # API: Check encryption availability
584
+ @app.route('/api/encryption-status', methods=['GET'])
585
+ def api_encryption_status():
586
+ return jsonify({'available': is_encryption_available()})
587
+
588
+ print(f"Starting web editor at http://{host}:{port}")
589
+ print("Press Ctrl+C to stop the server")
590
+ app.run(host=host, port=port, debug=False)
591
+
@@ -2,10 +2,7 @@ import argparse
2
2
  from typing import Optional, List
3
3
 
4
4
  import illumio_pylo as pylo
5
- import json
6
- import hashlib
7
5
 
8
- from illumio_pylo import log
9
6
  from . import Command
10
7
  from illumio_pylo.API.JsonPayloadTypes import LabelObjectJsonStructure
11
8