Skip to content

Commit

Permalink
fix: API Key expiry field
Browse files Browse the repository at this point in the history
Signed-off-by: Trey <[email protected]>
  • Loading branch information
TreyWW committed Oct 19, 2024
1 parent 4cfb539 commit ba316f3
Show file tree
Hide file tree
Showing 11 changed files with 65 additions and 51 deletions.
2 changes: 1 addition & 1 deletion backend/core/api/public/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def authenticate_credentials(self, raw_key) -> tuple[User | Organization | None,
except model.DoesNotExist:
raise AuthenticationFailed("Invalid token.")

if token.has_expired():
if token.has_expired:
raise AuthenticationFailed("Token has expired.")

# todo: make sure this is safe to set request.user = <Team> obj
Expand Down
2 changes: 1 addition & 1 deletion backend/core/api/public/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ def process_request(self, request):
token_key = auth_header.split(" ")[1]
try:
token = APIAuthToken.objects.get(key=token_key, active=True)
if not token.has_expired():
if not token.has_expired:
request.auth = token
request.api_token = token
except APIAuthToken.DoesNotExist:
Expand Down
15 changes: 5 additions & 10 deletions backend/core/api/public/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import os
from django.utils import timezone

from backend.core.models import OwnerBase
from backend.core.models import OwnerBase, ExpiresBase


class APIAuthToken(OwnerBase):
class APIAuthToken(OwnerBase, ExpiresBase):
id = models.AutoField(primary_key=True)

hashed_key = models.CharField("Key", max_length=128, unique=True)
Expand All @@ -18,9 +18,9 @@ class APIAuthToken(OwnerBase):
description = models.TextField("Description", blank=True, null=True)
created = models.DateTimeField("Created", auto_now_add=True)
last_used = models.DateTimeField("Last Used", null=True, blank=True)
expires = models.DateTimeField("Expires", null=True, blank=True, help_text="Leave blank for no expiry")
expired = models.BooleanField("Expired", default=False, help_text="If the key has expired")
active = models.BooleanField("Active", default=True, help_text="If the key is active")
# expires = models.DateTimeField("Expires", null=True, blank=True, help_text="Leave blank for no expiry")
# expired = models.BooleanField("Expired", default=False, help_text="If the key has expired")
# active = models.BooleanField("Active", default=True, help_text="If the key is active")
scopes = models.JSONField("Scopes", default=list, help_text="List of permitted scopes")

class AdministratorServiceTypes(models.TextChoices):
Expand All @@ -36,11 +36,6 @@ class Meta:
def __str__(self):
return self.name

def has_expired(self):
if not self.expires:
return False
return self.expires < timezone.now()

def update_last_used(self):
self.last_used = timezone.now()
self.save()
Expand Down
4 changes: 2 additions & 2 deletions backend/core/api/settings/api_keys.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
@web_require_scopes("api_keys:write")
def generate_api_key_endpoint(request: WebRequest) -> HttpResponse:
name = request.POST.get("name")
expiry = request.POST.get("expiry")
expires = request.POST.get("expires")
description = request.POST.get("description")
administrator_toggle = True if request.POST.get("administrator") == "on" else False
administrator_type = request.POST.get("administrator_type")
Expand All @@ -29,7 +29,7 @@ def generate_api_key_endpoint(request: WebRequest) -> HttpResponse:
request.user.logged_in_as_team or request.user,
name,
permissions,
expires=expiry,
expires=expires,
description=description,
administrator_toggle=administrator_toggle,
administrator_type=administrator_type,
Expand Down
6 changes: 5 additions & 1 deletion backend/core/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,14 @@ def delete_if_expired_for(self, days: int = 14) -> bool:
@property
def remaining_active_time(self):
"""Return the remaining time until expiration, or None if already expired or no expiration set."""
if self.expires and self.expires > timezone.now():
if not self.has_expired:
return self.expires - timezone.now()
return None

@property
def has_expired(self):
return self.expires and self.expires <= timezone.now()

def is_active(self):
return self.active

Expand Down
41 changes: 17 additions & 24 deletions backend/core/service/api_keys/generate.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from django.core.exceptions import ValidationError

from backend.core.api.public import APIAuthToken
from backend.models import User, Organization
from backend.core.service.permissions.scopes import validate_scopes
Expand All @@ -12,17 +14,14 @@ def generate_public_api_key(
expires=None,
description=None,
administrator_toggle: bool = False,
administrator_type: str | None = None
administrator_type: str | None = None,
) -> tuple[APIAuthToken | None, str]:
if not validate_name(api_key_name):
return None, "Invalid key name"

if not validate_description(description):
return None, "Invalid description"

if not validate_expiry(expires):
return None, "Invalid expiry"

if api_key_exists_under_name(owner, api_key_name):
return None, "A key with this name already exists in your account"

Expand Down Expand Up @@ -52,6 +51,20 @@ def generate_public_api_key(
else:
token.user = owner

try:
token.full_clean()
except ValidationError as validation_errors:
field, error_list = next(iter(validation_errors.error_dict.items()))

field = "Permissions" if field == "scopes" else field.title()

if isinstance(error_list[0], ValidationError):
error_message = error_list[0].messages[0]
else:
error_message = error_list[0]

return None, f"{field}: {error_message}"

token.save()

return token, raw_key
Expand All @@ -74,26 +87,6 @@ def validate_description(description: str | None) -> bool:
return not description or len(description) <= 255


def validate_expiry(expires: str | int) -> bool:
"""
Accept no expiry
Accept expiry < 256
"""

if not expires:
return True

try:
expires = int(expires)
except ValueError:
return False

if expires < 0 or expires > 255:
return False

return True


def api_key_exists_under_name(owner: User | Organization, name: str | None) -> bool:
"""
Check if API key exists under a given name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def authenticate_api_key(request: WebRequest) -> APIAuthenticationServiceRespons
administrator_service_type=APIAuthToken.AdministratorServiceTypes.AWS_WEBHOOK_CALLBACK,
)

if token.has_expired():
if token.has_expired:
return APIAuthenticationServiceResponse(error_message="Token expired", status_code=400)
except APIAuthToken.DoesNotExist:
return APIAuthenticationServiceResponse(error_message="Token not found", status_code=400)
Expand Down
27 changes: 27 additions & 0 deletions backend/migrations/0067_remove_apiauthtoken_expired_and_more.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Generated by Django 5.1.1 on 2024-10-19 19:03

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("backend", "0066_delete_apikey_remove_verificationcodes_expiry_and_more"),
]

operations = [
migrations.RemoveField(
model_name="apiauthtoken",
name="expired",
),
migrations.AlterField(
model_name="apiauthtoken",
name="active",
field=models.BooleanField(default=True),
),
migrations.AlterField(
model_name="apiauthtoken",
name="expires",
field=models.DateTimeField(blank=True, help_text="When the item will expire", null=True, verbose_name="Expires"),
),
]
13 changes: 2 additions & 11 deletions frontend/templates/modals/generate_api_key.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,21 +23,12 @@
<div class="form-control">
<label class="label justify-start">
Expires
<span class="required_star">*</span>
<span class="tooltip tooltip-primary tooltip-right ml-2"
data-tip="When should this token expire? Set to 0 for no expiry.">
data-tip="When should this token expire? Leave blank for no expiry.">
<i class="fa fa-info-circle"></i>
</span>
</label>
<input class="peer input input-bordered"
name="expires"
maxlength="3"
value="0"
pattern="^(?:[0-9]|[1-9][0-9]|[1-2][0-9]{2}|3[0-5][0-9]|36[0-5])$"
required>
<label class="label peer-[&amp;:not(:placeholder-shown):not(:focus):invalid]:block hidden">
<span class="label-text-alt text-error">Please enter an expiry between 0 (no expiry) and 365 (364 days)</span>
</label>
<input class="peer input input-bordered" name="expires" type="date" required>
</div>
<div class="form-control">
<label class="label justify-start">Key Description</label>
Expand Down
2 changes: 2 additions & 0 deletions frontend/templates/pages/settings/pages/api_keys.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
<tr>
<th>Name</th>
<th>Last used</th>
<th>Expires</th>
<th>Created at</th>
<th>Actions</th>
</tr>
</thead>
Expand Down
2 changes: 2 additions & 0 deletions frontend/templates/pages/settings/settings/api_key_row.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
<tr>
<td>{{ key.name | default:"No name given" }}</td>
<td>{{ key.last_used | default:"Never used" }}</td>
<td>{{ key.expires | date:"d M, Y" | default:"Never" }}</td>
<td>{{ key.created | date:"d M, Y H:iA" }}</td>
<td>
<button class="btn btn-error btn-sm"
hx-delete="{% url 'api:settings:api_keys revoke' key_id=key.id %}"
Expand Down

0 comments on commit ba316f3

Please sign in to comment.