mirror of
https://github.com/retspen/webvirtcloud
synced 2026-11-08 19:26:48 +00:00
feat(accounts): API tokens for scripts and tools
The REST API took only the login session, so a script could not use it without a browser. A user now makes API tokens on the profile page (API Tokens tab): a name, and an expiry of 30, 90 or 365 days or none. The token is shown once, on a page that is not cached, and then kept only as a SHA-512 digest; the list shows its name, first characters, creation and expiry, and a token is deleted there. Tools send it as "Authorization: Token wvc_...", and act as its user, with that user's permissions. Making and deleting a token is logged, without the token. A password alone (HTTP Basic) still does not reach the API, as it would skip the OTP second factor: a token is made in a session that passed the login and its OTP. The token class comes first, so a refused request gets 401 with its challenge instead of 403. django-rest-knox 5.1.0 provides the tokens (accounts.ApiToken adds the name); settings.py takes "knox", KNOX_TOKEN_MODEL and REST_KNOX from the template.
This commit is contained in:
parent
ba51b3e4d6
commit
1c60696ea1
14 changed files with 290 additions and 13 deletions
|
|
@ -82,6 +82,7 @@ INSTALLED_APPS = [
|
|||
"logs",
|
||||
"operations",
|
||||
"rest_framework",
|
||||
"knox",
|
||||
"axes",
|
||||
]
|
||||
|
||||
|
|
@ -362,9 +363,12 @@ OTP_ENABLED = False
|
|||
# REST Framework & OpenAPI (drf-spectacular)
|
||||
REST_FRAMEWORK = {
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
# Session only: a password alone (HTTP Basic) must not reach the API, as it
|
||||
# would skip the OTP second factor the login form enforces.
|
||||
# An API token, or a session: a password alone (HTTP Basic) must not reach
|
||||
# the API, as it would skip the OTP second factor the login form enforces;
|
||||
# tokens are made in a session. The token class comes first, so a refused
|
||||
# request gets 401 with its challenge, not 403.
|
||||
"DEFAULT_AUTHENTICATION_CLASSES": [
|
||||
"knox.auth.TokenAuthentication",
|
||||
"rest_framework.authentication.SessionAuthentication",
|
||||
],
|
||||
# A view that forgets its own permission_classes is not public.
|
||||
|
|
@ -373,6 +377,11 @@ REST_FRAMEWORK = {
|
|||
],
|
||||
}
|
||||
|
||||
# API tokens (django-rest-knox): kept as SHA-512 digests, sent as
|
||||
# "Authorization: Token wvc_..."; made and deleted on the profile page
|
||||
KNOX_TOKEN_MODEL = "accounts.ApiToken"
|
||||
REST_KNOX = {"TOKEN_PREFIX": "wvc_"}
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
"TITLE": "Webvirtcloud REST-API",
|
||||
"DESCRIPTION": "Webvirtcloud REST API",
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class SchemaPermissionTestCase(TestCase):
|
|||
def test_schema_requires_login(self):
|
||||
client = APIClient()
|
||||
for url in SCHEMA_URLS:
|
||||
self.assertEqual(client.get(url).status_code, 403, url)
|
||||
self.assertEqual(client.get(url).status_code, 401, url)
|
||||
|
||||
def test_any_user_may_read_the_schema(self):
|
||||
user = get_user_model().objects.create_user("schema_reader", password="pw")
|
||||
|
|
@ -41,4 +41,4 @@ class SchemaPermissionTestCase(TestCase):
|
|||
return Response({})
|
||||
|
||||
response = Probe.as_view()(APIRequestFactory().get("/probe/"))
|
||||
self.assertEqual(response.status_code, 403)
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ ALLOWED_FOR_ANY_USER = {
|
|||
"accounts:profile": "the user's own profile",
|
||||
"accounts:change_password": "the user's own password; new users get the permission by default",
|
||||
"accounts:ssh_key_create": "the user's own SSH keys",
|
||||
"accounts:api_token_create": "the user's own API tokens",
|
||||
"set_language": "UI language switch",
|
||||
"console": "renders an error without a valid token; access is checked per VM",
|
||||
"ds_openstack_index": "cloud-init datasource, fetched by VMs",
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@ from rest_framework.settings import api_settings
|
|||
|
||||
|
||||
class BasicAuthDisabledTestCase(TestCase):
|
||||
def test_session_is_the_only_default_authentication(self):
|
||||
# exactly SessionAuthentication: not Basic (a password-only door) and
|
||||
# not Token/JWT either, which would be another password-free door
|
||||
names = [c.__name__ for c in api_settings.DEFAULT_AUTHENTICATION_CLASSES]
|
||||
self.assertEqual(names, ["SessionAuthentication"])
|
||||
def test_a_token_or_a_session_are_the_only_default_authentication(self):
|
||||
# exactly knox's tokens and sessions: not Basic (a password-only door).
|
||||
# A token is no other door: it is made in a session that passed the
|
||||
# login and its OTP (accounts.views.api_token_create)
|
||||
classes = [f"{c.__module__}.{c.__name__}" for c in api_settings.DEFAULT_AUTHENTICATION_CLASSES]
|
||||
self.assertEqual(classes, ["knox.auth.TokenAuthentication",
|
||||
"rest_framework.authentication.SessionAuthentication"])
|
||||
|
||||
def test_api_rejects_http_basic_credentials(self):
|
||||
User = get_user_model()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue