pw-connector 0.3.0__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.
Files changed (56) hide show
  1. pw_connector-0.3.0/LICENSE +21 -0
  2. pw_connector-0.3.0/PKG-INFO +39 -0
  3. pw_connector-0.3.0/README.md +27 -0
  4. pw_connector-0.3.0/cli/__init__.py +2 -0
  5. pw_connector-0.3.0/cli/commands/__init__.py +20 -0
  6. pw_connector-0.3.0/cli/commands/api_command.py +106 -0
  7. pw_connector-0.3.0/cli/commands/base.py +79 -0
  8. pw_connector-0.3.0/cli/commands/exec_command.py +220 -0
  9. pw_connector-0.3.0/cli/commands/get_command.py +80 -0
  10. pw_connector-0.3.0/cli/commands/recovery_command.py +262 -0
  11. pw_connector-0.3.0/cli/commands/rsa_command.py +204 -0
  12. pw_connector-0.3.0/cli/commands/shamir_command.py +212 -0
  13. pw_connector-0.3.0/cli/commands/update_command.py +109 -0
  14. pw_connector-0.3.0/cli/commands/users_command.py +243 -0
  15. pw_connector-0.3.0/cli/file_utils.py +70 -0
  16. pw_connector-0.3.0/cli/main.py +105 -0
  17. pw_connector-0.3.0/cli/utils.py +76 -0
  18. pw_connector-0.3.0/passwork_client/__init__.py +3 -0
  19. pw_connector-0.3.0/passwork_client/base32.py +103 -0
  20. pw_connector-0.3.0/passwork_client/constants/__init__.py +8 -0
  21. pw_connector-0.3.0/passwork_client/constants/link_expiration_time.py +5 -0
  22. pw_connector-0.3.0/passwork_client/constants/link_type.py +3 -0
  23. pw_connector-0.3.0/passwork_client/crypto.py +291 -0
  24. pw_connector-0.3.0/passwork_client/exceptions.py +19 -0
  25. pw_connector-0.3.0/passwork_client/modules/__init__.py +30 -0
  26. pw_connector-0.3.0/passwork_client/modules/api_client.py +228 -0
  27. pw_connector-0.3.0/passwork_client/modules/app.py +16 -0
  28. pw_connector-0.3.0/passwork_client/modules/batch.py +24 -0
  29. pw_connector-0.3.0/passwork_client/modules/inbox.py +40 -0
  30. pw_connector-0.3.0/passwork_client/modules/item.py +205 -0
  31. pw_connector-0.3.0/passwork_client/modules/link.py +73 -0
  32. pw_connector-0.3.0/passwork_client/modules/master_key.py +73 -0
  33. pw_connector-0.3.0/passwork_client/modules/session.py +61 -0
  34. pw_connector-0.3.0/passwork_client/modules/shortcut.py +100 -0
  35. pw_connector-0.3.0/passwork_client/modules/snapshot.py +45 -0
  36. pw_connector-0.3.0/passwork_client/modules/user.py +106 -0
  37. pw_connector-0.3.0/passwork_client/modules/vault.py +62 -0
  38. pw_connector-0.3.0/passwork_client/modules/vault_type.py +86 -0
  39. pw_connector-0.3.0/passwork_client/passwork_client.py +51 -0
  40. pw_connector-0.3.0/passwork_client/shamir/__init__.py +17 -0
  41. pw_connector-0.3.0/passwork_client/shamir/activity_log.py +155 -0
  42. pw_connector-0.3.0/passwork_client/shamir/audit.py +274 -0
  43. pw_connector-0.3.0/passwork_client/shamir/holder_rsa.py +77 -0
  44. pw_connector-0.3.0/passwork_client/shamir/primitives.py +140 -0
  45. pw_connector-0.3.0/passwork_client/shamir/recovery.py +86 -0
  46. pw_connector-0.3.0/passwork_client/shamir/split_helper.py +25 -0
  47. pw_connector-0.3.0/passwork_client/utils.py +283 -0
  48. pw_connector-0.3.0/pw_connector.egg-info/PKG-INFO +39 -0
  49. pw_connector-0.3.0/pw_connector.egg-info/SOURCES.txt +54 -0
  50. pw_connector-0.3.0/pw_connector.egg-info/dependency_links.txt +1 -0
  51. pw_connector-0.3.0/pw_connector.egg-info/entry_points.txt +2 -0
  52. pw_connector-0.3.0/pw_connector.egg-info/requires.txt +5 -0
  53. pw_connector-0.3.0/pw_connector.egg-info/top_level.txt +2 -0
  54. pw_connector-0.3.0/pyproject.toml +9 -0
  55. pw_connector-0.3.0/setup.cfg +4 -0
  56. pw_connector-0.3.0/setup.py +62 -0
