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.
- pw_connector-0.3.0/LICENSE +21 -0
- pw_connector-0.3.0/PKG-INFO +39 -0
- pw_connector-0.3.0/README.md +27 -0
- pw_connector-0.3.0/cli/__init__.py +2 -0
- pw_connector-0.3.0/cli/commands/__init__.py +20 -0
- pw_connector-0.3.0/cli/commands/api_command.py +106 -0
- pw_connector-0.3.0/cli/commands/base.py +79 -0
- pw_connector-0.3.0/cli/commands/exec_command.py +220 -0
- pw_connector-0.3.0/cli/commands/get_command.py +80 -0
- pw_connector-0.3.0/cli/commands/recovery_command.py +262 -0
- pw_connector-0.3.0/cli/commands/rsa_command.py +204 -0
- pw_connector-0.3.0/cli/commands/shamir_command.py +212 -0
- pw_connector-0.3.0/cli/commands/update_command.py +109 -0
- pw_connector-0.3.0/cli/commands/users_command.py +243 -0
- pw_connector-0.3.0/cli/file_utils.py +70 -0
- pw_connector-0.3.0/cli/main.py +105 -0
- pw_connector-0.3.0/cli/utils.py +76 -0
- pw_connector-0.3.0/passwork_client/__init__.py +3 -0
- pw_connector-0.3.0/passwork_client/base32.py +103 -0
- pw_connector-0.3.0/passwork_client/constants/__init__.py +8 -0
- pw_connector-0.3.0/passwork_client/constants/link_expiration_time.py +5 -0
- pw_connector-0.3.0/passwork_client/constants/link_type.py +3 -0
- pw_connector-0.3.0/passwork_client/crypto.py +291 -0
- pw_connector-0.3.0/passwork_client/exceptions.py +19 -0
- pw_connector-0.3.0/passwork_client/modules/__init__.py +30 -0
- pw_connector-0.3.0/passwork_client/modules/api_client.py +228 -0
- pw_connector-0.3.0/passwork_client/modules/app.py +16 -0
- pw_connector-0.3.0/passwork_client/modules/batch.py +24 -0
- pw_connector-0.3.0/passwork_client/modules/inbox.py +40 -0
- pw_connector-0.3.0/passwork_client/modules/item.py +205 -0
- pw_connector-0.3.0/passwork_client/modules/link.py +73 -0
- pw_connector-0.3.0/passwork_client/modules/master_key.py +73 -0
- pw_connector-0.3.0/passwork_client/modules/session.py +61 -0
- pw_connector-0.3.0/passwork_client/modules/shortcut.py +100 -0
- pw_connector-0.3.0/passwork_client/modules/snapshot.py +45 -0
- pw_connector-0.3.0/passwork_client/modules/user.py +106 -0
- pw_connector-0.3.0/passwork_client/modules/vault.py +62 -0
- pw_connector-0.3.0/passwork_client/modules/vault_type.py +86 -0
- pw_connector-0.3.0/passwork_client/passwork_client.py +51 -0
- pw_connector-0.3.0/passwork_client/shamir/__init__.py +17 -0
- pw_connector-0.3.0/passwork_client/shamir/activity_log.py +155 -0
- pw_connector-0.3.0/passwork_client/shamir/audit.py +274 -0
- pw_connector-0.3.0/passwork_client/shamir/holder_rsa.py +77 -0
- pw_connector-0.3.0/passwork_client/shamir/primitives.py +140 -0
- pw_connector-0.3.0/passwork_client/shamir/recovery.py +86 -0
- pw_connector-0.3.0/passwork_client/shamir/split_helper.py +25 -0
- pw_connector-0.3.0/passwork_client/utils.py +283 -0
- pw_connector-0.3.0/pw_connector.egg-info/PKG-INFO +39 -0
- pw_connector-0.3.0/pw_connector.egg-info/SOURCES.txt +54 -0
- pw_connector-0.3.0/pw_connector.egg-info/dependency_links.txt +1 -0
- pw_connector-0.3.0/pw_connector.egg-info/entry_points.txt +2 -0
- pw_connector-0.3.0/pw_connector.egg-info/requires.txt +5 -0
- pw_connector-0.3.0/pw_connector.egg-info/top_level.txt +2 -0
- pw_connector-0.3.0/pyproject.toml +9 -0
- pw_connector-0.3.0/setup.cfg +4 -0
- 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,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)
|