datalad_next.credman.CredentialManager
- class datalad_next.credman.CredentialManager(cfg: ConfigManager | None = None)[source]
Bases:
object
Facility to get, set, remove and query credentials.
A credential in this context is a set of properties (key-value pairs) associated with exactly one secret.
At present, the only backend for secret storage is the Python keyring package, as interfaced via a custom DataLad wrapper. Store for credential properties is implemented using DataLad's (i.e. Git's) configuration system. All properties are stored in the global (i.e., user) scope under configuration items following the pattern:
datalad.credential.<name>.<property>
where
<name>
is a credential name/identifier, and<property>
is an arbitrarily named credential property, whose name must follow the git-config syntax for variable names (case-insensitive, only alphanumeric characters and-
, and must start with an alphabetic character).Create a
CredentialManager
instance is fast, virtually no initialization needs to be performed. All internal properties are lazily evaluated. This facilitates usage in code where it is difficult to incorporate a long-lived central instance.API
With one exception, all parameter names of methods in the core API outside
**kwargs
must have a_
prefix that distinguishes credential properties from method parameters. The one exception is thename
parameter, which is used as a primary identifier (albeit being optional for some operations).The
obtain()
method is provided as an additional convenience, and implements a standard workflow for obtaining a credential in a wide variety of scenarios (credential name, credential properties, secret either respectively already known or yet unknown).- get(name=None, *, _prompt=None, _type_hint=None, **kwargs)[source]
Get properties and secret of a credential.
This is a read-only method that never modifies information stored on a credential in any backend.
Credential property lookup is supported via a number approaches. When providing
name
, all existing corresponding configuration items are found and reported, and an existing secret is retrieved from name-based secret backends (presentlykeyring
). When providing atype
property or a_type_hint
the lookup of additional properties in the keyring-backend is enabled, using predefined property name lists for a number of known credential types.For all given property keys that have no value assigned after the initial lookup, manual/interactive entry is attempted, whenever a custom
_prompt
was provided. This include requesting a secret. If manually entered information is contained in the return credential record, the record contains an additional_edited
property with a value ofTrue
.If no secret is known after lookup and a potential manual data entry, a plain
None
is returned instead of a full credential record.- Parameters:
name (str, optional) -- Name of the credential to be retrieved
_prompt (str or None) -- Instructions for credential entry to be displayed when missing properties are encountered. If
None
, manual entry is disabled._type_hint (str or None) -- In case no
type
property is included inkwargs
, this parameter is used to determine a credential type, to possibly enable further lookup/entry of additional properties for a known credential type**kwargs -- Credential property name/value pairs to overwrite/amend potentially existing properties. For any property with a value of
None
, manual data entry will be performed, unless a value could be retrieved on lookup, or prompting was not enabled.
- Returns:
Return
None
, if no secret for the credential was found or entered. Otherwise returns the complete credential record, comprising all properties and the secret. An additional_edited
key with a value ofTrue
is added whenever the returned record contains manually entered information.- Return type:
dict or None
- Raises:
ValueError -- When the method is called without any information that could be used to identify a credential
- obtain(name: str | None = None, *, prompt: str | None = None, type_hint: str | None = None, query_props: Dict | None = None, expected_props: List | Tuple | None = None)[source]
Obtain a credential by query or prompt (if needed)
This convenience method implements a standard workflow to obtain a credential. It supports credential selection by credential name/identifier, and falls back onto querying for a credential matching a set of specified properties (as key-value mappings). If no suitable credential is known, a user is prompted to enter one interactively (if possible in the current session).
If a credential was entered manually, any given
type_hint
will be included as atype
property of the returned credential, and the returned credential has an_edited=True
property. Likewise, anyrealm
property included in thequery_props
is included in the returned credential in this case.If desired, a credential workflow can be completed, after a credential was found to be valid/working, by storing or updating it in the credential store:
cm = CredentialManager() cname, cprops = cm.obtain(...) # verify credential is working ... # set/update cm.set(cname, _lastused=True, **cprops)
In the code sketch above, if
cname
isNone
(as it will be for a newly entered credential,set()
will prompt for a name to store the credential under, and will offer a user the choice to skip storing a credential. For any previously known credential, thelast-used
property will be updated to enable preferred selection in future credential discovery attempts viaobtain()
.Examples
Minimal call to get a credential entered (manually):
credman.obtain(type_hint='token', prompt='Credential please!')
Without a prompt text no interaction is attempted, and without a type hint it is unknown what (and how much) to enter.
Minimal call to retrieve a credential by its identifier:
credman.obtain('my-github-token')
Minimal call to retrieve the last-used credential for a particular authentication "realm". In this case "realm" is a property that was previously set to match a particular service/location, and is now used to match credentials against:
credman.obtain(query_props={'realm': 'mysecretlair'})
- Parameters:
name (str, optional) -- Name of the credential to be retrieved
prompt (str, optional) -- Passed to
CredentialManager.get()
if a credential name was provided, or no suitable credential could be found by querying.type_hint (str, optional) -- In case no
type
property is included inquery_props
, this parameter is passed toCredentialManager.get()
.query_props (dict, optional) -- Credential property to be used for querying for a suitable credential. When multiple credentials match a query, the last-used credential is selected.
expected_props (list or tuple, optional) -- When specified, a credential will be inspected to contain properties matching all listed property names, or a
ValueError
will be raised.
- Returns:
Credential name (possibly different from the input, when a credential was discovered based on properties), and credential properties.
- Return type:
(str, dict)
- Raises:
ValueError -- Raised when no matching credential could be found and none was entered. Also raised, when a credential selected from a query result or a manually entered one is missing any of the properties with a name given in
expected_props
.
- query(*, _sortby=None, _reverse=True, **kwargs)[source]
Query for all (matching) credentials, sorted by a property
This method is a companion of
query_()
, and the same limitations regarding credential discovery apply.In contrast to
query_()
, this method return a list instead of yielding credentials one by one. This returned list is optionally sorted.- Parameters:
_sortby (str, optional) -- Name of a credential property to provide a value to sort by. Credentials that do not carry the specified property always sort last, regardless of sort order.
_reverse (bool, optional) -- Flag whether to sort ascending or descending when sorting. By default credentials are return in descending property value order. This flag does not impact the fact that credentials without the property to sort by always sort last.
**kwargs -- Pass on as-is to
query_()
- Returns:
Each item is a 2-tuple. The first element in each tuple is the credential name, the second element is the credential record as returned by
get()
for any matching credential.- Return type:
list(str, dict)
- query_(**kwargs)[source]
Query for all (matching) credentials.
Credentials are yielded in no particular order.
This method cannot find credentials for which only a secret was deposited in the keyring.
This method does support lookup of credentials defined in DataLad's "provider" configurations.
- Parameters:
**kwargs -- If not given, any found credential is yielded. Otherwise, any credential must match all property name/value pairs
- Yields:
tuple(str, dict) -- The first element in the tuple is the credential name, the second element is the credential record as returned by
get()
for any matching credential.
- remove(name, *, type_hint=None)[source]
Remove a credential, including all properties and secret
Presently, all supported backends require the specification of a credential
name
for lookup. This may change in the future, when support for alternative backends is added, at which point thename
parameter would become optional, and additional parameters would be added.- Returns:
True if a credential was removed, and False if not (because no respective credential was found).
- Return type:
bool
- Raises:
RuntimeError -- This exception is raised whenever a property cannot be removed successfully. Likely cause is that it is defined in a configuration scope or backend for which write-access is not supported.
- secret_names = {'user_password': 'password'}
- set(name, *, _lastused=False, _suggested_name=None, _context=None, **kwargs)[source]
Set credential properties and secret
Presently, all supported backends require the specification of a credential
name
for storage. This may change in the future, when support for alternative backends is added, at which point thename
parameter would become optional.All properties provided as kwargs with keys not starting with _ and with values that are not
None
will be stored. Ifkwargs
do not contain asecret
specification, manual entry will be attempted. The associated prompt with be either the name of thesecret
field of a known credential (as identified via atype
property), or the label'secret'
.All properties with an associated value of
None
will be removed (unset).- Parameters:
name (str or None) -- Credential name. If None, the name will be prompted for and setting the credential is skipped if no name is provided.
_lastused (bool, optional) -- If set, automatically add an additional credential property
'last-used'
with the current timestamp in ISO 8601 format._suggested_name (str, optional) -- If name is None, this name (if given) is presented as a default suggestion that can be accepted without having to enter it manually. If this name suggestion conflicts with an existing credential, it is ignored and not presented as a suggestion.
_context (str, optional) -- If given, will be included in the prompt for a missing credential name to provide context for a user. It should be written to fit into a parenthical statement after "Enter a name to save the credential (...)", e.g. "for download from <URL>".
**kwargs -- Any number of credential property key/value pairs to set (update), or remove. With one exception, values of
None
indicate removal of a property from a credential. However,secret=None
does not lead to the removal of a credential's secret, because it would result in an incomplete credential. Instead, it will cause a credential's effectivesecret
property to be written to the secret store. The effective secret might come from other sources, such as particular configuration scopes or environment variables (i.e., matching thedatalad.credential.<name>.secret
configuration item. Properties whose names start with an underscore are automatically removed prior storage.
- Returns:
key/values of all modified credential properties with respect to their previously recorded values. None is returned in case a user did not enter a missing credential name. If a user entered a credential name, it is included in the returned dictionary under the 'name' key.
- Return type:
dict or None
- Raises:
RuntimeError -- This exception is raised whenever a property cannot be removed successfully. Likely cause is that it is defined in a configuration scope or backend for which write-access is not supported.
ValueError -- When property names in kwargs are not syntax-compliant.
- valid_property_names_regex = re.compile('[a-z0-9]+[a-z0-9-]*$')