diff --git a/scimma_admin/hopskotch_auth/migrations/0006_scramcredentials_nickname.py b/scimma_admin/hopskotch_auth/migrations/0006_scramcredentials_nickname.py new file mode 100644 index 0000000..27db853 --- /dev/null +++ b/scimma_admin/hopskotch_auth/migrations/0006_scramcredentials_nickname.py @@ -0,0 +1,19 @@ +# Generated by Django 3.0.7 on 2021-03-22 19:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hopskotch_auth', '0005_add_authz'), + ] + + operations = [ + migrations.AddField( + model_name='scramcredentials', + name='nickname', + field=models.CharField(default='', max_length=64), + preserve_default=False, + ), + ] diff --git a/scimma_admin/hopskotch_auth/models.py b/scimma_admin/hopskotch_auth/models.py index 68414e2..369af18 100644 --- a/scimma_admin/hopskotch_auth/models.py +++ b/scimma_admin/hopskotch_auth/models.py @@ -87,6 +87,12 @@ class SCRAMCredentials(models.Model): default = False, ) + # a user-chosen nickname, which need not be globally unique and may be changed + nickname = models.CharField( + max_length = 64, + editable = True, + ) + @classmethod def generate(cls, owner: User, username: str, password: str, alg: SCRAMAlgorithm, salt: Optional[bytes] = None, iterations: int = 4096): @@ -168,7 +174,7 @@ def delete_credentials(user, cred_username): creds.delete() -def new_credentials(owner): +def new_credentials(owner, nickname: str = ""): username = rand_username(owner) alphabet = string.ascii_letters + string.digits @@ -181,6 +187,7 @@ def new_credentials(owner): alg=SCRAMAlgorithm.SHA512, salt=rand_salt, ) + creds.nickname=nickname if (len(nickname) > 0) else username creds.save() bundle = CredentialGenerationBundle( creds=creds, @@ -190,6 +197,13 @@ def new_credentials(owner): return bundle +def check_credential_nickname(owner: User, nickname: str) -> bool: + """ Return whether the specified credential nickname is free for use (not already in use) by + the given user. + """ + return not SCRAMCredentials.objects.filter(owner=owner, nickname=nickname).exists() + + @dataclass class CredentialGenerationBundle: """ The collection of data generated ephemerally for new user credentials. """ diff --git a/scimma_admin/hopskotch_auth/templates/hopskotch_auth/create.html b/scimma_admin/hopskotch_auth/templates/hopskotch_auth/create.html index f286e98..faa6f46 100644 --- a/scimma_admin/hopskotch_auth/templates/hopskotch_auth/create.html +++ b/scimma_admin/hopskotch_auth/templates/hopskotch_auth/create.html @@ -22,11 +22,13 @@

New Credentials Generated

This is the only time the password will be revealed.

Username:

{{ username }}

Password:

{{ password }}
+

Nickname:

{{ nickname }}
{% csrf_token %} +

These credentials will be usable within 10 seconds.

diff --git a/scimma_admin/hopskotch_auth/templates/hopskotch_auth/edit_credential.html b/scimma_admin/hopskotch_auth/templates/hopskotch_auth/edit_credential.html index d589994..ebda194 100644 --- a/scimma_admin/hopskotch_auth/templates/hopskotch_auth/edit_credential.html +++ b/scimma_admin/hopskotch_auth/templates/hopskotch_auth/edit_credential.html @@ -4,7 +4,8 @@ {% block page-header %}Credential Management{% endblock %} {% block page-body %} -

Credential {{ cred.username }}

+

Credential {{ cred.username }} + {% if cred.nickname != cred.username %} ({{ cred.nickname }}){% endif %}

{% if cred.suspended %}
diff --git a/scimma_admin/hopskotch_auth/templates/hopskotch_auth/index.html b/scimma_admin/hopskotch_auth/templates/hopskotch_auth/index.html index 3510b54..9bb3953 100644 --- a/scimma_admin/hopskotch_auth/templates/hopskotch_auth/index.html +++ b/scimma_admin/hopskotch_auth/templates/hopskotch_auth/index.html @@ -20,6 +20,7 @@

Active credentials

Kafka Username + Nickname Created On (Pacific Time) Actions @@ -28,6 +29,7 @@

Active credentials

{% for cred in credentials %} {{ cred.username }} + {{ cred.nickname }} {{ cred.created_at|localtime }}
@@ -43,7 +45,7 @@

Active credentials

{% empty %} - No existing credentials found. + No existing credentials found. {% endfor %} @@ -56,6 +58,14 @@

Create credentials

A username and password will be generated for you.

{% csrf_token %} + + +
diff --git a/scimma_admin/hopskotch_auth/views.py b/scimma_admin/hopskotch_auth/views.py index c25532f..b27e47f 100644 --- a/scimma_admin/hopskotch_auth/views.py +++ b/scimma_admin/hopskotch_auth/views.py @@ -67,12 +67,30 @@ def login_failure(request): def create(request): logger.info(f"User {request.user.username} ({request.user.email}) requested " f"to create a new credential from {request.META['REMOTE_ADDR']}") - bundle = new_credentials(request.user) + + nickname = "" + if "nickname" in request.POST: + nickname = request.POST.get("nickname") + logger.info(f"User requested credential nickname is {nickname}") + + if len(nickname) > SCRAMCredentials.nickname.field.max_length: + return redirect_with_error(request, "Create a credential", + "requested credential nickname too long", + "index") + + # check only non-empty nicknames, as empty ones will be replaced with the auto-generated + # name by new_credentials + if len(nickname) > 0 and not check_credential_nickname(request.user, nickname): + return redirect_with_error(request, "Create a credential", + "requested credential nickname is already in use", + "index") + + bundle = new_credentials(request.user, nickname) logger.info(f"Created new credential {bundle.username} on behalf of user " f"{request.user.username} ({request.user.email})") return render( request, 'hopskotch_auth/create.html', - dict(username=bundle.username, password=bundle.password), + dict(username=bundle.username, password=bundle.password, nickname=bundle.creds.nickname), ) @@ -106,8 +124,12 @@ def delete(request): @login_required def download(request): myfile = StringIO() - myfile.write("username,password\n") - myfile.write(f"{request.POST['username']},{request.POST['password']}") + myfile.write("username,password,nickname\n") + myfile.write(f"{request.POST['username']},{request.POST['password']}") + if('nickname' in request.POST): + myfile.write(f",{request.POST['nickname']}") + else: + myfile.write(",") myfile.flush() myfile.seek(0) # move the pointer to the beginning of the buffer response = HttpResponse(FileWrapper(myfile), content_type='text/plain')