Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement credential nicknames #34

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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,
),
]
16 changes: 15 additions & 1 deletion scimma_admin/hopskotch_auth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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. """
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@ <h2>New Credentials Generated</h2>
<h4>This is the only time the password will be revealed.</h4>
<p>Username:</p><pre>{{ username }}</pre>
<p>Password:</p><pre>{{ password }}</pre>
<p>Nickname:</p><pre>{{ nickname }}</pre>
</div>
<form action="{% url 'download' %}" method="post">
{% csrf_token %}
<input type="hidden" value="{{ username }}" name="username">
<input type="hidden" value="{{ password }}" name="password">
<input type="hidden" value="{{ nickname }}" name="nickname">
<button class="btn btn-outline-primary" type="submit" name="download" id="download">Download</button>
</form>
<p>These credentials will be usable within 10 seconds.</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
{% block page-header %}Credential Management{% endblock %}

{% block page-body %}
<h2>Credential {{ cred.username }}</h2>
<h2>Credential {{ cred.username }} ({{ cred.nickname }})</h2>
cnweaver marked this conversation as resolved.
Show resolved Hide resolved

{% if cred.suspended %}
<section>
Expand Down
12 changes: 11 additions & 1 deletion scimma_admin/hopskotch_auth/templates/hopskotch_auth/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ <h2>Active credentials</h2>
<thead>
<tr>
<th scope="col">Kafka Username</th>
<th scope="col">Nickname</th>
<th scope="col">Created On (Pacific Time)</th>
<th scope="col">Actions</th>
</tr>
Expand All @@ -28,6 +29,7 @@ <h2>Active credentials</h2>
{% for cred in credentials %}
<tr>
<td scope="row">{{ cred.username }}</td>
<td scope="row">{{ cred.nickname }}</td>
<td>{{ cred.created_at|localtime }}</td>
<td>
<form action="{% url 'edit_credential' %}" method="get">
Expand All @@ -43,7 +45,7 @@ <h2>Active credentials</h2>
</tr>
{% empty %}
<tr>
<td class="info" colspan="3">No existing credentials found.</td>
<td class="info" colspan="4">No existing credentials found.</td>
</tr>
{% endfor %}
</tbody>
Expand All @@ -56,6 +58,14 @@ <h2>Create credentials</h2>
<p>A username and password will be generated for you.</p>
<form action="{% url 'create' %}" method="post">
{% csrf_token %}
<label for="nickname">Nickname (Optional)</label>
<button type="button" class="btn btn-outline-info btn-sm" data-toggle="popover"
title="Help" data-content="A nickname is a name for your own convenience, to make it
easier to recognize different credentials. It is a good idea to choose a nickname
based on what you plan to use a credential for, like 'laptop-analysis' or
'alert-sender'. If you do not choose one, it will be the same as the auto-generated
username.">?</button>
<input type="text" name="nickname" class="vTextField" maxlength="64" id="nickname">
<input class="btn btn-outline-primary" type="submit" value="Create new credentials">
</form>
</section>
Expand Down
30 changes: 26 additions & 4 deletions scimma_admin/hopskotch_auth/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
)


Expand Down Expand Up @@ -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,kafkahost,nickname\n")
cnweaver marked this conversation as resolved.
Show resolved Hide resolved
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')
Expand Down