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

1"""Token storage solutions. 

2 

3Retrieve, store or delete tokens in your keyring. Load tokens from config files or 

4prompt the user for input. 

5""" 

6 

7from keyring.backend import get_all_keyring 

8import configparser 

9import os 

10import logging 

11 

12from typing import Optional 

13from .token import Token 

14 

15log = logging.getLogger(__name__) 

16 

17 

18DEFAULT_DEPLOYMENT = "https://snip.roentgen.physik.uni-goettingen.de/" 

19FILE_NAME = ".sniprc" 

20 

21 

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 

28 

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. 

33 

34 see :func:`get_token_from_keyring`, :func:`get_token_from_configs`, :func:`get_token_from_user` 

35 for more information. 

36 

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. 

45 

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 

54 

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 

60 

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 

66 

67 # Try user input 

68 if interactive: 

69 token = get_token_from_user(str(book_id), deployment_url) 

70 

71 if token is not None: 

72 log.info(f"Got token via user input.") 

73 return token 

74 

75 raise ValueError("Token not found in keyring, config or user input.") 

76 

77 

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. 

83 

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. 

90 

91 Returns 

92 ------- 

93 Token : Token | None 

94 The token object. None if no token is found. 

95 """ 

96 

97 if deployment_url is None: 

98 deployment_url = DEFAULT_DEPLOYMENT 

99 

100 token_str = None 

101 keyr = _get_keyring() 

102 

103 if keyr is None: 

104 log.debug("No keyring backend found.") 

105 return None 

106 

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 

113 

114 if token_str is None: 

115 log.debug("Token not found in keyring.") 

116 return None 

117 

118 return Token(book_id, deployment_url, token_str) 

119 

120 

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. 

127 

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 

133 

134 The configuration file should be in the ini format and have the following structure: 

135 

136 .. code-block:: ini 

137 [any_name] 

138 deployment = https://snip.roentgen.physik.uni-goettingen.de/ 

139 book_id = 123 

140 token = token 

141 

142 Section names can be arbitrary, but should be unique. The deployment is optional but highly encouraged. 

143 

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. 

152 

153 Returns 

154 ------- 

155 Token : Token | None 

156 The token object. None if no token is found. 

157 """ 

158 

159 def_depl = False 

160 if deployment_url is None: 

161 deployment_url = DEFAULT_DEPLOYMENT 

162 def_depl = True 

163 

164 if file is not None and not isinstance(file, list): 

165 file = [file] 

166 

167 conf = _get_file_config(file) 

168 

169 # Iter all config keys and raise an error if tokens are duplicated 

170 

171 tok: dict[str, str | None] = dict( 

172 string=None, 

173 section=None, 

174 ) 

175 

176 for section in conf.sections(): 

177 if conf[section].get("book_id") == book_id: 

178 

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 

184 

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 

192 

193 if tok["string"] is None: 

194 log.debug("Token not found in config files.") 

195 return None 

196 

197 return Token(book_id, deployment_url, tok["string"]) 

198 

199 

200def delete_token_from_keyring( 

201 book_id: str, 

202 deployment_url: Optional[str] = None, 

203) -> None: 

204 """Delete a token from the keyring. 

205 

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 """ 

213 

214 if deployment_url is None: 

215 deployment_url = DEFAULT_DEPLOYMENT 

216 

217 keyr = _get_keyring() 

218 if keyr is None: 

219 log.debug("No keyring backend found.") 

220 return 

221 

222 log.debug(f"Deleting token from keyring: {keyr.name}") 

223 

224 keyr.delete_password(deployment_url, str(book_id)) 

225 

226 

227def store_token_in_keyring( 

228 token: Token, 

229) -> None: 

230 """Store a token in the keyring with the highest priority backend. 

231 

232 Parameters 

233 ---------- 

234 token : Token 

235 The token object. 

236 """ 

237 

238 keyr = _get_keyring() 

239 if keyr is None: 

240 log.debug("No keyring backend found.") 

241 return 

242 

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.") 

250 

251 keyr.set_password(token.deployment_url, token.book_id, token.token) 

252 

253 

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. 

259 

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. 

266 

267 Returns 

268 ------- 

269 Token : Token | None 

270 The token object. None if no token is found. 

271 """ 

272 

273 if deployment_url is None: 

274 deployment_url = DEFAULT_DEPLOYMENT 

275 

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 

282 

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.") 

294 

295 return Token(book_id, deployment_url, token_s) 

296 

297 

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 

303 

304 return max(keyrings, key=lambda keyring: keyring.priority) 

305 

306 

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 """ 

315 

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"]) 

321 

322 # parse content with configparser 

323 # reverse order to put higher priority files last 

324 rc_files.reverse() 

325 

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}") 

329 

330 return config