fix REST API CSRF and auth handling

This commit is contained in:
Nick Sweeting 2024-09-03 14:16:44 -07:00
parent 41a318a8bd
commit 01094ecb03
No known key found for this signature in database
9 changed files with 164 additions and 89 deletions

View file

@ -1,12 +1,63 @@
from django.contrib import admin from django.contrib import admin
from datetime import datetime
from django.utils.html import format_html
from api.auth import get_or_create_api_token
def get_abid_info(self, obj, request=None):
try:
return format_html(
# URL Hash: <code style="font-size: 10px; user-select: all">{}</code><br/>
'''
<a href="{}" style="font-size: 16px; font-family: monospace; user-select: all; border-radius: 8px; background-color: #ddf; padding: 3px 5px; border: 1px solid #aaa; margin-bottom: 8px; display: inline-block; vertical-align: top;">{}</a> &nbsp; &nbsp; <a href="{}" style="color: limegreen; font-size: 0.9em; vertical-align: 1px; font-family: monospace;">📖 API DOCS</a>
<br/><hr/>
<div style="opacity: 0.8">
&nbsp; &nbsp; <small style="opacity: 0.8">.abid: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; user-select: all">{}</code></small><br/>
&nbsp; &nbsp; <small style="opacity: 0.8">.abid.uuid: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; user-select: all">{}</code></small><br/>
&nbsp; &nbsp; <small style="opacity: 0.8">.id: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px; user-select: all">{}</code></small><br/>
<hr/>
&nbsp; &nbsp; TS: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; {}</code> &nbsp; &nbsp; &nbsp;&nbsp; {}: <code style="user-select: all">{}</code><br/>
&nbsp; &nbsp; URI: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; "><b style="user-select: all">{}</b> &nbsp; &nbsp; {}</code> &nbsp;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; <span style="display:inline-block; vertical-align: -4px; width: 290px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{}: <code style="user-select: all">{}</code></span>
&nbsp; SALT: &nbsp; <code style="font-size: 10px;"><b style="display:inline-block; user-select: all; width: 50px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{}</b></code><br/>
&nbsp; &nbsp; SUBTYPE: &nbsp; &nbsp; &nbsp; <code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {}</code> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {}: <code style="user-select: all">{}</code><br/>
&nbsp; &nbsp; RAND: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; &nbsp; &nbsp; {}</code> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {}: <code style="user-select: all">{}</code>
<br/><hr/>
&nbsp; &nbsp; <small style="opacity: 0.5">.old_id: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px; user-select: all">{}</code></small><br/>
</div>
''',
obj.api_url + (f'?api_key={get_or_create_api_token(request.user)}' if request and request.user else ''), obj.api_url, obj.api_docs_url,
str(obj.abid),
str(obj.ABID.uuid),
str(obj.id),
obj.ABID.ts, str(obj.ABID.uuid)[0:14], obj.abid_ts_src, obj.abid_values['ts'].isoformat() if isinstance(obj.abid_values['ts'], datetime) else obj.abid_values['ts'],
obj.ABID.uri, str(obj.ABID.uuid)[14:26], obj.abid_uri_src, str(obj.abid_values['uri']),
obj.ABID.uri_salt,
obj.ABID.subtype, str(obj.ABID.uuid)[26:28], obj.abid_subtype_src, str(obj.abid_values['subtype']),
obj.ABID.rand, str(obj.ABID.uuid)[28:36], obj.abid_rand_src, str(obj.abid_values['rand'])[-7:],
str(getattr(obj, 'old_id', '')),
)
except Exception as e:
return str(e)
class ABIDModelAdmin(admin.ModelAdmin): class ABIDModelAdmin(admin.ModelAdmin):
list_display = ('created', 'created_by', 'abid', '__str__') list_display = ('created', 'created_by', 'abid', '__str__')
sort_fields = ('created', 'created_by', 'abid', '__str__') sort_fields = ('created', 'created_by', 'abid', '__str__')
readonly_fields = ('abid', 'created', '__str__') readonly_fields = ('created', 'modified', '__str__', 'API')
def API(self, obj):
return get_abid_info(self, obj, request=self.request)
def queryset(self, request):
self.request = request
return super().queryset(request)
def change_view(self, request, object_id, form_url="", extra_context=None):
self.request = request
return super().change_view(request, object_id, form_url, extra_context)
def get_form(self, request, obj=None, **kwargs): def get_form(self, request, obj=None, **kwargs):
self.request = request
form = super().get_form(request, obj, **kwargs) form = super().get_form(request, obj, **kwargs)
if 'created_by' in form.base_fields: if 'created_by' in form.base_fields:
form.base_fields['created_by'].initial = request.user form.base_fields['created_by'].initial = request.user

