/
backend.py
258 lines (207 loc) · 7.43 KB
/
backend.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
"""
Keyring implementation support
"""
import os
import abc
import logging
import operator
import copy
from typing import Optional
from .py310compat import metadata
from . import credentials, errors, util
from .util import properties
log = logging.getLogger(__name__)
by_priority = operator.attrgetter('priority')
_limit = None
class KeyringBackendMeta(abc.ABCMeta):
"""
A metaclass that's both an ABCMeta and a type that keeps a registry of
all (non-abstract) types.
"""
def __init__(cls, name, bases, dict):
super().__init__(name, bases, dict)
if not hasattr(cls, '_classes'):
cls._classes = set()
classes = cls._classes
if not cls.__abstractmethods__:
classes.add(cls)
class KeyringBackend(metaclass=KeyringBackendMeta):
"""The abstract base class of the keyring, every backend must implement
this interface.
"""
def __init__(self):
self.set_properties_from_env()
# @abc.abstractproperty
def priority(cls):
"""
Each backend class must supply a priority, a number (float or integer)
indicating the priority of the backend relative to all other backends.
The priority need not be static -- it may (and should) vary based
attributes of the environment in which is runs (platform, available
packages, etc.).
A higher number indicates a higher priority. The priority should raise
a RuntimeError with a message indicating the underlying cause if the
backend is not suitable for the current environment.
As a rule of thumb, a priority between zero but less than one is
suitable, but a priority of one or greater is recommended.
"""
@properties.ClassProperty
@classmethod
def viable(cls):
with errors.ExceptionRaisedContext() as exc:
cls.priority
return not exc
@classmethod
def get_viable_backends(cls):
"""
Return all subclasses deemed viable.
"""
return filter(operator.attrgetter('viable'), cls._classes)
@properties.ClassProperty
@classmethod
def name(cls):
"""
The keyring name, suitable for display.
The name is derived from module and class name.
"""
parent, sep, mod_name = cls.__module__.rpartition('.')
mod_name = mod_name.replace('_', ' ')
return ' '.join([mod_name, cls.__name__])
def __str__(self):
keyring_class = type(self)
return "{}.{} (priority: {:g})".format(
keyring_class.__module__, keyring_class.__name__, keyring_class.priority
)
@abc.abstractmethod
def get_password(self, service: str, username: str) -> Optional[str]:
"""Get password of the username for the service"""
return None
@abc.abstractmethod
def set_password(self, service: str, username: str, password: str) -> None:
"""Set password for the username of the service.
If the backend cannot store passwords, raise
PasswordSetError.
"""
raise errors.PasswordSetError("reason")
# for backward-compatibility, don't require a backend to implement
# delete_password
# @abc.abstractmethod
def delete_password(self, service: str, username: str) -> None:
"""Delete the password for the username of the service.
If the backend cannot delete passwords, raise
PasswordDeleteError.
"""
raise errors.PasswordDeleteError("reason")
# for backward-compatibility, don't require a backend to implement
# get_credential
# @abc.abstractmethod
def get_credential(
self,
service: str,
username: Optional[str],
) -> Optional[credentials.Credential]:
"""Gets the username and password for the service.
Returns a Credential instance.
The *username* argument is optional and may be omitted by
the caller or ignored by the backend. Callers must use the
returned username.
"""
# The default implementation requires a username here.
if username is not None:
password = self.get_password(service, username)
if password is not None:
return credentials.SimpleCredential(username, password)
return None
def set_properties_from_env(self):
"""For all KEYRING_PROPERTY_* env var, set that property."""
def parse(item):
key, value = item
pre, sep, name = key.partition('KEYRING_PROPERTY_')
return sep and (name.lower(), value)
props = filter(None, map(parse, os.environ.items()))
for name, value in props:
setattr(self, name, value)
def with_properties(self, **kwargs):
alt = copy.copy(self)
vars(alt).update(kwargs)
return alt
class Crypter:
"""Base class providing encryption and decryption"""
@abc.abstractmethod
def encrypt(self, value):
"""Encrypt the value."""
pass
@abc.abstractmethod
def decrypt(self, value):
"""Decrypt the value."""
pass
class NullCrypter(Crypter):
"""A crypter that does nothing"""
def encrypt(self, value):
return value
def decrypt(self, value):
return value
def _load_plugins():
"""
Locate all setuptools entry points by the name 'keyring backends'
and initialize them.
Any third-party library may register an entry point by adding the
following to their setup.cfg::
[options.entry_points]
keyring.backends =
plugin_name = mylib.mymodule:initialize_func
`plugin_name` can be anything, and is only used to display the name
of the plugin at initialization time.
`initialize_func` is optional, but will be invoked if callable.
"""
for ep in metadata.entry_points(group='keyring.backends'):
try:
log.debug('Loading %s', ep.name)
init_func = ep.load()
if callable(init_func):
init_func()
except Exception:
log.exception(f"Error initializing plugin {ep}.")
@util.once
def get_all_keyring():
"""
Return a list of all implemented keyrings that can be constructed without
parameters.
"""
_load_plugins()
viable_classes = KeyringBackend.get_viable_backends()
rings = util.suppress_exceptions(viable_classes, exceptions=TypeError)
return list(rings)
class SchemeSelectable:
"""
Allow a backend to select different "schemes" for the
username and service.
>>> backend = SchemeSelectable()
>>> backend._query('contoso', 'alice')
{'username': 'alice', 'service': 'contoso'}
>>> backend._query('contoso')
{'service': 'contoso'}
>>> backend.scheme = 'KeePassXC'
>>> backend._query('contoso', 'alice')
{'UserName': 'alice', 'Title': 'contoso'}
>>> backend._query('contoso', 'alice', foo='bar')
{'UserName': 'alice', 'Title': 'contoso', 'foo': 'bar'}
"""
scheme = 'default'
schemes = dict(
default=dict(username='username', service='service'),
KeePassXC=dict(username='UserName', service='Title'),
)
def _query(self, service, username=None, **base):
scheme = self.schemes[self.scheme]
return dict(
{
scheme['username']: username,
scheme['service']: service,
}
if username
else {
scheme['service']: service,
},
**base,
)