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.
Files changed (26) hide show
  1. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/NEWS +5 -0
  2. grimoirelab_toolkit-1.2.3/PKG-INFO +205 -0
  3. grimoirelab_toolkit-1.2.3/README.md +179 -0
  4. grimoirelab_toolkit-1.2.3/grimoirelab_toolkit/_version.py +2 -0
  5. grimoirelab_toolkit-1.2.3/grimoirelab_toolkit/credential_manager/__init__.py +36 -0
  6. grimoirelab_toolkit-1.2.3/grimoirelab_toolkit/credential_manager/__main__.py +4 -0
  7. grimoirelab_toolkit-1.2.3/grimoirelab_toolkit/credential_manager/bw_manager.py +212 -0
  8. grimoirelab_toolkit-1.2.3/grimoirelab_toolkit/credential_manager/exceptions.py +53 -0
  9. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/pyproject.toml +1 -1
  10. grimoirelab_toolkit-1.2.3/tests/__init__.py +0 -0
  11. grimoirelab_toolkit-1.2.3/tests/test_bw_manager.py +265 -0
  12. grimoirelab_toolkit-1.2.2/PKG-INFO +0 -90
  13. grimoirelab_toolkit-1.2.2/README.md +0 -64
  14. grimoirelab_toolkit-1.2.2/grimoirelab_toolkit/_version.py +0 -2
  15. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/AUTHORS +0 -0
  16. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/LICENSE +0 -0
  17. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/grimoirelab_toolkit/__init__.py +0 -0
  18. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/grimoirelab_toolkit/datetime.py +0 -0
  19. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/grimoirelab_toolkit/identities.py +0 -0
  20. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/grimoirelab_toolkit/introspect.py +0 -0
  21. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/grimoirelab_toolkit/uris.py +0 -0
  22. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/tests/run_tests.py +0 -0
  23. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/tests/test_datetime.py +0 -0
  24. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/tests/test_identities.py +0 -0
  25. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/tests/test_introspect.py +0 -0
  26. {grimoirelab_toolkit-1.2.2 → grimoirelab_toolkit-1.2.3}/tests/test_uris.py +0 -0
@@ -1,5 +1,10 @@
1
1
  # Releases
2
2
 
3
+ ## grimoirelab-toolkit 1.2.3 - (2025-11-25)
4
+
5
+ No changes list available.
6
+
7
+
3
8
  ## grimoirelab-toolkit 1.2.2 - (2025-11-11)
4
9
 
5
10
  * Update Poetry's package dependencies
@@ -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 [![Build Status](https://github.com/chaoss/grimoirelab-toolkit/workflows/tests/badge.svg)](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push) [![Coverage Status](https://img.shields.io/coveralls/chaoss/grimoirelab-toolkit.svg)](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 [![Build Status](https://github.com/chaoss/grimoirelab-toolkit/workflows/tests/badge.svg)](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push) [![Coverage Status](https://img.shields.io/coveralls/chaoss/grimoirelab-toolkit.svg)](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,2 @@
1
+ # File auto-generated by semverup on 2025-11-25 14:48:35.382838
2
+ __version__ = "1.2.3"
@@ -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,4 @@
1
+ from .credential_manager import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -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
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "grimoirelab-toolkit"
3
- version = "1.2.2"
3
+ version = "1.2.3"
4
4
  description = "Toolkit of common functions used across GrimoireLab"
5
5
  authors = [
6
6
  "GrimoireLab Developers"
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 [![Build Status](https://github.com/chaoss/grimoirelab-toolkit/workflows/tests/badge.svg)](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push) [![Coverage Status](https://img.shields.io/coveralls/chaoss/grimoirelab-toolkit.svg)](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 [![Build Status](https://github.com/chaoss/grimoirelab-toolkit/workflows/tests/badge.svg)](https://github.com/chaoss/grimoirelab-toolkit/actions?query=workflow:tests+branch:main+event:push) [![Coverage Status](https://img.shields.io/coveralls/chaoss/grimoirelab-toolkit.svg)](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.
@@ -1,2 +0,0 @@
1
- # File auto-generated by semverup on 2025-11-11 12:19:50.852511
2
- __version__ = "1.2.2"