@@ -0,0 +1,21 @@
1
+ Лицензия MIT
2
+
3
+ Авторские права (c) 2026 ООО Пассворк
4
+
5
+ Настоящим предоставляется бесплатное разрешение любому лицу, получившему копию
6
+ данного программного обеспечения и сопутствующей документации (далее — «Программное обеспечение»),
7
+ использовать Программное обеспечение без ограничений, включая, но не ограничиваясь,
8
+ права на использование, копирование, изменение, объединение, публикацию, распространение,
9
+ сублицензирование и/или продажу копий Программного обеспечения, а также лицам, которым
10
+ предоставляется данное Программное обеспечение, при соблюдении следующих условий:
11
+
12
+ Указанное выше уведомление об авторских правах и данное разрешение должны быть включены
13
+ во все копии или значимые части Программного обеспечения.
14
+
15
+ ПРОГРАММНОЕ ОБЕСПЕЧЕНИЕ ПРЕДОСТАВЛЯЕТСЯ «КАК ЕСТЬ», БЕЗ КАКИХ-ЛИБО ГАРАНТИЙ,
16
+ ЯВНЫХ ИЛИ ПОДРАЗУМЕВАЕМЫХ, ВКЛЮЧАЯ, НО НЕ ОГРАНИЧИВАЯСЬ, ГАРАНТИИ ТОВАРНОЙ ПРИГОДНОСТИ,
17
+ СООТВЕТСТВИЯ ПО ЕГО КОНКРЕТНОМУ НАЗНАЧЕНИЮ И ОТСУТСТВИЯ НАРУШЕНИЙ ПРАВ. НИ В КАКОМ СЛУЧАЕ
18
+ АВТОРЫ ИЛИ ПРАВООБЛАДАТЕЛИ НЕ НЕСУТ ОТВЕТСТВЕННОСТИ ПО ИСКАМ О ВОЗМЕЩЕНИИ УЩЕРБА, УБЫТКОВ
19
+ ИЛИ ДРУГИХ ТРЕБОВАНИЙ ПО ДЕЙСТВУЮЩИМ КОНТРАКТАМ, ДЕЛИКТАМ ИЛИ ИНОМУ, ВОЗНИКШИМ ИЗ, ИМЕЮЩИМ
20
+ ПРИЧИНОЙ ИЛИ СВЯЗАННЫМ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ ИЛИ ИСПОЛЬЗОВАНИЕМ ПРОГРАММНОГО ОБЕСПЕЧЕНИЯ
21
+ ИЛИ ИНЫМИ ДЕЙСТВИЯМИ С ПРОГРАММНЫМ ОБЕСПЕЧЕНИЕМ.
@@ -0,0 +1,39 @@
1
+ Metadata-Version: 2.4
2
+ Name: pw-connector
3
+ Version: 0.3.0
4
+ Summary: Python SDK and CLI for a zero-knowledge password vault API
5
+ Home-page: https://gitverse.ru/passwork-ru/passwork-python
6
+ Author: pw-connector
7
+ License: MIT
8
+ Project-URL: Issues, https://gitverse.ru/passwork-ru/passwork-python/issues
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Programming Language :: Python :: 3 :: Only
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Topic :: Security :: Cryptography
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.11
18
+ Description-Content-Type: text/markdown
19
+ License-File: LICENSE
20
+ Requires-Dist: requests<3.0,>=2.31
21
+ Requires-Dist: python-dotenv<2.0,>=1.0
22
+ Requires-Dist: cryptography<49,>=48.0.1
23
+ Requires-Dist: pbkdf2<2.0,>=1.3
24
+ Requires-Dist: horcrux==1.3.0
25
+ Dynamic: author
26
+ Dynamic: classifier
27
+ Dynamic: description
28
+ Dynamic: description-content-type
29
+ Dynamic: home-page
30
+ Dynamic: license
31
+ Dynamic: license-file
32
+ Dynamic: project-url
33
+ Dynamic: requires-dist
34
+ Dynamic: requires-python
35
+ Dynamic: summary
36
+
37
+ # pw-connector
38
+
39
+ Python SDK and CLI for a zero-knowledge password vault API (client-side encryption, vault/item operations, DevOps secret injection).
@@ -0,0 +1,27 @@
1
+ ## Python-коннектор, CLI и Docker для Пассворка
2
+
3
+ Официальный репозиторий Python-коннектора, CLI-утилиты и Docker-образов для интеграции с API Пассворка. Используйте Python-коннектор для программного доступа и клиентского шифрования, CLI для DevOps-задач и прямых вызовов API, а Docker — для запуска инструментов в пайплайнах.
4
+
5
+ ### Компоненты
6
+
7
+ - `passwork_client/` — Python-библиотека для программной интеграции с Пассворком. Обеспечивает клиентское шифрование (работа с мастер-ключом), управление сессиями и удобные API для CRUD-операций над объектами Пассворка (паролями, сейфами, пользователями, ярлыками, снимками). Подходит для бэкенд-сервисов, скриптов автоматизации и кастомных инструментов.
8
+ - `cli/` — `passwork-cli`, консольная утилита для DevOps и SRE, позволяющая использовать Пассворк в качестве менеджера секретов. Получайте и внедряйте секреты в переменные окружения или параметры команд, ищите по тегам и папкам, работайте с пользовательскими полями, обновляйте токены и выполняйте прямые вызовы API. Предназначена для локального использования, серверов и эфемерных CI-агентов.
9
+ - `docker/` — Docker-образы для запуска CLI в контейнерах в рамках CI/CD (GitHub Actions, GitLab CI, Bitbucket Pipelines и др.). Обеспечивает изолированную среду выполнения без локальных зависимостей Python; включает Dockerfile и примеры конфигурации пайплайнов для быстрой интеграции.
10
+
11
+ ### Документация
12
+
13
+ - [Введение в API и интеграции](https://passwork.ru/docs/api-and-integrations/intro/)
14
+ - [Руководство по Python-коннектору](https://passwork.ru/docs/api-and-integrations/python-connector/)
15
+ - [Руководство по CLI-утилите](https://passwork.ru/docs/api-and-integrations/cli-utility/)
16
+ - [Docker-контейнер для CLI](https://passwork.ru/docs/api-and-integrations/docker-container-for-cli/)
17
+ - [Восстановление доступа по схеме Шамира](https://passwork.ru/docs/recovery/intro)
18
+
19
+ ### Примеры использования
20
+
21
+ Примеры работы с Python-коннектором и CLI находятся в отдельном репозитории:
22
+
23
+ - Примеры интеграции с API Пассворка (включая восстановление по схеме Шамира): [passwork-ru/passwork-integration-examples](https://gitverse.ru/passwork-ru/passwork-integration-examples)
24
+
25
+ ### Восстановление по схеме Шамира
26
+
27
+ Пороговое восстановление доступа администратора к сейфу (M из N): `passwork-cli shamir` / `rsa` / `users create-service-account` / `recovery grant-admin`. Требуются клиентское шифрование и Python ≥ 3.11 (`horcrux==1.3.0`). Подробности — в документации и примерах выше.
@@ -0,0 +1,2 @@
1
+ # This file marks the directory as a Python package
2
+ from .main import main
@@ -0,0 +1,20 @@
1
+ from .exec_command import ExecuteCommandStrategy
2
+ from .api_command import ApiCallStrategy
3
+ from .get_command import GetCommandStrategy
4
+ from .update_command import UpdateCommandStrategy
5
+ from .shamir_command import ShamirCommand
6
+ from .rsa_command import RsaCommand
7
+ from .users_command import UsersCommand
8
+ from .recovery_command import RecoveryCommand
9
+
10
+ # Create a mapping of command names to strategy classes
11
+ COMMAND_STRATEGIES = {
12
+ "exec": ExecuteCommandStrategy,
13
+ "api": ApiCallStrategy,
14
+ "get": GetCommandStrategy,
15
+ "update": UpdateCommandStrategy,
16
+ "shamir": ShamirCommand,
17
+ "rsa": RsaCommand,
18
+ "users": UsersCommand,
19
+ "recovery": RecoveryCommand,
20
+ }
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+ import json
4
+ from .base import PassworkCommand
5
+
6
+
7
+ class ApiCallStrategy(PassworkCommand):
8
+ """
9
+ Strategy for making direct API calls to the Passwork API.
10
+ """
11
+ def execute(self, client, args):
12
+ try:
13
+ # Parse the parameters if provided
14
+ payload = {}
15
+ if args.params:
16
+ try:
17
+ payload = json.loads(args.params)
18
+ except json.JSONDecodeError as e:
19
+ print(f"Error parsing parameters JSON: {e}", file=sys.stderr)
20
+ return 1
21
+
22
+ # Format the endpoint correctly (add /api/ prefix)
23
+ endpoint = args.endpoint
24
+ # Remove leading slash if present
25
+ if endpoint.startswith('/'):
26
+ endpoint = endpoint[1:]
27
+ # Add /api/ prefix
28
+ endpoint = f"/api/{endpoint}"
29
+
30
+ # Execute the API call using the updated call method
31
+ response = client.call(
32
+ method=args.method,
33
+ endpoint=endpoint,
34
+ payload=payload
35
+ )
36
+
37
+ # Extract field from response if specified
38
+ if args.field:
39
+ extracted_data = self._extract_field(response, args.field)
40
+ # Print the extracted data
41
+ print(json.dumps(extracted_data, indent=2))
42
+ else:
43
+ # Print the full response as JSON
44
+ print(json.dumps(response, indent=2))
45
+
46
+ return 0
47
+
48
+ except Exception as e:
49
+ print(f"Error making API call: {e}", file=sys.stderr)
50
+ return 1
51
+
52
+ def _extract_field(self, data, field_name):
53
+ """
54
+ Extract a specific field from the API response.
55
+ If the response is an array, extract the field from each item.
56
+ Args:
57
+ data: The API response data (dict or list)
58
+ field_name: Name of the field to extract
59
+ Returns:
60
+ The extracted field value or list of values
61
+ """
62
+ # If data is a list, extract field from each item
63
+ if isinstance(data, list):
64
+ result = []
65
+ for item in data:
66
+ if isinstance(item, dict) and field_name in item:
67
+ result.append(item[field_name])
68
+ return result
69
+
70
+ # If data is a dict, extract the field
71
+ elif isinstance(data, dict):
72
+ # Handle nested fields using dot notation (e.g., "user.name")
73
+ if "." in field_name:
74
+ parts = field_name.split(".", 1) # Split into first part and rest
75
+ if parts[0] in data and isinstance(data[parts[0]], (dict, list)):
76
+ return self._extract_field(data[parts[0]], parts[1])
77
+ return None
78
+
79
+ # Handle extracting from a nested array using brackets (e.g., "items[0]")
80
+ elif "[" in field_name and field_name.endswith("]"):
81
+ base_name, index_str = field_name.split("[", 1)
82
+ index = int(index_str[:-1]) # Remove the closing bracket and convert to int
83
+
84
+ if base_name in data and isinstance(data[base_name], list) and 0 <= index < len(data[base_name]):
85
+ return data[base_name][index]
86
+ return None
87
+
88
+ # Simple field extraction
89
+ elif field_name in data:
90
+ return data[field_name]
91
+
92
+ # Field not found or data type not supported
93
+ return None
94
+
95
+ @staticmethod
96
+ def add_command(subparsers):
97
+ params = PassworkCommand.base_params() | {
98
+ "--method": {"required": True, "choices": ["GET", "POST", "PUT", "PATCH", "DELETE"], "help": "HTTP method"},
99
+ "--endpoint": {"required": True, "help": "API endpoint (e.g. v1/items) without leading /api/"},
100
+ "--params": {"help": "JSON string of parameters to pass to the API call"},
101
+ "--field": {"help": "Field to extract from the API response"}
102
+ }
103
+
104
+ parser = subparsers.add_parser("api", help="Make a direct API call to Passwork")
105
+ for key, value in params.items():
106
+ parser.add_argument(key, **value)
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ from abc import ABC, abstractmethod
3
+
4
+
5
+ class PassworkCommand(ABC):
6
+ """
7
+ Abstract base class for Passwork CLI command strategies.
8
+ """
9
+ requires_client = True
10
+
11
+ @abstractmethod
12
+ def execute(self, client, args):
13
+ """
14
+ Execute the command strategy.
15
+ Args:
16
+ client (PassworkClient): Initialized Passwork client
17
+ args (Namespace): Command line arguments
18
+ Returns:
19
+ int: Exit code
20
+ """
21
+ pass
22
+
23
+ @staticmethod
24
+ def add_command(subparsers):
25
+ pass
26
+
27
+ @staticmethod
28
+ def base_params():
29
+ return {
30
+ "--host": {"help": "Passwork API host URL"},
31
+ "--token": {"help": "Passwork access token"},
32
+ "--refresh-token": {"help": "Passwork refresh token"},
33
+ "--master-key": {"help": "Passwork master key for decryption"},
34
+ "--no-ssl-verify": {
35
+ "action": "store_true",
36
+ "help": "Disable SSL certificate verification. DEPRECATED: use --ssl-verify (boolean mode)",
37
+ },
38
+ "--ssl-verify": {
39
+ "help": "Enable SSL verification (or provide CA bundle path)",
40
+ "nargs": "?",
41
+ "const": True,
42
+ },
43
+ }
44
+
45
+
46
+ class ItemBaseCommand:
47
+ def _get_password(self, client, args):
48
+ """
49
+ Get password based on the provided arguments.
50
+ Strategy: If password_id is provided, get a single password
51
+ If shortcut_id is provided, get a single password
52
+ Returns:
53
+ dict: password object or shortcut object
54
+ """
55
+ # Case 1: Password ID
56
+ if hasattr(args, 'password_id') and args.password_id:
57
+ # Trim the parameter
58
+ password_id = args.password_id.strip()
59
+ if not password_id:
60
+ return {}
61
+
62
+ # Single ID
63
+ return client.get_item(password_id)
64
+
65
+ # Case 1: Shortcut ID(s)
66
+ if hasattr(args, 'shortcut_id') and args.shortcut_id:
67
+ # Trim the parameter
68
+ shortcut_id = args.shortcut_id.strip()
69
+ if not shortcut_id:
70
+ return {}
71
+
72
+ # Single ID
73
+ shortcut = client.get_shortcut(shortcut_id)
74
+ if shortcut['password']:
75
+ return shortcut['password']
76
+
77
+ return {}
78
+
79
+ raise ValueError("No password ID or shortcut ID criteria provided")
@@ -0,0 +1,220 @@
1
+ #!/usr/bin/env python3
2
+ import os
3
+ import re
4
+ import sys
5
+ import subprocess
6
+ from .base import PassworkCommand
7
+
8
+
9
+ class ExecuteCommandStrategy(PassworkCommand):
10
+ """
11
+ Strategy for retrieving passwords and executing a command with them
12
+ as environment variables.
13
+ Supports:
14
+ 1. Single password by ID
15
+ 2. Multiple passwords by IDs
16
+ 3. Password search by vault ID, folder ID, or tags
17
+ """
18
+ def execute(self, client, args):
19
+ try:
20
+ # Get passwords based on provided parameters
21
+ passwords = self._get_passwords(client, args)
22
+
23
+ if not passwords:
24
+ print("Error: No passwords found", file=sys.stderr)
25
+ return 1
26
+
27
+ # Set up environment with passwords
28
+ env = os.environ.copy()
29
+
30
+ # Add each password to environment variables
31
+ for password_item in passwords:
32
+ # Use sanitized password name as environment variable name
33
+ env_var_name = self._sanitize_env_var_name(password_item.get("name", "PASSWORD"))
34
+
35
+ item = password_item
36
+ if "password" in password_item and type(password_item["password"]) is dict:
37
+ item = password_item["password"]
38
+
39
+ # Set password as environment variable
40
+ if "password" in item:
41
+ env[env_var_name] = item["password"]
42
+
43
+ # Process custom fields of type "text" or "password"
44
+ if "customs" in item and item["customs"]:
45
+ for custom in item["customs"]:
46
+ if custom.get("type") in ["text", "password"] and "name" in custom and "value" in custom:
47
+ # Sanitize custom field name to make a valid environment variable
48
+ custom_env_var = self._sanitize_env_var_name(custom["name"])
49
+ env[custom_env_var] = custom["value"]
50
+
51
+ # Execute the command
52
+ process = subprocess.run(args.cmd, shell=True, env=env)
53
+
54
+ return process.returncode
55
+
56
+ except Exception as e:
57
+ print(f"Error executing command: {e}", file=sys.stderr)
58
+ return 1
59
+
60
+ def _get_passwords(self, client, args):
61
+ """
62
+ Get passwords based on the provided arguments.
63
+ Strategy:
64
+ 1. If password_id is provided, get a single password or multiple (if comma-separated)
65
+ 2. If search parameters are provided, search for passwords
66
+ Returns:
67
+ list: List of password objects
68
+ """
69
+ # Case 1: Password ID(s)
70
+ if hasattr(args, 'password_id') and args.password_id:
71
+ # Trim the parameter
72
+ password_id = args.password_id.strip()
73
+ if not password_id:
74
+ return []
75
+
76
+ # Check if it contains multiple IDs (comma-separated)
77
+ if ',' in password_id:
78
+ # Split by comma, clean each value and filter out empty ones
79
+ id_list = [id.strip() for id in password_id.split(',')]
80
+ id_list = [id for id in id_list if id]
81
+
82
+ if not id_list:
83
+ return []
84
+
85
+ # If we have multiple IDs, get all of them
86
+ if len(id_list) > 1:
87
+ return client.get_items(id_list)
88
+ # If we only have one ID after cleaning, use it as a single ID
89
+ else:
90
+ password = client.get_item(id_list[0])
91
+ return [password] if password else []
92
+
93
+ # Single ID
94
+ password = client.get_item(password_id)
95
+ return [password] if password else []
96
+
97
+ # Case 1: Shortcut ID(s)
98
+ if hasattr(args, 'shortcut_id') and args.shortcut_id:
99
+ # Trim the parameter
100
+ shortcut_id = args.shortcut_id.strip()
101
+ if not shortcut_id:
102
+ return []
103
+
104
+ # Check if it contains multiple IDs (comma-separated)
105
+ if ',' in shortcut_id:
106
+ # Split by comma, clean each value and filter out empty ones
107
+ id_list = [id.strip() for id in shortcut_id.split(',')]
108
+ id_list = [id for id in id_list if id]
109
+
110
+ if not id_list:
111
+ return []
112
+
113
+ # If we have multiple IDs, get all of them
114
+ if len(id_list) > 1:
115
+ return client.get_shortcut_items(id_list)
116
+ # If we only have one ID after cleaning, use it as a single ID
117
+ else:
118
+ shortcut = client.get_shortcut(id_list[0])
119
+ return [shortcut] if shortcut else []
120
+
121
+ # Single ID
122
+ shortcut = client.get_shortcut(shortcut_id)
123
+ return [shortcut] if shortcut else []
124
+
125
+ # Case 2: Search parameters
126
+ search_params = {}
127
+
128
+ # Check if any search parameter is provided and process it
129
+ # Vault IDs
130
+ if hasattr(args, 'vault_id') and args.vault_id:
131
+ # Trim and validate
132
+ vault_id = args.vault_id.strip()
133
+ if vault_id:
134
+ # Process as list if contains commas
135
+ if ',' in vault_id:
136
+ values = [item.strip() for item in vault_id.split(',')]
137
+ values = [item for item in values if item]
138
+ if values:
139
+ search_params['vault_ids'] = values
140
+ else:
141
+ search_params['vault_ids'] = [vault_id]
142
+
143
+ # Folder IDs
144
+ if hasattr(args, 'folder_id') and args.folder_id:
145
+ # Trim and validate
146
+ folder_id = args.folder_id.strip()
147
+ if folder_id:
148
+ # Process as list if contains commas
149
+ if ',' in folder_id:
150
+ values = [item.strip() for item in folder_id.split(',')]
151
+ values = [item for item in values if item]
152
+ if values:
153
+ search_params['folder_ids'] = values
154
+ else:
155
+ search_params['folder_ids'] = [folder_id]
156
+
157
+ # Tags
158
+ if hasattr(args, 'tags') and args.tags:
159
+ # Trim and validate
160
+ tags = args.tags.strip()
161
+ if tags:
162
+ # Process as list if contains commas
163
+ if ',' in tags:
164
+ values = [item.strip() for item in tags.split(',')]
165
+ values = [item for item in values if item]
166
+ if values:
167
+ search_params['tags'] = values
168
+ else:
169
+ search_params['tags'] = [tags]
170
+
171
+ # If no search parameters, throw error
172
+ if not search_params:
173
+ raise ValueError("No password ID or search criteria provided")
174
+
175
+ # Search and decrypt
176
+ items = client.search_and_decrypt(**search_params)
177
+ items.extend(client.search_and_decrypt_shortcut(**search_params))
178
+ return items
179
+
180
+ def _sanitize_env_var_name(self, name):
181
+ """
182
+ Sanitize a name to make it a valid environment variable name.
183
+ Rules:
184
+ 1. Replace non-alphanumeric with underscore
185
+ 2. Ensure starts with a letter
186
+ Args:
187
+ name (str): Original name
188
+ Returns:
189
+ str: Sanitized environment variable name
190
+ """
191
+ # Replace non-alphanumeric with underscore
192
+ sanitized = re.sub(r'[^a-zA-Z0-9]', '_', name)
193
+
194
+ # Ensure starts with a letter
195
+ if not sanitized[0].isalpha():
196
+ sanitized = 'P_' + sanitized
197
+
198
+ return sanitized
199
+
200
+ @staticmethod
201
+ def add_command(subparsers):
202
+ parser = subparsers.add_parser("exec", help="Execute a command with passwords as environment variables")
203
+
204
+ params = PassworkCommand.base_params()
205
+ for key, value in params.items():
206
+ parser.add_argument(key, **value)
207
+
208
+ group_param = {
209
+ "--password-id": {"help": "ID of password(s) to retrieve (comma-separated for multiple)"},
210
+ "--shortcut-id": {"help": "ID of shortcut(s) to retrieve (comma-separated for multiple)"},
211
+ "--vault-id": {"help": "ID(s) of vault(s) to search in (comma-separated for multiple)"},
212
+ "--folder-id": {"help": "ID(s) of folder(s) to search in (comma-separated for multiple)"},
213
+ "--tags": {"help": "Tag(s) to search for (comma-separated for multiple)"},
214
+ }
215
+
216
+ group = parser.add_argument_group("Password identification (at least one required)")
217
+ for key, value in group_param.items():
218
+ group.add_argument(key, **value)
219
+
220
+ parser.add_argument("--cmd", help="Command to execute")
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env python3
2
+ import sys
3
+ from .base import PassworkCommand, ItemBaseCommand
4
+ from passwork_client.utils import generate_totp, generate_totp_by_url
5
+
6
+
7
+ class GetCommandStrategy(PassworkCommand, ItemBaseCommand):
8
+
9
+ def execute(self, client, args):
10
+ try:
11
+ # Get passwords based on provided parameters
12
+ password = self._get_password(client, args)
13
+ if not password:
14
+ print("Error: Password not found", file=sys.stderr)
15
+ return 1
16
+
17
+ if args.totp_code:
18
+ secret = self._extract_field(password, args.totp_code)
19
+ if not secret:
20
+ print("Error: TOTP secret not found", file=sys.stderr)
21
+ return 1
22
+
23
+ print(self._generate_totp(secret))
24
+ return 0
25
+
26
+ if args.field:
27
+ print(self._extract_field(password, args.field))
28
+ else:
29
+
30
+ if "password" in password and password["password"]:
31
+ print(password["password"])
32
+ else:
33
+ print("The password is not filled in or is empty", file=sys.stderr)
34
+ return 1
35
+
36
+ return 0
37
+
38
+ except Exception as e:
39
+ print(f"Error getting password command: {e}", file=sys.stderr)
40
+ return 1
41
+
42
+ def _extract_field(self, data, field_name):
43
+ customs = data.get("customs") or []
44
+ if customs:
45
+ for custom in customs:
46
+ if custom["name"] == field_name:
47
+ return custom["value"]
48
+
49
+ if field_name in data:
50
+ return data[field_name]
51
+
52
+ # Field not found
53
+ return None
54
+
55
+ def _generate_totp(self, secret: str):
56
+
57
+ if "otpauth://" in secret.lower():
58
+ return generate_totp_by_url(secret)
59
+
60
+ return generate_totp(secret)
61
+
62
+ @staticmethod
63
+ def add_command(subparsers):
64
+ parser = subparsers.add_parser("get", help="Get password/shortcut from Passwork")
65
+
66
+ params = PassworkCommand.base_params() | {
67
+ "--field": {"help": "Show item field"},
68
+ "--totp-code": {"help": "Returned TOTP code"}
69
+ }
70
+ for key, value in params.items():
71
+ parser.add_argument(key, **value)
72
+
73
+ group_param = {
74
+ "--password-id": {"help": "ID of password to retrieve"},
75
+ "--shortcut-id": {"help": "ID of shortcut to retrieve"},
76
+ }
77
+
78
+ group = parser.add_argument_group("Password identification (at least one required)")
79
+ for key, value in group_param.items():
80
+ group.add_argument(key, **value)