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 the name 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 (presently keyring). When providing a type 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 of True.

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 in kwargs, 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 of True 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 a type property of the returned credential, and the returned credential has an _edited=True property. Likewise, any realm property included in the query_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 is None (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, the last-used property will be updated to enable preferred selection in future credential discovery attempts via obtain().

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 in query_props, this parameter is passed to CredentialManager.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 the name 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 the name parameter would become optional.

All properties provided as kwargs with keys not starting with _ and with values that are not None will be stored. If kwargs do not contain a secret specification, manual entry will be attempted. The associated prompt with be either the name of the secret field of a known credential (as identified via a type 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 effective secret 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 the datalad.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-]*$')