grimoirelab-toolkit 1.2.2__tar.gz → 1.2.3__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.
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/NEWS +5 -0
- grimoirelab_toolkit-1.2.3/PKG-INFO +205 -0
- grimoirelab_toolkit-1.2.3/README.md +179 -0
- grimoirelab_toolkit-1.2.3/grimoirelab_toolkit/_version.py +2 -0
- grimoirelab_toolkit-1.2.3/grimoirelab_toolkit/credential_manager/__init__.py +36 -0
- grimoirelab_toolkit-1.2.3/grimoirelab_toolkit/credential_manager/__main__.py +4 -0
- grimoirelab_toolkit-1.2.3/grimoirelab_toolkit/credential_manager/bw_manager.py +212 -0
- grimoirelab_toolkit-1.2.3/grimoirelab_toolkit/credential_manager/exceptions.py +53 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/pyproject.toml +1 -1
- grimoirelab_toolkit-1.2.3/tests/__init__.py +0 -0
- grimoirelab_toolkit-1.2.3/tests/test_bw_manager.py +265 -0
- grimoirelab_toolkit-1.2.2/PKG-INFO +0 -90
- grimoirelab_toolkit-1.2.2/README.md +0 -64
- grimoirelab_toolkit-1.2.2/grimoirelab_toolkit/_version.py +0 -2
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/AUTHORS +0 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/LICENSE +0 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/grimoirelab_toolkit/__init__.py +0 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/grimoirelab_toolkit/datetime.py +0 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/grimoirelab_toolkit/identities.py +0 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/grimoirelab_toolkit/introspect.py +0 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/grimoirelab_toolkit/uris.py +0 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/tests/run_tests.py +0 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/tests/test_datetime.py +0 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/tests/test_identities.py +0 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/tests/test_introspect.py +0 -0
- {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/tests/test_uris.py +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: grimoirelab-toolkit
|
|
3
|
+
Version: 1.2.3
|
|
4
|
+
Summary: Toolkit of common functions used across GrimoireLab
|
|
5
|
+
License: GPL-3.0+
|
|
6
|
+
License-File: AUTHORS
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Keywords: development,grimoirelab
|
|
9
|
+
Author: GrimoireLab Developers
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Software Development
|
|
21
|
+
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
|
|
22
|
+
Project-URL: Homepage, https://chaoss.github.io/grimoirelab/
|
|
23
|
+
Project-URL: Repository, https://github.com/chaoss/grimoirelab-toolkit
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# GrimoireLab Toolkit [](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push) [](https://coveralls.io/r/chaoss/grimoirelab-toolkit?branch=main)
|
|
27
|
+
|
|
28
|
+
Toolkit of common functions used across GrimoireLab projects.
|
|
29
|
+
|
|
30
|
+
This package provides a library composed by functions widely used in other
|
|
31
|
+
GrimoireLab projects. These function deal with date handling, introspection,
|
|
32
|
+
URIs/URLs, among other topics.
|
|
33
|
+
|
|
34
|
+
## Requirements
|
|
35
|
+
|
|
36
|
+
* Python >= 3.8
|
|
37
|
+
|
|
38
|
+
You will also need some other libraries for running the tool, you can find the
|
|
39
|
+
whole list of dependencies in [pyproject.toml](pyproject.toml) file.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
There are several ways to install GrimoireLab Toolkit on your system: packages or source
|
|
44
|
+
code using Poetry or pip.
|
|
45
|
+
|
|
46
|
+
### PyPI
|
|
47
|
+
|
|
48
|
+
GrimoireLab Toolkit can be installed using pip, a tool for installing Python packages.
|
|
49
|
+
To do it, run the next command:
|
|
50
|
+
```
|
|
51
|
+
$ pip install grimoirelab-toolkit
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Source code
|
|
55
|
+
|
|
56
|
+
To install from the source code you will need to clone the repository first:
|
|
57
|
+
```
|
|
58
|
+
$ git clone https://github.com/chaoss/grimoirelab-toolkit
|
|
59
|
+
$ cd grimoirelab-toolkit
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Then use pip or Poetry to install the package along with its dependencies.
|
|
63
|
+
|
|
64
|
+
#### Pip
|
|
65
|
+
To install the package from local directory run the following command:
|
|
66
|
+
```
|
|
67
|
+
$ pip install .
|
|
68
|
+
```
|
|
69
|
+
In case you are a developer, you should install GrimoireLab Toolkit in editable mode:
|
|
70
|
+
```
|
|
71
|
+
$ pip install -e .
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Poetry
|
|
75
|
+
We use [poetry](https://python-poetry.org/) for dependency management and
|
|
76
|
+
packaging. You can install it following its [documentation](https://python-poetry.org/docs/#installation).
|
|
77
|
+
Once you have installed it, you can install GrimoireLab Toolkit and the dependencies in
|
|
78
|
+
a project isolated environment using:
|
|
79
|
+
```
|
|
80
|
+
$ poetry install
|
|
81
|
+
```
|
|
82
|
+
To spaw a new shell within the virtual environment use:
|
|
83
|
+
```
|
|
84
|
+
$ poetry shell
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Credential Manager Library
|
|
88
|
+
|
|
89
|
+
This is a module made to retrieve credentials from different secrets management systems like Bitwarden.
|
|
90
|
+
It accesses the secrets management service, looks for the desired credential and returns it in String form.
|
|
91
|
+
|
|
92
|
+
To use the module in your python code
|
|
93
|
+
|
|
94
|
+
### Bitwarden
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
from grimoirelab_toolkit.credential_manager import BitwardenManager
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Instantiate the Bitwarden manager using the api credentials for login
|
|
101
|
+
bw_manager = BitwardenManager("your_client_id", "your_client_secret", "your_master_password")
|
|
102
|
+
|
|
103
|
+
# Login
|
|
104
|
+
bw_manager.login()
|
|
105
|
+
|
|
106
|
+
# Retrieve a secret from Bitwarden
|
|
107
|
+
username = bw_manager.get_secret("github")
|
|
108
|
+
password = bw_manager.get_secret("elasticsearch")
|
|
109
|
+
|
|
110
|
+
# Logout
|
|
111
|
+
bw_manager.logout()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
#### Response format
|
|
116
|
+
|
|
117
|
+
When calling `get_secret(item_name)`, the method returns a JSON object with the following structure:
|
|
118
|
+
|
|
119
|
+
_NOTE: the parameter "item_name" corresponds with the field "name" of the json. That's the name of the item._
|
|
120
|
+
(in this case, GitHub)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
##### Example Response
|
|
124
|
+
|
|
125
|
+
```json
|
|
126
|
+
{
|
|
127
|
+
"passwordHistory": [
|
|
128
|
+
{
|
|
129
|
+
"lastUsedDate": "2024-11-05T10:27:18.411Z",
|
|
130
|
+
"password": "previous_password_value_1"
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"lastUsedDate": "2024-11-05T09:20:06.512Z",
|
|
134
|
+
"password": "previous_password_value_2"
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"revisionDate": "2025-05-11T14:40:19.456Z",
|
|
138
|
+
"creationDate": "2024-10-30T18:56:41.023Z",
|
|
139
|
+
"object": "item",
|
|
140
|
+
"id": "91300380-620f-4707-8de1-b21901383315",
|
|
141
|
+
"organizationId": null,
|
|
142
|
+
"folderId": null,
|
|
143
|
+
"type": 1,
|
|
144
|
+
"reprompt": 0,
|
|
145
|
+
"name": "GitHub",
|
|
146
|
+
"notes": null,
|
|
147
|
+
"favorite": false,
|
|
148
|
+
"fields": [
|
|
149
|
+
{
|
|
150
|
+
"name": "api-token",
|
|
151
|
+
"value": "TOKEN"
|
|
152
|
+
"type": 0,
|
|
153
|
+
"linkedId": null
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
"name": "api_key",
|
|
157
|
+
"value": "APIKEY",
|
|
158
|
+
"type": 0,
|
|
159
|
+
"linkedId": null
|
|
160
|
+
}
|
|
161
|
+
],
|
|
162
|
+
"login": {
|
|
163
|
+
"uris": [],
|
|
164
|
+
"username": "your_username",
|
|
165
|
+
"password": "your_password",
|
|
166
|
+
"totp": null,
|
|
167
|
+
"passwordRevisionDate": "2024-11-05T10:27:18.411Z"
|
|
168
|
+
},
|
|
169
|
+
"collectionIds": [],
|
|
170
|
+
"attachments": []
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Field Descriptions
|
|
175
|
+
|
|
176
|
+
- passwordHistory: Array of previously used passwords with timestamps
|
|
177
|
+
- revisionDate: Last modification timestamp (ISO 8601 format)
|
|
178
|
+
- creationDate: Item creation timestamp (ISO 8601 format)
|
|
179
|
+
- object: Always "item" for credential items
|
|
180
|
+
- id: Unique identifier for this item
|
|
181
|
+
- organizationId: Organization ID if shared, null for personal items
|
|
182
|
+
- folderId: Folder ID if organized, null otherwise
|
|
183
|
+
- type: Item type (1 = login, 2 = secure note, 3 = card, 4 = identity)
|
|
184
|
+
- name: Display name of the credential item (name used as argument in get_secret())
|
|
185
|
+
- notes: Optional notes field
|
|
186
|
+
- favorite: Boolean indicating if item is favorited
|
|
187
|
+
- fields: Array of custom fields with name-value pairs
|
|
188
|
+
- name: Field name
|
|
189
|
+
- value: Field value (can contain secrets)
|
|
190
|
+
- type: Field type (0 = text, 1 = hidden, 2 = boolean)
|
|
191
|
+
- login: Login credentials object
|
|
192
|
+
- username: Login username
|
|
193
|
+
- password: Login password
|
|
194
|
+
- totp: TOTP secret for 2FA (if configured)
|
|
195
|
+
- uris: Array of associated URIs/URLs
|
|
196
|
+
- passwordRevisionDate: Last password change timestamp
|
|
197
|
+
- collectionIds: Array of collection IDs this item belongs to
|
|
198
|
+
- attachments: Array of file attachments
|
|
199
|
+
|
|
200
|
+
The module uses the [Bitwarden CLI](https://bitwarden.com/help/cli/) to interact with Bitwarden.
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
Licensed under GNU General Public License (GPL), version 3 or later.
|
|
205
|
+
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# GrimoireLab Toolkit [](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push) [](https://coveralls.io/r/chaoss/grimoirelab-toolkit?branch=main)
|
|
2
|
+
|
|
3
|
+
Toolkit of common functions used across GrimoireLab projects.
|
|
4
|
+
|
|
5
|
+
This package provides a library composed by functions widely used in other
|
|
6
|
+
GrimoireLab projects. These function deal with date handling, introspection,
|
|
7
|
+
URIs/URLs, among other topics.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
* Python >= 3.8
|
|
12
|
+
|
|
13
|
+
You will also need some other libraries for running the tool, you can find the
|
|
14
|
+
whole list of dependencies in [pyproject.toml](pyproject.toml) file.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
There are several ways to install GrimoireLab Toolkit on your system: packages or source
|
|
19
|
+
code using Poetry or pip.
|
|
20
|
+
|
|
21
|
+
### PyPI
|
|
22
|
+
|
|
23
|
+
GrimoireLab Toolkit can be installed using pip, a tool for installing Python packages.
|
|
24
|
+
To do it, run the next command:
|
|
25
|
+
```
|
|
26
|
+
$ pip install grimoirelab-toolkit
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Source code
|
|
30
|
+
|
|
31
|
+
To install from the source code you will need to clone the repository first:
|
|
32
|
+
```
|
|
33
|
+
$ git clone https://github.com/chaoss/grimoirelab-toolkit
|
|
34
|
+
$ cd grimoirelab-toolkit
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then use pip or Poetry to install the package along with its dependencies.
|
|
38
|
+
|
|
39
|
+
#### Pip
|
|
40
|
+
To install the package from local directory run the following command:
|
|
41
|
+
```
|
|
42
|
+
$ pip install .
|
|
43
|
+
```
|
|
44
|
+
In case you are a developer, you should install GrimoireLab Toolkit in editable mode:
|
|
45
|
+
```
|
|
46
|
+
$ pip install -e .
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
#### Poetry
|
|
50
|
+
We use [poetry](https://python-poetry.org/) for dependency management and
|
|
51
|
+
packaging. You can install it following its [documentation](https://python-poetry.org/docs/#installation).
|
|
52
|
+
Once you have installed it, you can install GrimoireLab Toolkit and the dependencies in
|
|
53
|
+
a project isolated environment using:
|
|
54
|
+
```
|
|
55
|
+
$ poetry install
|
|
56
|
+
```
|
|
57
|
+
To spaw a new shell within the virtual environment use:
|
|
58
|
+
```
|
|
59
|
+
$ poetry shell
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Credential Manager Library
|
|
63
|
+
|
|
64
|
+
This is a module made to retrieve credentials from different secrets management systems like Bitwarden.
|
|
65
|
+
It accesses the secrets management service, looks for the desired credential and returns it in String form.
|
|
66
|
+
|
|
67
|
+
To use the module in your python code
|
|
68
|
+
|
|
69
|
+
### Bitwarden
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
from grimoirelab_toolkit.credential_manager import BitwardenManager
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# Instantiate the Bitwarden manager using the api credentials for login
|
|
76
|
+
bw_manager = BitwardenManager("your_client_id", "your_client_secret", "your_master_password")
|
|
77
|
+
|
|
78
|
+
# Login
|
|
79
|
+
bw_manager.login()
|
|
80
|
+
|
|
81
|
+
# Retrieve a secret from Bitwarden
|
|
82
|
+
username = bw_manager.get_secret("github")
|
|
83
|
+
password = bw_manager.get_secret("elasticsearch")
|
|
84
|
+
|
|
85
|
+
# Logout
|
|
86
|
+
bw_manager.logout()
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
#### Response format
|
|
91
|
+
|
|
92
|
+
When calling `get_secret(item_name)`, the method returns a JSON object with the following structure:
|
|
93
|
+
|
|
94
|
+
_NOTE: the parameter "item_name" corresponds with the field "name" of the json. That's the name of the item._
|
|
95
|
+
(in this case, GitHub)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
##### Example Response
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"passwordHistory": [
|
|
103
|
+
{
|
|
104
|
+
"lastUsedDate": "2024-11-05T10:27:18.411Z",
|
|
105
|
+
"password": "previous_password_value_1"
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"lastUsedDate": "2024-11-05T09:20:06.512Z",
|
|
109
|
+
"password": "previous_password_value_2"
|
|
110
|
+
}
|
|
111
|
+
],
|
|
112
|
+
"revisionDate": "2025-05-11T14:40:19.456Z",
|
|
113
|
+
"creationDate": "2024-10-30T18:56:41.023Z",
|
|
114
|
+
"object": "item",
|
|
115
|
+
"id": "91300380-620f-4707-8de1-b21901383315",
|
|
116
|
+
"organizationId": null,
|
|
117
|
+
"folderId": null,
|
|
118
|
+
"type": 1,
|
|
119
|
+
"reprompt": 0,
|
|
120
|
+
"name": "GitHub",
|
|
121
|
+
"notes": null,
|
|
122
|
+
"favorite": false,
|
|
123
|
+
"fields": [
|
|
124
|
+
{
|
|
125
|
+
"name": "api-token",
|
|
126
|
+
"value": "TOKEN"
|
|
127
|
+
"type": 0,
|
|
128
|
+
"linkedId": null
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
"name": "api_key",
|
|
132
|
+
"value": "APIKEY",
|
|
133
|
+
"type": 0,
|
|
134
|
+
"linkedId": null
|
|
135
|
+
}
|
|
136
|
+
],
|
|
137
|
+
"login": {
|
|
138
|
+
"uris": [],
|
|
139
|
+
"username": "your_username",
|
|
140
|
+
"password": "your_password",
|
|
141
|
+
"totp": null,
|
|
142
|
+
"passwordRevisionDate": "2024-11-05T10:27:18.411Z"
|
|
143
|
+
},
|
|
144
|
+
"collectionIds": [],
|
|
145
|
+
"attachments": []
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Field Descriptions
|
|
150
|
+
|
|
151
|
+
- passwordHistory: Array of previously used passwords with timestamps
|
|
152
|
+
- revisionDate: Last modification timestamp (ISO 8601 format)
|
|
153
|
+
- creationDate: Item creation timestamp (ISO 8601 format)
|
|
154
|
+
- object: Always "item" for credential items
|
|
155
|
+
- id: Unique identifier for this item
|
|
156
|
+
- organizationId: Organization ID if shared, null for personal items
|
|
157
|
+
- folderId: Folder ID if organized, null otherwise
|
|
158
|
+
- type: Item type (1 = login, 2 = secure note, 3 = card, 4 = identity)
|
|
159
|
+
- name: Display name of the credential item (name used as argument in get_secret())
|
|
160
|
+
- notes: Optional notes field
|
|
161
|
+
- favorite: Boolean indicating if item is favorited
|
|
162
|
+
- fields: Array of custom fields with name-value pairs
|
|
163
|
+
- name: Field name
|
|
164
|
+
- value: Field value (can contain secrets)
|
|
165
|
+
- type: Field type (0 = text, 1 = hidden, 2 = boolean)
|
|
166
|
+
- login: Login credentials object
|
|
167
|
+
- username: Login username
|
|
168
|
+
- password: Login password
|
|
169
|
+
- totp: TOTP secret for 2FA (if configured)
|
|
170
|
+
- uris: Array of associated URIs/URLs
|
|
171
|
+
- passwordRevisionDate: Last password change timestamp
|
|
172
|
+
- collectionIds: Array of collection IDs this item belongs to
|
|
173
|
+
- attachments: Array of file attachments
|
|
174
|
+
|
|
175
|
+
The module uses the [Bitwarden CLI](https://bitwarden.com/help/cli/) to interact with Bitwarden.
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
Licensed under GNU General Public License (GPL), version 3 or later.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) Grimoirelab Contributors
|
|
4
|
+
#
|
|
5
|
+
# This program is free software; you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
#
|
|
18
|
+
# Author:
|
|
19
|
+
# Alberto Ferrer Sánchez (alberefe@gmail.com)
|
|
20
|
+
#
|
|
21
|
+
|
|
22
|
+
from .bw_manager import BitwardenManager
|
|
23
|
+
from .exceptions import (
|
|
24
|
+
CredentialManagerError,
|
|
25
|
+
InvalidCredentialsError,
|
|
26
|
+
CredentialNotFoundError,
|
|
27
|
+
BitwardenCLIError,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"BitwardenManager",
|
|
32
|
+
"CredentialManagerError",
|
|
33
|
+
"InvalidCredentialsError",
|
|
34
|
+
"CredentialNotFoundError",
|
|
35
|
+
"BitwardenCLIError",
|
|
36
|
+
]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#
|
|
2
|
+
#
|
|
3
|
+
#
|
|
4
|
+
# This program is free software; you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
16
|
+
#
|
|
17
|
+
# Author:
|
|
18
|
+
# Alberto Ferrer Sánchez (alberefe@gmail.com)
|
|
19
|
+
#
|
|
20
|
+
import json
|
|
21
|
+
import subprocess
|
|
22
|
+
import logging
|
|
23
|
+
import shutil
|
|
24
|
+
|
|
25
|
+
from .exceptions import (
|
|
26
|
+
BitwardenCLIError,
|
|
27
|
+
InvalidCredentialsError,
|
|
28
|
+
CredentialNotFoundError,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class BitwardenManager:
|
|
35
|
+
"""Retrieve credentials from Bitwarden.
|
|
36
|
+
|
|
37
|
+
This class defines functions to log in, retrieve secrets
|
|
38
|
+
and log out of Bitwarden using the Bitwarden CLI. The
|
|
39
|
+
workflow is:
|
|
40
|
+
|
|
41
|
+
manager = BitwardenManager(client_id, client_secret, master_password)
|
|
42
|
+
manager.login()
|
|
43
|
+
manager.get_secret("github")
|
|
44
|
+
manager.get_secret("elasticsearch")
|
|
45
|
+
manager.logout()
|
|
46
|
+
|
|
47
|
+
The manager logs in using the client_id, client_secret, and
|
|
48
|
+
master_password given as arguments when creating the instance,
|
|
49
|
+
so the object is reusable along the program.
|
|
50
|
+
|
|
51
|
+
The path of Bitwarden CLI (bw) is retrieved using shutil.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def __init__(self, client_id: str, client_secret: str, master_password: str):
|
|
55
|
+
"""
|
|
56
|
+
Creates BitwardenManager object using API key authentication
|
|
57
|
+
|
|
58
|
+
:param str client_id: Bitwarden API client ID
|
|
59
|
+
:param str client_secret: Bitwarden API client secret
|
|
60
|
+
:param str master_password: Master password for unlocking the vault
|
|
61
|
+
"""
|
|
62
|
+
# Session key of the bw session
|
|
63
|
+
self.session_key = None
|
|
64
|
+
|
|
65
|
+
# API credentials
|
|
66
|
+
self.client_id = client_id
|
|
67
|
+
self.client_secret = client_secret
|
|
68
|
+
self.master_password = master_password
|
|
69
|
+
|
|
70
|
+
# Get the absolute path to the bw executable
|
|
71
|
+
self.bw_path = shutil.which("bw")
|
|
72
|
+
if not self.bw_path:
|
|
73
|
+
raise BitwardenCLIError("Bitwarden CLI (bw) not found in PATH")
|
|
74
|
+
|
|
75
|
+
# Set up environment variables for consistent execution context
|
|
76
|
+
self.env = {
|
|
77
|
+
"LANG": "C",
|
|
78
|
+
"BW_CLIENTID": client_id,
|
|
79
|
+
"BW_CLIENTSECRET": client_secret,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
def login(self) -> str | None:
|
|
83
|
+
"""Log into Bitwarden.
|
|
84
|
+
|
|
85
|
+
Use the API authentication key to log in and unlock the vault. After it,
|
|
86
|
+
it will obtain a session key that will be used by to access the vault.
|
|
87
|
+
|
|
88
|
+
:returns: The session key for the current Bitwarden session.
|
|
89
|
+
|
|
90
|
+
:raises InvalidCredentialsError: If invalid credentials are provided
|
|
91
|
+
:raises BitwardenCLIError: If Bitwarden CLI operations fail
|
|
92
|
+
"""
|
|
93
|
+
# Log in using API key
|
|
94
|
+
login_result = subprocess.run(
|
|
95
|
+
[self.bw_path, "login", "--apikey"],
|
|
96
|
+
input=f"{self.client_id}\n{self.client_secret}\n",
|
|
97
|
+
capture_output=True,
|
|
98
|
+
text=True,
|
|
99
|
+
env=self.env,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if login_result.returncode != 0:
|
|
103
|
+
error_msg = (
|
|
104
|
+
login_result.stderr.strip() if login_result.stderr else "Unknown error"
|
|
105
|
+
)
|
|
106
|
+
logger.error("Error logging in with API key: %s", error_msg)
|
|
107
|
+
raise InvalidCredentialsError(
|
|
108
|
+
"Invalid API credentials provided for Bitwarden"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# After login, we need to unlock the vault to get a session key
|
|
112
|
+
self.session_key = self._unlock_vault()
|
|
113
|
+
|
|
114
|
+
return self.session_key
|
|
115
|
+
|
|
116
|
+
def _unlock_vault(self) -> str:
|
|
117
|
+
"""Unlock the vault after authentication.
|
|
118
|
+
|
|
119
|
+
Executes the Bitwarden unlock command to obtain a session key
|
|
120
|
+
for an already authenticated user but locked vault.
|
|
121
|
+
|
|
122
|
+
:returns: Session key for the unlocked vault
|
|
123
|
+
:raises BitwardenCLIError: If unlock operation fails or returns empty session key
|
|
124
|
+
"""
|
|
125
|
+
# this uses the master password to unlock the vault
|
|
126
|
+
unlock_result = subprocess.run(
|
|
127
|
+
[self.bw_path, "unlock", "--raw"],
|
|
128
|
+
input=f"{self.master_password}\n",
|
|
129
|
+
capture_output=True,
|
|
130
|
+
text=True,
|
|
131
|
+
env=self.env,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if unlock_result.returncode != 0:
|
|
135
|
+
error_msg = (
|
|
136
|
+
unlock_result.stderr.strip()
|
|
137
|
+
if unlock_result.stderr
|
|
138
|
+
else "Unknown error"
|
|
139
|
+
)
|
|
140
|
+
logger.error("Error unlocking vault: %s", error_msg)
|
|
141
|
+
raise BitwardenCLIError(f"Failed to unlock vault: {error_msg}")
|
|
142
|
+
|
|
143
|
+
# the session key is used when retrieving the secrets with get_secret
|
|
144
|
+
session_key = unlock_result.stdout.strip()
|
|
145
|
+
if not session_key:
|
|
146
|
+
raise BitwardenCLIError("Empty session key received from unlock command")
|
|
147
|
+
|
|
148
|
+
return session_key
|
|
149
|
+
|
|
150
|
+
def get_secret(self, item_name: str) -> dict:
|
|
151
|
+
"""Retrieve an item from the Bitwarden vault.
|
|
152
|
+
|
|
153
|
+
Retrieves all the fields stored for an item with the name
|
|
154
|
+
provided as an argument and returns them as a dictionary.
|
|
155
|
+
|
|
156
|
+
The returned dictionary includes fields such as:
|
|
157
|
+
- login: username, password, URIs, TOTP
|
|
158
|
+
- fields: custom fields
|
|
159
|
+
- notes: secure notes
|
|
160
|
+
- name, id, and other metadata
|
|
161
|
+
|
|
162
|
+
:param str item_name: The name of the item to retrieve
|
|
163
|
+
|
|
164
|
+
:returns: Dictionary containing the item data
|
|
165
|
+
:rtype: dict
|
|
166
|
+
|
|
167
|
+
:raises CredentialNotFoundError: If the specific credential is not found
|
|
168
|
+
:raises BitwardenCLIError: If Bitwarden CLI operations fail
|
|
169
|
+
"""
|
|
170
|
+
# Pass session key via command line parameter
|
|
171
|
+
result = subprocess.run(
|
|
172
|
+
[self.bw_path, "get", "item", item_name, "--session", self.session_key],
|
|
173
|
+
capture_output=True,
|
|
174
|
+
text=True,
|
|
175
|
+
env=self.env,
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
if result.returncode != 0:
|
|
179
|
+
raise CredentialNotFoundError(f"Credential not found: '{item_name}'")
|
|
180
|
+
|
|
181
|
+
# Parse the JSON response returned in stdout
|
|
182
|
+
try:
|
|
183
|
+
item = json.loads(result.stdout)
|
|
184
|
+
except json.JSONDecodeError as e:
|
|
185
|
+
logger.error("Failed to parse Bitwarden response: %s", str(e))
|
|
186
|
+
raise BitwardenCLIError(f"Invalid JSON response from Bitwarden: {e}")
|
|
187
|
+
|
|
188
|
+
return item
|
|
189
|
+
|
|
190
|
+
def logout(self) -> None:
|
|
191
|
+
"""Log out from Bitwarden and invalidate the session.
|
|
192
|
+
|
|
193
|
+
This method ends the current session and clears the session key.
|
|
194
|
+
"""
|
|
195
|
+
logger.info("Logging out from Bitwarden")
|
|
196
|
+
|
|
197
|
+
# Execute logout command
|
|
198
|
+
result = subprocess.run(
|
|
199
|
+
[self.bw_path, "logout"],
|
|
200
|
+
capture_output=True,
|
|
201
|
+
text=True,
|
|
202
|
+
env=self.env,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if result.returncode != 0:
|
|
206
|
+
error_msg = result.stderr.strip() if result.stderr else "Unknown error"
|
|
207
|
+
logger.error("Error during logout: %s", error_msg)
|
|
208
|
+
|
|
209
|
+
# Clear session key for security
|
|
210
|
+
self.session_key = None
|
|
211
|
+
|
|
212
|
+
logger.info("Successfully logged out from Bitwarden")
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) Grimoirelab Contributors
|
|
4
|
+
#
|
|
5
|
+
# This program is free software; you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
#
|
|
18
|
+
# Author:
|
|
19
|
+
# Alberto Ferrer Sánchez (alberefe@gmail.com)
|
|
20
|
+
#
|
|
21
|
+
|
|
22
|
+
"""Custom exceptions for the credential manager module."""
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"CredentialManagerError",
|
|
26
|
+
"InvalidCredentialsError",
|
|
27
|
+
"CredentialNotFoundError",
|
|
28
|
+
"BitwardenCLIError",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CredentialManagerError(Exception):
|
|
33
|
+
"""Base exception for all credential manager errors."""
|
|
34
|
+
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class InvalidCredentialsError(CredentialManagerError):
|
|
39
|
+
"""Raised when invalid credentials are provided."""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CredentialNotFoundError(CredentialManagerError):
|
|
45
|
+
"""Raised when a specific credential is not found in a secret."""
|
|
46
|
+
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class BitwardenCLIError(CredentialManagerError):
|
|
51
|
+
"""Raised for Bitwarden CLI specific errors."""
|
|
52
|
+
|
|
53
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
#
|
|
3
|
+
# Copyright (C) Grimoirelab Contributors
|
|
4
|
+
#
|
|
5
|
+
# This program is free software; you can redistribute it and/or modify
|
|
6
|
+
# it under the terms of the GNU General Public License as published by
|
|
7
|
+
# the Free Software Foundation; either version 3 of the License, or
|
|
8
|
+
# (at your option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful,
|
|
11
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
# GNU General Public License for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU General Public License
|
|
16
|
+
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
#
|
|
18
|
+
# Author:
|
|
19
|
+
# Alberto Ferrer Sánchez (alberefe@gmail.com)
|
|
20
|
+
#
|
|
21
|
+
|
|
22
|
+
import unittest
|
|
23
|
+
from unittest.mock import patch, MagicMock, call
|
|
24
|
+
|
|
25
|
+
from grimoirelab_toolkit.credential_manager.bw_manager import BitwardenManager
|
|
26
|
+
from grimoirelab_toolkit.credential_manager.exceptions import (
|
|
27
|
+
InvalidCredentialsError,
|
|
28
|
+
BitwardenCLIError,
|
|
29
|
+
CredentialNotFoundError,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestBitwardenManager(unittest.TestCase):
|
|
34
|
+
"""Tests for BitwardenManager class."""
|
|
35
|
+
|
|
36
|
+
def setUp(self):
|
|
37
|
+
"""Set up common test fixtures."""
|
|
38
|
+
|
|
39
|
+
self.client_id = "test_client_id"
|
|
40
|
+
self.client_secret = "test_client_secret"
|
|
41
|
+
self.master_password = "test_master_password"
|
|
42
|
+
self.session_key = "test_session_key"
|
|
43
|
+
|
|
44
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
|
|
45
|
+
def test_initialization_success(self, mock_which):
|
|
46
|
+
"""Test successful initialization with valid credentials."""
|
|
47
|
+
|
|
48
|
+
mock_which.return_value = "/usr/bin/bw"
|
|
49
|
+
|
|
50
|
+
manager = BitwardenManager(
|
|
51
|
+
self.client_id, self.client_secret, self.master_password
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
self.assertEqual(manager.client_id, self.client_id)
|
|
55
|
+
self.assertEqual(manager.client_secret, self.client_secret)
|
|
56
|
+
self.assertEqual(manager.master_password, self.master_password)
|
|
57
|
+
self.assertIsNone(manager.session_key)
|
|
58
|
+
self.assertEqual(manager.bw_path, "/usr/bin/bw")
|
|
59
|
+
self.assertIn("BW_CLIENTID", manager.env)
|
|
60
|
+
self.assertIn("BW_CLIENTSECRET", manager.env)
|
|
61
|
+
|
|
62
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
|
|
63
|
+
def test_initialization_bw_not_found(self, mock_which):
|
|
64
|
+
"""Test initialization fails when bw CLI is not found."""
|
|
65
|
+
|
|
66
|
+
mock_which.return_value = None
|
|
67
|
+
|
|
68
|
+
with self.assertRaises(BitwardenCLIError) as context:
|
|
69
|
+
BitwardenManager(self.client_id, self.client_secret, self.master_password)
|
|
70
|
+
|
|
71
|
+
self.assertIn("Bitwarden CLI (bw) not found in PATH", str(context.exception))
|
|
72
|
+
|
|
73
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
|
|
74
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
|
|
75
|
+
def test_login_success(self, mock_which, mock_run):
|
|
76
|
+
"""Test successful login and unlock."""
|
|
77
|
+
|
|
78
|
+
mock_which.return_value = "/usr/bin/bw"
|
|
79
|
+
mock_run.side_effect = [
|
|
80
|
+
MagicMock(returncode=0, stdout="Logged in!", stderr=""), # login
|
|
81
|
+
MagicMock(returncode=0, stdout="test_session_key\n", stderr=""), # unlock
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
manager = BitwardenManager(
|
|
85
|
+
self.client_id, self.client_secret, self.master_password
|
|
86
|
+
)
|
|
87
|
+
session_key = manager.login()
|
|
88
|
+
|
|
89
|
+
self.assertEqual(session_key, "test_session_key")
|
|
90
|
+
self.assertEqual(manager.session_key, "test_session_key")
|
|
91
|
+
self.assertEqual(mock_run.call_count, 2)
|
|
92
|
+
|
|
93
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
|
|
94
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
|
|
95
|
+
def test_login_failure(self, mock_which, mock_run):
|
|
96
|
+
"""Test login failure with invalid credentials."""
|
|
97
|
+
|
|
98
|
+
mock_which.return_value = "/usr/bin/bw"
|
|
99
|
+
mock_run.return_value = MagicMock(returncode=1, stderr="Invalid credentials")
|
|
100
|
+
|
|
101
|
+
manager = BitwardenManager(
|
|
102
|
+
self.client_id, self.client_secret, self.master_password
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
with self.assertRaises(InvalidCredentialsError):
|
|
106
|
+
manager.login()
|
|
107
|
+
|
|
108
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
|
|
109
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
|
|
110
|
+
def test_unlock_failure(self, mock_which, mock_run):
|
|
111
|
+
"""Test unlock failure after successful login."""
|
|
112
|
+
|
|
113
|
+
mock_which.return_value = "/usr/bin/bw"
|
|
114
|
+
mock_run.side_effect = [
|
|
115
|
+
MagicMock(returncode=0, stdout="Logged in!", stderr=""), # login
|
|
116
|
+
MagicMock(returncode=1, stderr="Unlock failed", stdout=""), # unlock
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
manager = BitwardenManager(
|
|
120
|
+
self.client_id, self.client_secret, self.master_password
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
with self.assertRaises(BitwardenCLIError) as context:
|
|
124
|
+
manager.login()
|
|
125
|
+
|
|
126
|
+
self.assertIn("Failed to unlock vault", str(context.exception))
|
|
127
|
+
|
|
128
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
|
|
129
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
|
|
130
|
+
def test_get_secret_success(self, mock_which, mock_run):
|
|
131
|
+
"""Test successful secret retrieval."""
|
|
132
|
+
|
|
133
|
+
mock_which.return_value = "/usr/bin/bw"
|
|
134
|
+
secret_result = MagicMock(
|
|
135
|
+
returncode=0, stdout='{"name":"github","login":{"password":"secret123"}}'
|
|
136
|
+
)
|
|
137
|
+
mock_run.return_value = secret_result
|
|
138
|
+
|
|
139
|
+
manager = BitwardenManager(
|
|
140
|
+
self.client_id, self.client_secret, self.master_password
|
|
141
|
+
)
|
|
142
|
+
manager.session_key = self.session_key
|
|
143
|
+
result = manager.get_secret("github")
|
|
144
|
+
|
|
145
|
+
# Now returns a parsed dict, not subprocess result
|
|
146
|
+
self.assertIsInstance(result, dict)
|
|
147
|
+
self.assertEqual(result["name"], "github")
|
|
148
|
+
self.assertEqual(result["login"]["password"], "secret123")
|
|
149
|
+
mock_run.assert_called_once()
|
|
150
|
+
|
|
151
|
+
# Verify the get_secret call includes session key
|
|
152
|
+
call_args = mock_run.call_args
|
|
153
|
+
self.assertEqual(
|
|
154
|
+
call_args[0][0],
|
|
155
|
+
["/usr/bin/bw", "get", "item", "github", "--session", self.session_key],
|
|
156
|
+
)
|
|
157
|
+
self.assertTrue(call_args[1]["capture_output"])
|
|
158
|
+
self.assertTrue(call_args[1]["text"])
|
|
159
|
+
|
|
160
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
|
|
161
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
|
|
162
|
+
def test_get_secret_returns_parsed_dict(self, mock_which, mock_run):
|
|
163
|
+
"""Test that get_secret returns parsed dict from JSON response."""
|
|
164
|
+
|
|
165
|
+
mock_which.return_value = "/usr/bin/bw"
|
|
166
|
+
secret_result = MagicMock(returncode=0, stdout='{"data":"value"}', stderr="")
|
|
167
|
+
mock_run.return_value = secret_result
|
|
168
|
+
|
|
169
|
+
manager = BitwardenManager(
|
|
170
|
+
self.client_id, self.client_secret, self.master_password
|
|
171
|
+
)
|
|
172
|
+
manager.session_key = self.session_key
|
|
173
|
+
result = manager.get_secret("my_item")
|
|
174
|
+
|
|
175
|
+
# The method returns a parsed dict, not subprocess result
|
|
176
|
+
self.assertIsInstance(result, dict)
|
|
177
|
+
self.assertEqual(result["data"], "value")
|
|
178
|
+
|
|
179
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
|
|
180
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
|
|
181
|
+
def test_get_secret_not_found(self, mock_which, mock_run):
|
|
182
|
+
"""Test get_secret raises error when item not found."""
|
|
183
|
+
|
|
184
|
+
mock_which.return_value = "/usr/bin/bw"
|
|
185
|
+
secret_result = MagicMock(returncode=1, stderr="Not found")
|
|
186
|
+
mock_run.return_value = secret_result
|
|
187
|
+
|
|
188
|
+
manager = BitwardenManager(
|
|
189
|
+
self.client_id, self.client_secret, self.master_password
|
|
190
|
+
)
|
|
191
|
+
manager.session_key = self.session_key
|
|
192
|
+
|
|
193
|
+
with self.assertRaises(CredentialNotFoundError) as context:
|
|
194
|
+
manager.get_secret("nonexistent")
|
|
195
|
+
|
|
196
|
+
self.assertIn("Credential not found", str(context.exception))
|
|
197
|
+
|
|
198
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
|
|
199
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
|
|
200
|
+
def test_get_secret_invalid_json(self, mock_which, mock_run):
|
|
201
|
+
"""Test get_secret raises error when response is not valid JSON."""
|
|
202
|
+
|
|
203
|
+
mock_which.return_value = "/usr/bin/bw"
|
|
204
|
+
secret_result = MagicMock(returncode=0, stdout="not valid json")
|
|
205
|
+
mock_run.return_value = secret_result
|
|
206
|
+
|
|
207
|
+
manager = BitwardenManager(
|
|
208
|
+
self.client_id, self.client_secret, self.master_password
|
|
209
|
+
)
|
|
210
|
+
manager.session_key = self.session_key
|
|
211
|
+
|
|
212
|
+
with self.assertRaises(BitwardenCLIError) as context:
|
|
213
|
+
manager.get_secret("github")
|
|
214
|
+
|
|
215
|
+
self.assertIn("Invalid JSON response", str(context.exception))
|
|
216
|
+
|
|
217
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
|
|
218
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
|
|
219
|
+
def test_logout_success(self, mock_which, mock_run):
|
|
220
|
+
"""Test successful logout clears session data."""
|
|
221
|
+
|
|
222
|
+
mock_which.return_value = "/usr/bin/bw"
|
|
223
|
+
mock_run.side_effect = [
|
|
224
|
+
MagicMock(returncode=0, stdout="Logged in!"), # login
|
|
225
|
+
MagicMock(returncode=0, stdout="test_session_key"), # unlock
|
|
226
|
+
MagicMock(returncode=0, stdout="You have logged out."), # logout
|
|
227
|
+
]
|
|
228
|
+
|
|
229
|
+
manager = BitwardenManager(
|
|
230
|
+
self.client_id, self.client_secret, self.master_password
|
|
231
|
+
)
|
|
232
|
+
manager.login()
|
|
233
|
+
|
|
234
|
+
self.assertEqual(manager.session_key, "test_session_key")
|
|
235
|
+
|
|
236
|
+
manager.logout()
|
|
237
|
+
|
|
238
|
+
self.assertIsNone(manager.session_key)
|
|
239
|
+
self.assertEqual(mock_run.call_count, 3)
|
|
240
|
+
|
|
241
|
+
# Verify logout was called
|
|
242
|
+
logout_call = call(
|
|
243
|
+
["/usr/bin/bw", "logout"], capture_output=True, text=True, env=manager.env
|
|
244
|
+
)
|
|
245
|
+
self.assertIn(logout_call, mock_run.call_args_list)
|
|
246
|
+
|
|
247
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.subprocess.run")
|
|
248
|
+
@patch("grimoirelab_toolkit.credential_manager.bw_manager.shutil.which")
|
|
249
|
+
def test_logout_failure_still_clears_data(self, mock_which, mock_run):
|
|
250
|
+
"""Test logout still clears session data even when command fails."""
|
|
251
|
+
|
|
252
|
+
mock_which.return_value = "/usr/bin/bw"
|
|
253
|
+
mock_run.side_effect = [
|
|
254
|
+
MagicMock(returncode=0, stdout="Logged in!"), # login
|
|
255
|
+
MagicMock(returncode=0, stdout="test_session_key"), # unlock
|
|
256
|
+
MagicMock(returncode=1, stderr="Logout failed"), # logout
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
manager = BitwardenManager(
|
|
260
|
+
self.client_id, self.client_secret, self.master_password
|
|
261
|
+
)
|
|
262
|
+
manager.login()
|
|
263
|
+
manager.logout()
|
|
264
|
+
|
|
265
|
+
self.assertIsNone(manager.session_key)
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: grimoirelab-toolkit
|
|
3
|
-
Version: 1.2.2
|
|
4
|
-
Summary: Toolkit of common functions used across GrimoireLab
|
|
5
|
-
License: GPL-3.0+
|
|
6
|
-
License-File: AUTHORS
|
|
7
|
-
License-File: LICENSE
|
|
8
|
-
Keywords: development,grimoirelab
|
|
9
|
-
Author: GrimoireLab Developers
|
|
10
|
-
Requires-Python: >=3.10,<4.0
|
|
11
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
14
|
-
Classifier: Programming Language :: Python :: 3
|
|
15
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
-
Classifier: Topic :: Software Development
|
|
21
|
-
Requires-Dist: python-dateutil (>=2.8.2,<3.0.0)
|
|
22
|
-
Project-URL: Homepage, https://chaoss.github.io/grimoirelab/
|
|
23
|
-
Project-URL: Repository, https://github.com/chaoss/grimoirelab-toolkit
|
|
24
|
-
Description-Content-Type: text/markdown
|
|
25
|
-
|
|
26
|
-
# GrimoireLab Toolkit [](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push) [](https://coveralls.io/r/chaoss/grimoirelab-toolkit?branch=main)
|
|
27
|
-
|
|
28
|
-
Toolkit of common functions used across GrimoireLab projects.
|
|
29
|
-
|
|
30
|
-
This package provides a library composed by functions widely used in other
|
|
31
|
-
GrimoireLab projects. These function deal with date handling, introspection,
|
|
32
|
-
URIs/URLs, among other topics.
|
|
33
|
-
|
|
34
|
-
## Requirements
|
|
35
|
-
|
|
36
|
-
* Python >= 3.8
|
|
37
|
-
|
|
38
|
-
You will also need some other libraries for running the tool, you can find the
|
|
39
|
-
whole list of dependencies in [pyproject.toml](pyproject.toml) file.
|
|
40
|
-
|
|
41
|
-
## Installation
|
|
42
|
-
|
|
43
|
-
There are several ways to install GrimoireLab Toolkit on your system: packages or source
|
|
44
|
-
code using Poetry or pip.
|
|
45
|
-
|
|
46
|
-
### PyPI
|
|
47
|
-
|
|
48
|
-
GrimoireLab Toolkit can be installed using pip, a tool for installing Python packages.
|
|
49
|
-
To do it, run the next command:
|
|
50
|
-
```
|
|
51
|
-
$ pip install grimoirelab-toolkit
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### Source code
|
|
55
|
-
|
|
56
|
-
To install from the source code you will need to clone the repository first:
|
|
57
|
-
```
|
|
58
|
-
$ git clone https://github.com/chaoss/grimoirelab-toolkit
|
|
59
|
-
$ cd grimoirelab-toolkit
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
Then use pip or Poetry to install the package along with its dependencies.
|
|
63
|
-
|
|
64
|
-
#### Pip
|
|
65
|
-
To install the package from local directory run the following command:
|
|
66
|
-
```
|
|
67
|
-
$ pip install .
|
|
68
|
-
```
|
|
69
|
-
In case you are a developer, you should install GrimoireLab Toolkit in editable mode:
|
|
70
|
-
```
|
|
71
|
-
$ pip install -e .
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
#### Poetry
|
|
75
|
-
We use [poetry](https://python-poetry.org/) for dependency management and
|
|
76
|
-
packaging. You can install it following its [documentation](https://python-poetry.org/docs/#installation).
|
|
77
|
-
Once you have installed it, you can install GrimoireLab Toolkit and the dependencies in
|
|
78
|
-
a project isolated environment using:
|
|
79
|
-
```
|
|
80
|
-
$ poetry install
|
|
81
|
-
```
|
|
82
|
-
To spaw a new shell within the virtual environment use:
|
|
83
|
-
```
|
|
84
|
-
$ poetry shell
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
## License
|
|
88
|
-
|
|
89
|
-
Licensed under GNU General Public License (GPL), version 3 or later.
|
|
90
|
-
|
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
# GrimoireLab Toolkit [](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push) [](https://coveralls.io/r/chaoss/grimoirelab-toolkit?branch=main)
|
|
2
|
-
|
|
3
|
-
Toolkit of common functions used across GrimoireLab projects.
|
|
4
|
-
|
|
5
|
-
This package provides a library composed by functions widely used in other
|
|
6
|
-
GrimoireLab projects. These function deal with date handling, introspection,
|
|
7
|
-
URIs/URLs, among other topics.
|
|
8
|
-
|
|
9
|
-
## Requirements
|
|
10
|
-
|
|
11
|
-
* Python >= 3.8
|
|
12
|
-
|
|
13
|
-
You will also need some other libraries for running the tool, you can find the
|
|
14
|
-
whole list of dependencies in [pyproject.toml](pyproject.toml) file.
|
|
15
|
-
|
|
16
|
-
## Installation
|
|
17
|
-
|
|
18
|
-
There are several ways to install GrimoireLab Toolkit on your system: packages or source
|
|
19
|
-
code using Poetry or pip.
|
|
20
|
-
|
|
21
|
-
### PyPI
|
|
22
|
-
|
|
23
|
-
GrimoireLab Toolkit can be installed using pip, a tool for installing Python packages.
|
|
24
|
-
To do it, run the next command:
|
|
25
|
-
```
|
|
26
|
-
$ pip install grimoirelab-toolkit
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
### Source code
|
|
30
|
-
|
|
31
|
-
To install from the source code you will need to clone the repository first:
|
|
32
|
-
```
|
|
33
|
-
$ git clone https://github.com/chaoss/grimoirelab-toolkit
|
|
34
|
-
$ cd grimoirelab-toolkit
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
Then use pip or Poetry to install the package along with its dependencies.
|
|
38
|
-
|
|
39
|
-
#### Pip
|
|
40
|
-
To install the package from local directory run the following command:
|
|
41
|
-
```
|
|
42
|
-
$ pip install .
|
|
43
|
-
```
|
|
44
|
-
In case you are a developer, you should install GrimoireLab Toolkit in editable mode:
|
|
45
|
-
```
|
|
46
|
-
$ pip install -e .
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
#### Poetry
|
|
50
|
-
We use [poetry](https://python-poetry.org/) for dependency management and
|
|
51
|
-
packaging. You can install it following its [documentation](https://python-poetry.org/docs/#installation).
|
|
52
|
-
Once you have installed it, you can install GrimoireLab Toolkit and the dependencies in
|
|
53
|
-
a project isolated environment using:
|
|
54
|
-
```
|
|
55
|
-
$ poetry install
|
|
56
|
-
```
|
|
57
|
-
To spaw a new shell within the virtual environment use:
|
|
58
|
-
```
|
|
59
|
-
$ poetry shell
|
|
60
|
-
```
|
|
61
|
-
|
|
62
|
-
## License
|
|
63
|
-
|
|
64
|
-
Licensed under GNU General Public License (GPL), version 3 or later.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|