View file

@ -18,6 +18,7 @@ from django.db import models
from django.utils import timezone from django.utils import timezone
from django.db.utils import OperationalError from django.db.utils import OperationalError
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.urls import reverse_lazy
from django_stubs_ext.db.models import TypedModelMeta from django_stubs_ext.db.models import TypedModelMeta
@ -210,6 +211,15 @@ class ABIDModel(models.Model):
Get a typeid.TypeID (stripe-style) representation of the object's ABID. Get a typeid.TypeID (stripe-style) representation of the object's ABID.
""" """
return self.ABID.typeid return self.ABID.typeid
@property
def api_url(self) -> str:
# /api/v1/core/any/{abid}
return reverse_lazy('api-1:get_any', args=[self.abid])
@property
def api_docs_url(self) -> str:
return f'/api/v1/docs#/{self._meta.app_label.title()}%20Models/api_v1_{self._meta.app_label}_get_{self._meta.db_table}'

View file

@ -1,13 +1,34 @@
__package__ = 'archivebox.api' __package__ = 'archivebox.api'
from typing import Optional, cast from typing import Any, Optional, cast
from datetime import timedelta
from django.http import HttpRequest from django.http import HttpRequest
from django.utils import timezone
from django.contrib.auth import login from django.contrib.auth import login
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.contrib.auth.models import AbstractBaseUser from django.contrib.auth.models import AbstractBaseUser
from ninja.security import HttpBearer, APIKeyQuery, APIKeyHeader, HttpBasicAuth, django_auth_superuser from ninja.security import HttpBearer, APIKeyQuery, APIKeyHeader, HttpBasicAuth, django_auth_superuser
from ninja.errors import HttpError
def get_or_create_api_token(user):
from api.models import APIToken
if user and user.is_superuser:
api_tokens = APIToken.objects.filter(created_by_id=user.pk, expires__gt=timezone.now())
if api_tokens.exists():
# unexpired token exists, use it
api_token = api_tokens.last()
else:
# does not exist, create a new one
api_token = APIToken.objects.create(created_by_id=user.pk, expires=timezone.now() + timedelta(days=30))
assert api_token.is_valid(), f"API token is not valid {api_token}"
return api_token
return None
def auth_using_token(token, request: Optional[HttpRequest]=None) -> Optional[AbstractBaseUser]: def auth_using_token(token, request: Optional[HttpRequest]=None) -> Optional[AbstractBaseUser]:
@ -16,21 +37,20 @@ def auth_using_token(token, request: Optional[HttpRequest]=None) -> Optional[Abs
user = None user = None
submitted_empty_form = token in ('string', '', None) submitted_empty_form = str(token).strip() in ('string', '', 'None', 'null')
if submitted_empty_form: if not submitted_empty_form:
assert request is not None, 'No request provided for API key authentication'
user = request.user # see if user is authed via django session and use that as the default
else:
try: try:
token = APIToken.objects.get(token=token) token = APIToken.objects.get(token=token)
if token.is_valid(): if token.is_valid():
user = token.created_by user = token.created_by
request._api_token = token
except APIToken.DoesNotExist: except APIToken.DoesNotExist:
pass pass
if not user: if not user:
print('[❌] Failed to authenticate API user using API Key:', request) # print('[❌] Failed to authenticate API user using API Key:', request)
return None return None
return cast(AbstractBaseUser, user) return cast(AbstractBaseUser, user)
def auth_using_password(username, password, request: Optional[HttpRequest]=None) -> Optional[AbstractBaseUser]: def auth_using_password(username, password, request: Optional[HttpRequest]=None) -> Optional[AbstractBaseUser]:
@ -38,17 +58,14 @@ def auth_using_password(username, password, request: Optional[HttpRequest]=None)
user = None user = None
submitted_empty_form = (username, password) in (('string', 'string'), ('', ''), (None, None)) submitted_empty_form = (username, password) in (('string', 'string'), ('', ''), (None, None))
if submitted_empty_form: if not submitted_empty_form:
assert request is not None, 'No request provided for API key authentication'
user = request.user # see if user is authed via django session and use that as the default
else:
user = authenticate( user = authenticate(
username=username, username=username,
password=password, password=password,
) )
if not user: if not user:
print('[❌] Failed to authenticate API user using API Key:', request) # print('[❌] Failed to authenticate API user using API Key:', request)
user = None user = None
return cast(AbstractBaseUser | None, user) return cast(AbstractBaseUser | None, user)
@ -56,28 +73,41 @@ def auth_using_password(username, password, request: Optional[HttpRequest]=None)
### Base Auth Types ### Base Auth Types
class APITokenAuthCheck: class APITokenAuthCheck:
"""The base class for authentication methods that use an api.models.APIToken""" """The base class for authentication methods that use an api.models.APIToken"""
def authenticate(self, request: HttpRequest, key: Optional[str]=None) -> Optional[AbstractBaseUser]: def authenticate(self, request: HttpRequest, key: Optional[str]=None) -> Optional[AbstractBaseUser]:
user = auth_using_token( request.user = auth_using_token(
token=key, token=key,
request=request, request=request,
) )
if user is not None: if request.user and request.user.pk:
login(request, user, backend='django.contrib.auth.backends.ModelBackend') # Don't set cookie/persist login ouside this erquest, user may be accessing the API from another domain (CSRF/CORS):
return user # login(request, request.user, backend='django.contrib.auth.backends.ModelBackend')
request._api_auth_method = self.__class__.__name__
if not request.user.is_superuser:
raise HttpError(403, 'Valid API token but User does not have permission (make sure user.is_superuser=True)')
return request.user
class UserPassAuthCheck: class UserPassAuthCheck:
"""The base class for authentication methods that use a username & password""" """The base class for authentication methods that use a username & password"""
def authenticate(self, request: HttpRequest, username: Optional[str]=None, password: Optional[str]=None) -> Optional[AbstractBaseUser]: def authenticate(self, request: HttpRequest, username: Optional[str]=None, password: Optional[str]=None) -> Optional[AbstractBaseUser]:
user = auth_using_password( request.user = auth_using_password(
username=username, username=username,
password=password, password=password,
request=request, request=request,
) )
if user is not None: if request.user and request.user.pk:
login(request, user, backend='django.contrib.auth.backends.ModelBackend') # Don't set cookie/persist login ouside this erquest, user may be accessing the API from another domain (CSRF/CORS):
return user # login(request, request.user, backend='django.contrib.auth.backends.ModelBackend')
request._api_auth_method = self.__class__.__name__
if not request.user.is_superuser:
raise HttpError(403, 'Valid API token but User does not have permission (make sure user.is_superuser=True)')
return request.user
### Django-Ninja-Provided Auth Methods ### Django-Ninja-Provided Auth Methods
@ -98,7 +128,6 @@ class UsernameAndPasswordAuth(UserPassAuthCheck, HttpBasicAuth):
"""Allow authenticating by passing username & password via HTTP Basic Authentication (not recommended)""" """Allow authenticating by passing username & password via HTTP Basic Authentication (not recommended)"""
pass pass
### Enabled Auth Methods ### Enabled Auth Methods
API_AUTH_METHODS = [ API_AUTH_METHODS = [

View file

@ -53,7 +53,26 @@ class NinjaAPIWithIOCapture(NinjaAPI):
response = super().create_temporal_response(request) response = super().create_temporal_response(request)
print('RESPONDING NOW', response) # Diable caching of API responses entirely
response['Cache-Control'] = 'no-store'
# Add debug stdout and stderr headers to response
response['X-ArchiveBox-Stdout'] = str(request.stdout)[200:]
response['X-ArchiveBox-Stderr'] = str(request.stderr)[200:]
# response['X-ArchiveBox-View'] = self.get_openapi_operation_id(request) or 'Unknown'
# Add Auth Headers to response
api_token = getattr(request, '_api_token', None)
token_expiry = api_token.expires.isoformat() if api_token else 'Never'
response['X-ArchiveBox-Auth-Method'] = getattr(request, '_api_auth_method', None) or 'None'
response['X-ArchiveBox-Auth-Expires'] = token_expiry
response['X-ArchiveBox-Auth-Token-Id'] = api_token.abid if api_token else 'None'
response['X-ArchiveBox-Auth-User-Id'] = request.user.pk if request.user.pk else 'None'
response['X-ArchiveBox-Auth-User-Username'] = request.user.username if request.user.pk else 'None'
# import ipdb; ipdb.set_trace()
# print('RESPONDING NOW', response)
return response return response

View file

@ -7,10 +7,10 @@ from django.utils import timezone
from datetime import timedelta from datetime import timedelta
from api.models import APIToken from api.models import APIToken
from api.auth import auth_using_token, auth_using_password from api.auth import auth_using_token, auth_using_password, get_or_create_api_token
router = Router(tags=['Authentication']) router = Router(tags=['Authentication'], auth=None)
class PasswordAuthSchema(Schema): class PasswordAuthSchema(Schema):
@ -28,14 +28,8 @@ def get_api_token(request, auth_data: PasswordAuthSchema):
) )
if user and user.is_superuser: if user and user.is_superuser:
api_tokens = APIToken.objects.filter(created_by_id=user.pk, expires__gt=timezone.now()) api_token = get_or_create_api_token(user)
if api_tokens.exists(): assert api_token is not None, "Failed to create API token"
api_token = api_tokens.last()
else:
api_token = APIToken.objects.create(created_by_id=user.pk, expires=timezone.now() + timedelta(days=30))
assert api_token.is_valid(), f"API token is not valid {api_token.abid}"
return api_token.__json__() return api_token.__json__()
return {"success": False, "errors": ["Invalid credentials"]} return {"success": False, "errors": ["Invalid credentials"]}

View file

@ -16,8 +16,10 @@ from ..util import ansi_to_html
from ..config import ONLY_NEW from ..config import ONLY_NEW
from .auth import API_AUTH_METHODS
# router for API that exposes archivebox cli subcommands as REST endpoints # router for API that exposes archivebox cli subcommands as REST endpoints
router = Router(tags=['ArchiveBox CLI Sub-Commands']) router = Router(tags=['ArchiveBox CLI Sub-Commands'], auth=API_AUTH_METHODS)
# Schemas # Schemas

View file

@ -12,11 +12,15 @@ from django.contrib.auth import get_user_model
from ninja import Router, Schema, FilterSchema, Field, Query from ninja import Router, Schema, FilterSchema, Field, Query
from ninja.pagination import paginate, PaginationBase from ninja.pagination import paginate, PaginationBase
from ninja.errors import HttpError
from core.models import Snapshot, ArchiveResult, Tag from core.models import Snapshot, ArchiveResult, Tag
from api.models import APIToken, OutboundWebhook
from abid_utils.abid import ABID from abid_utils.abid import ABID
router = Router(tags=['Core Models']) from .auth import API_AUTH_METHODS
router = Router(tags=['Core Models'], auth=API_AUTH_METHODS)
@ -421,4 +425,10 @@ def get_any(request, abid: str):
except Exception: except Exception:
pass pass
return response if abid.startswith(APIToken.abid_prefix):
raise HttpError(403, 'APIToken objects are not accessible via REST API')
if abid.startswith(OutboundWebhook.abid_prefix):
raise HttpError(403, 'OutboundWebhook objects are not accessible via REST API')
raise HttpError(404, 'Object with given ABID not found')

View file

@ -34,6 +34,7 @@ from core.models import Snapshot, ArchiveResult, Tag, SnapshotTag
from core.forms import AddLinkForm from core.forms import AddLinkForm
from core.mixins import SearchResultsAdminMixin from core.mixins import SearchResultsAdminMixin
from api.models import APIToken from api.models import APIToken
from api.auth import get_or_create_api_token
from abid_utils.models import get_or_create_system_user_pk from abid_utils.models import get_or_create_system_user_pk
from abid_utils.admin import ABIDModelAdmin from abid_utils.admin import ABIDModelAdmin
@ -268,37 +269,7 @@ class SnapshotActionForm(ActionForm):
# ) # )
def get_abid_info(self, obj):
return format_html(
# URL Hash: <code style="font-size: 10px; user-select: all">{}</code><br/>
'''
<a href="{}" style="font-size: 16px; font-family: monospace; user-select: all; border-radius: 8px; background-color: #ddf; padding: 3px 5px; border: 1px solid #aaa; margin-bottom: 8px; display: inline-block; vertical-align: top;">{}</a> &nbsp; &nbsp; <a href="{}" style="color: limegreen; font-size: 0.9em; vertical-align: 1px; font-family: monospace;">📖 API DOCS</a>
<br/><hr/>
<div style="opacity: 0.8">
&nbsp; &nbsp; <small style="opacity: 0.8">.abid: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; user-select: all">{}</code></small><br/>
&nbsp; &nbsp; <small style="opacity: 0.8">.abid.uuid: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; user-select: all">{}</code></small><br/>
&nbsp; &nbsp; <small style="opacity: 0.8">.id: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px; user-select: all">{}</code></small><br/>
<hr/>
&nbsp; &nbsp; TS: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; {}</code> &nbsp; &nbsp; &nbsp;&nbsp; {}: <code style="user-select: all">{}</code><br/>
&nbsp; &nbsp; URI: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px; "><b style="user-select: all">{}</b> &nbsp; &nbsp; {}</code> &nbsp;&nbsp; &nbsp; &nbsp; &nbsp;&nbsp; <span style="display:inline-block; vertical-align: -4px; width: 290px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{}: <code style="user-select: all">{}</code></span>
&nbsp; SALT: &nbsp; <code style="font-size: 10px;"><b style="display:inline-block; user-select: all; width: 50px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;">{}</b></code><br/>
&nbsp; &nbsp; SUBTYPE: &nbsp; &nbsp; &nbsp; <code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {}</code> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {}: <code style="user-select: all">{}</code><br/>
&nbsp; &nbsp; RAND: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; <code style="font-size: 10px;"><b style="user-select: all">{}</b> &nbsp; &nbsp; &nbsp; {}</code> &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {}: <code style="user-select: all">{}</code>
<br/><hr/>
&nbsp; &nbsp; <small style="opacity: 0.5">.old_id: &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;<code style="font-size: 10px; user-select: all">{}</code></small><br/>
</div>
''',
obj.api_url, obj.api_url, obj.api_docs_url,
str(obj.abid),
str(obj.ABID.uuid),
str(obj.id),
obj.ABID.ts, str(obj.ABID.uuid)[0:14], obj.abid_ts_src, obj.abid_values['ts'].isoformat() if isinstance(obj.abid_values['ts'], datetime) else obj.abid_values['ts'],
obj.ABID.uri, str(obj.ABID.uuid)[14:26], obj.abid_uri_src, str(obj.abid_values['uri']),
obj.ABID.uri_salt,
obj.ABID.subtype, str(obj.ABID.uuid)[26:28], obj.abid_subtype_src, str(obj.abid_values['subtype']),
obj.ABID.rand, str(obj.ABID.uuid)[28:36], obj.abid_rand_src, str(obj.abid_values['rand'])[-7:],
str(getattr(obj, 'old_id', '')),
)
@admin.register(Snapshot, site=archivebox_admin) @admin.register(Snapshot, site=archivebox_admin)
@ -321,6 +292,7 @@ class SnapshotAdmin(SearchResultsAdminMixin, ABIDModelAdmin):
show_full_result_count = False show_full_result_count = False
def changelist_view(self, request, extra_context=None): def changelist_view(self, request, extra_context=None):
self.request = request
extra_context = extra_context or {} extra_context = extra_context or {}
try: try:
return super().changelist_view(request, extra_context | GLOBAL_CONTEXT) return super().changelist_view(request, extra_context | GLOBAL_CONTEXT)
@ -329,6 +301,7 @@ class SnapshotAdmin(SearchResultsAdminMixin, ABIDModelAdmin):
return super().changelist_view(request, GLOBAL_CONTEXT) return super().changelist_view(request, GLOBAL_CONTEXT)
def change_view(self, request, object_id, form_url="", extra_context=None): def change_view(self, request, object_id, form_url="", extra_context=None):
self.request = request
snapshot = None snapshot = None
try: try:
@ -350,6 +323,7 @@ class SnapshotAdmin(SearchResultsAdminMixin, ABIDModelAdmin):
if snapshot: if snapshot:
object_id = str(snapshot.id) object_id = str(snapshot.id)
return super().change_view( return super().change_view(
request, request,
object_id, object_id,
@ -430,12 +404,6 @@ class SnapshotAdmin(SearchResultsAdminMixin, ABIDModelAdmin):
obj.extension or '-', obj.extension or '-',
) )
def API(self, obj):
try:
return get_abid_info(self, obj)
except Exception as e:
return str(e)
@admin.display( @admin.display(
description='Title', description='Title',
ordering='title', ordering='title',
@ -603,8 +571,6 @@ class SnapshotAdmin(SearchResultsAdminMixin, ABIDModelAdmin):
# actions = ['delete_selected'] # actions = ['delete_selected']
# ordering = ['-id'] # ordering = ['-id']
# def API(self, obj):
# return get_abid_info(self, obj)
@admin.register(Tag, site=archivebox_admin) @admin.register(Tag, site=archivebox_admin)
@ -619,11 +585,6 @@ class TagAdmin(ABIDModelAdmin):
paginator = AccelleratedPaginator paginator = AccelleratedPaginator
def API(self, obj):
try:
return get_abid_info(self, obj)
except Exception as e:
return str(e)
def num_snapshots(self, tag): def num_snapshots(self, tag):
return format_html( return format_html(
@ -660,6 +621,10 @@ class ArchiveResultAdmin(ABIDModelAdmin):
paginator = AccelleratedPaginator paginator = AccelleratedPaginator
def change_view(self, request, object_id, form_url="", extra_context=None):
self.request = request
return super().change_view(request, object_id, form_url, extra_context)
@admin.display( @admin.display(
description='Snapshot Info' description='Snapshot Info'
) )
@ -672,12 +637,6 @@ class ArchiveResultAdmin(ABIDModelAdmin):
result.snapshot.url[:128], result.snapshot.url[:128],
) )
def API(self, obj):
try:
return get_abid_info(self, obj)
except Exception as e:
raise e
return str(e)
@admin.display( @admin.display(
description='Snapshot Tags' description='Snapshot Tags'
@ -735,7 +694,7 @@ class ArchiveResultAdmin(ABIDModelAdmin):
class APITokenAdmin(ABIDModelAdmin): class APITokenAdmin(ABIDModelAdmin):
list_display = ('created', 'abid', 'created_by', 'token_redacted', 'expires') list_display = ('created', 'abid', 'created_by', 'token_redacted', 'expires')
sort_fields = ('abid', 'created', 'created_by', 'expires') sort_fields = ('abid', 'created', 'created_by', 'expires')
readonly_fields = ('abid', 'created') readonly_fields = ('created', 'modified', 'API')
search_fields = ('id', 'abid', 'created_by__username', 'token') search_fields = ('id', 'abid', 'created_by__username', 'token')
fields = ('created_by', 'token', 'expires', *readonly_fields) fields = ('created_by', 'token', 'expires', *readonly_fields)
@ -747,4 +706,4 @@ class APITokenAdmin(ABIDModelAdmin):
class CustomWebhookAdmin(WebhookAdmin, ABIDModelAdmin): class CustomWebhookAdmin(WebhookAdmin, ABIDModelAdmin):
list_display = ('created', 'created_by', 'abid', *WebhookAdmin.list_display) list_display = ('created', 'created_by', 'abid', *WebhookAdmin.list_display)
sort_fields = ('created', 'created_by', 'abid', 'referenced_model', 'endpoint', 'last_success', 'last_error') sort_fields = ('created', 'created_by', 'abid', 'referenced_model', 'endpoint', 'last_success', 'last_error')
readonly_fields = ('abid', 'created', *WebhookAdmin.readonly_fields) readonly_fields = ('created', 'modified', 'API', *WebhookAdmin.readonly_fields)

View file

@ -298,10 +298,11 @@ SECURE_REFERRER_POLICY = 'strict-origin-when-cross-origin'
CSRF_COOKIE_SECURE = False CSRF_COOKIE_SECURE = False
SESSION_COOKIE_SECURE = False SESSION_COOKIE_SECURE = False
SESSION_COOKIE_HTTPONLY = True
SESSION_COOKIE_DOMAIN = None SESSION_COOKIE_DOMAIN = None
SESSION_COOKIE_AGE = 1209600 # 2 weeks SESSION_COOKIE_AGE = 1209600 # 2 weeks
SESSION_EXPIRE_AT_BROWSER_CLOSE = False SESSION_EXPIRE_AT_BROWSER_CLOSE = False
SESSION_SAVE_EVERY_REQUEST = True SESSION_SAVE_EVERY_REQUEST = False
SESSION_ENGINE = "django.contrib.sessions.backends.db" SESSION_ENGINE = "django.contrib.sessions.backends.db"