Coverage for src/snip/token/storage.py: 45%
121 statements
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-20 14:59 +0200
« prev ^ index » next coverage.py v7.6.1, created at 2024-09-20 14:59 +0200
1"""Token storage solutions.
3Retrieve, store or delete tokens in your keyring. Load tokens from config files or
4prompt the user for input.
5"""
7from keyring.backend import get_all_keyring
8import configparser
9import os
10import logging
12from typing import Optional
13from .token import Token
15log = logging.getLogger(__name__)
18DEFAULT_DEPLOYMENT = "https://snip.roentgen.physik.uni-goettingen.de/"
19FILE_NAME = ".sniprc"
22def get_token(
23 book_id: str,
24 deployment_url: Optional[str],
25 interactive: bool = True,
26) -> Token:
27 """Get a token with all methods possible
29 Retrieves a token from keyring, config files or user input. The token is
30 retrieved first from the keyring, if not found it is retrieved from the config
31 files. If the token is not found in the config files, the user is prompted for
32 input.
34 see :func:`get_token_from_keyring`, :func:`get_token_from_configs`, :func:`get_token_from_user`
35 for more information.
37 Parameters
38 ----------
39 book_id : str
40 The book id for which the token is stored.
41 deployment_url : str, optional
42 The deployment url. Defaults to DEFAULT_DEPLOYMENT.
43 interactive : bool, optional
44 Whether to prompt the user for input, errors if user input is required.
46 Returns
47 -------
48 Token : Token
49 The token object. Error if no token is found.
50 """
51 token: Token | None = None
52 if deployment_url is None:
53 deployment_url = DEFAULT_DEPLOYMENT
55 # Try keyring first
56 token = get_token_from_keyring(str(book_id), deployment_url)
57 if token is not None:
58 log.info(f"Found token in keyring.")
59 return token
61 # Try config files
62 token = get_token_from_configs(str(book_id), deployment_url)
63 if token is not None:
64 log.info(f"Found token in config file.")
65 return token
67 # Try user input
68 if interactive:
69 token = get_token_from_user(str(book_id), deployment_url)
71 if token is not None:
72 log.info(f"Got token via user input.")
73 return token
75 raise ValueError("Token not found in keyring, config or user input.")
78def get_token_from_keyring(
79 book_id: str,
80 deployment_url: Optional[str] = None,
81) -> Token | None:
82 """Get a token from the keyring with the highest priority backend.
84 Parameters
85 ----------
86 book_id : str
87 The book id for which the token is stored.
88 deployment_url : str, optional
89 The deployment url.
91 Returns
92 -------
93 Token : Token | None
94 The token object. None if no token is found.
95 """
97 if deployment_url is None:
98 deployment_url = DEFAULT_DEPLOYMENT
100 token_str = None
101 keyr = _get_keyring()
103 if keyr is None:
104 log.debug("No keyring backend found.")
105 return None
107 log.debug(f"Retrieving token from keyring: {keyr.name}")
108 try:
109 token_str = keyr.get_password(deployment_url, str(book_id))
110 except Exception as e:
111 log.error(f"Error retrieving token from keyring: {e}")
112 return None
114 if token_str is None:
115 log.debug("Token not found in keyring.")
116 return None
118 return Token(book_id, deployment_url, token_str)
121def get_token_from_configs(
122 book_id: str,
123 deployment_url: Optional[str] = None,
124 file: list[str] | str | None = None,
125) -> Token | None:
126 """Get a token from the config files.
128 We try to retrieve the token from the following config files:
129 - local: .sniprc
130 - user: ~/.sniprc
131 - global: /etc/snip_lab/.sniprc
132 - environment variable: SNIPRC
134 The configuration file should be in the ini format and have the following structure:
136 .. code-block:: ini
137 [any_name]
138 deployment = https://snip.roentgen.physik.uni-goettingen.de/
139 book_id = 123
140 token = token
142 Section names can be arbitrary, but should be unique. The deployment is optional but highly encouraged.
144 Parameters
145 ----------
146 book_id : str
147 The book id for which the token is stored.
148 deployment_url : str, optional
149 The deployment url. Defaults to DEFAULT_DEPLOYMENT.
150 file : list[str] | str, optional
151 The file or list of files to read the token from. If None, the default locations are tried.
153 Returns
154 -------
155 Token : Token | None
156 The token object. None if no token is found.
157 """
159 def_depl = False
160 if deployment_url is None:
161 deployment_url = DEFAULT_DEPLOYMENT
162 def_depl = True
164 if file is not None and not isinstance(file, list):
165 file = [file]
167 conf = _get_file_config(file)
169 # Iter all config keys and raise an error if tokens are duplicated
171 tok: dict[str, str | None] = dict(
172 string=None,
173 section=None,
174 )
176 for section in conf.sections():
177 if conf[section].get("book_id") == book_id:
179 # Check if deployment is the same or not set if default
180 if (def_depl and conf[section].get("deployment") is not None) and (
181 conf[section].get("deployment") != deployment_url
182 ):
183 continue
185 token = conf[section].get("token")
186 if tok["string"] is not None:
187 raise ValueError(
188 f"Duplicate token found in config files. {section} and {tok['section']} hold the same book_id and deployment."
189 )
190 tok["string"] = token
191 tok["section"] = section
193 if tok["string"] is None:
194 log.debug("Token not found in config files.")
195 return None
197 return Token(book_id, deployment_url, tok["string"])
200def delete_token_from_keyring(
201 book_id: str,
202 deployment_url: Optional[str] = None,
203) -> None:
204 """Delete a token from the keyring.
206 Parameters
207 ----------
208 book_id : str
209 The book id for which the token is stored.
210 deployment_url : str, optional
211 The deployment url.
212 """
214 if deployment_url is None:
215 deployment_url = DEFAULT_DEPLOYMENT
217 keyr = _get_keyring()
218 if keyr is None:
219 log.debug("No keyring backend found.")
220 return
222 log.debug(f"Deleting token from keyring: {keyr.name}")
224 keyr.delete_password(deployment_url, str(book_id))
227def store_token_in_keyring(
228 token: Token,
229) -> None:
230 """Store a token in the keyring with the highest priority backend.
232 Parameters
233 ----------
234 token : Token
235 The token object.
236 """
238 keyr = _get_keyring()
239 if keyr is None:
240 log.debug("No keyring backend found.")
241 return
243 log.debug(f"Storing token in keyring: {keyr.name}")
244 if token.deployment_url is None:
245 token.deployment_url = DEFAULT_DEPLOYMENT
246 if token.book_id is None:
247 raise ValueError("Token book_id cannot be None.")
248 if token.token is None:
249 raise ValueError("Token token cannot be None.")
251 keyr.set_password(token.deployment_url, token.book_id, token.token)
254def get_token_from_user(
255 book_id: str,
256 deployment_url: Optional[str] = None,
257) -> Token | None:
258 """Get a token from user input.
260 Parameters
261 ----------
262 book_id : str
263 The book id for which the token is stored.
264 deployment_url : str, optional
265 The deployment url.
267 Returns
268 -------
269 Token : Token | None
270 The token object. None if no token is found.
271 """
273 if deployment_url is None:
274 deployment_url = DEFAULT_DEPLOYMENT
276 # Ask for a new token and catch cancel
277 try:
278 token_s = input(f"Enter token for book {book_id} at {deployment_url}: ")
279 except Exception as e:
280 log.error(f"Error getting token from user: {e}")
281 return None
283 # Ask if user wants to save token in keyring
284 save = input("Do you want to save the token in the keyring? (y/n): ")
285 if save.lower() == "y":
286 keyr = _get_keyring()
287 if keyr is not None:
288 keyr.set_password(deployment_url, str(book_id), token_s)
289 log.info(f"Token saved in keyring: {keyr.name}")
290 else:
291 log.error("No keyring backend found. Token not saved.")
292 else:
293 log.info("Token not saved in keyring.")
295 return Token(book_id, deployment_url, token_s)
298def _get_keyring():
299 """Retrieves keyring with the highest priority backend."""
300 keyrings = get_all_keyring()
301 if not keyrings or len(keyrings) == 0:
302 return None
304 return max(keyrings, key=lambda keyring: keyring.priority)
307def _get_file_config(files: list[str] | None = None) -> configparser.ConfigParser:
308 """
309 The following files are tried:
310 - local: .sniprc
311 - user: ~/.sniprc
312 - global: /etc/snip_lab/.sniprc
313 - environment variable: SNIPRC
314 """
316 # define all possible file locations
317 locations = ["", "~/", "/etc/snip_lab/"]
318 rc_files = [loc + FILE_NAME for loc in locations]
319 if "SNIPRC" in os.environ:
320 rc_files.insert(0, os.environ["SNIPRC"])
322 # parse content with configparser
323 # reverse order to put higher priority files last
324 rc_files.reverse()
326 config = configparser.ConfigParser()
327 config.read(files if files else rc_files)
328 log.debug(f"Read config from {files if files else rc_files}")
330 return config