Repository: datalad/datalad-next

Issue:
Better support for lonely token credential specification
```sh
DATALAD_CREDENTIAL_MIKE_TOKEN=some datalad credentials get mike
mike(secret ✗): [token=some]
```

So right now it considers it to be a credential, but a credential without a secret and a `token` property.

This is suboptimal. This means that tokens can end up in a config file, more or less silently. It also means that we do not know anything about the nature of the credential (although we could), and we cannot say whether it is complete or not (although we could).

I'd say we should not only refuse to store credentials without a secret, but we should also disregard any credentials that can be determined to be incomplete.

In the case above a user could just use `secret` instead of `token`, but if they do not, we should either refuse (credential with no secret) or auto-convert (credential of type token with secret as given by the token env var).

Comments:

Patch:
diff --git a/datalad_next/credman/manager.py b/datalad_next/credman/manager.py
index 4d4167f..167a64c 100644
--- a/datalad_next/credman/manager.py
+++ b/datalad_next/credman/manager.py
@@ -201,8 +201,9 @@ class CredentialManager(object):
             # no secret, no credential
             if any(not p.startswith('_') for p in cred):
                 lgr.debug(
-                    'Not reporting on credential fragment with no secret: %r',
-                    cred,
+                    'Not reporting on credential fragment '
+                    '(name=%r) with no secret: %r',
+                    name, cred,
                 )
             return
 
@@ -727,6 +728,17 @@ class CredentialManager(object):
         _type_hint = cred.get('type', _type_hint)
         if _type_hint:
             cred['type'] = _type_hint
+            return
+
+        # if we get here, we don't know what type this is
+        # let's derive one for a few clear-cut cases where we can be
+        # reasonable sure what type a credential is
+        if set(cred) == set(('token',)):
+            # all we have is a token property -- very likely a token-type
+            # credential. Move the token to the secret property and
+            # assign the type
+            cred['type'] = 'token'
+            cred['secret'] = cred.pop('token')
 
     def _complete_credential_props(
             self, name: str,


Test patch:
diff --git a/datalad_next/credman/tests/test_credman.py b/datalad_next/credman/tests/test_credman.py
index 07523bf..bb04db5 100644
--- a/datalad_next/credman/tests/test_credman.py
+++ b/datalad_next/credman/tests/test_credman.py
@@ -215,6 +215,21 @@ def test_credman_get():
     assert 'mysecret' == res['secret']
 
 
+def test_credman_get_guess_type():
+    # define token-only-no-type credential in config override
+    credman = CredentialManager(
+        ConfigManager(overrides={
+            'datalad.credential.mike.token': 'some',
+        })
+    )
+    # we get it reported fine, token property converted to the
+    # 'secret' and a proper 'type' assigned
+    assert credman.get('mike') == {
+        'secret': 'some',
+        'type': 'token',
+    }
+
+
 def test_credman_obtain(memory_keyring):
     credman = CredentialManager(ConfigManager())
     # senseless, but valid call