move api auth from authorization header to X-API-Key header
This commit is contained in:
parent
f64e65f297
commit
4491f68dc7
8 changed files with 43 additions and 41 deletions
|
@ -44,20 +44,20 @@ def get_status(request):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class APITokenSchema(Schema):
|
class APIKeySchema(Schema):
|
||||||
token: NonEmptyStr = APIField(
|
key: NonEmptyStr = APIField(
|
||||||
title="API token",
|
title="API key",
|
||||||
description="API token to be directly used with `Authorization: Bearer <token>` HTTP header."
|
description="API secret to be directly used with `X-API-Key` HTTP header."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@auth_api_router.get('/session/', response=APITokenSchema, auth=None,
|
@auth_api_router.get('/session/', response=APIKeySchema, auth=None,
|
||||||
summary="get session-bound token")
|
summary="get session-bound key")
|
||||||
def session_token(request):
|
def session_key(request):
|
||||||
"""
|
"""
|
||||||
Get an API token that is bound to the transmitted session cookie.
|
Get an API key that is bound to the transmitted session cookie.
|
||||||
|
|
||||||
Keep in mind that this API token will be invalid if the session gets signed out or similar.
|
Keep in mind that this API key will be invalid if the session gets signed out or similar.
|
||||||
"""
|
"""
|
||||||
session_id = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
|
session_id = request.COOKIES.get(settings.SESSION_COOKIE_NAME, None)
|
||||||
return {"token": "anonymous" if session_id is None else f"session:{session_id}"}
|
return {"key": "anonymous" if session_id is None else f"session:{session_id}"}
|
||||||
|
|
|
@ -6,10 +6,10 @@ from importlib import import_module
|
||||||
from django.contrib.auth import get_user as auth_get_user
|
from django.contrib.auth import get_user as auth_get_user
|
||||||
from django.contrib.auth.models import AnonymousUser
|
from django.contrib.auth.models import AnonymousUser
|
||||||
from django.utils.functional import SimpleLazyObject, lazy
|
from django.utils.functional import SimpleLazyObject, lazy
|
||||||
from ninja.security import HttpBearer
|
from ninja.security import APIKeyHeader
|
||||||
|
|
||||||
from c3nav import settings
|
from c3nav import settings
|
||||||
from c3nav.api.exceptions import APIPermissionDenied, APITokenInvalid
|
from c3nav.api.exceptions import APIPermissionDenied, APIKeyInvalid
|
||||||
from c3nav.api.models import Secret
|
from c3nav.api.models import Secret
|
||||||
from c3nav.api.schema import APIErrorSchema
|
from c3nav.api.schema import APIErrorSchema
|
||||||
from c3nav.control.middleware import UserPermissionsMiddleware
|
from c3nav.control.middleware import UserPermissionsMiddleware
|
||||||
|
@ -31,16 +31,18 @@ class APIAuthDetails:
|
||||||
|
|
||||||
|
|
||||||
description = """
|
description = """
|
||||||
An API token can be acquired in 4 ways:
|
An API key can be acquired in 4 ways:
|
||||||
|
|
||||||
* Use `anonymous` for guest access.
|
* Use `anonymous` for guest access.
|
||||||
* Generate a session-bound temporary token using the auth session endpoint.
|
* Generate a session-bound temporary key using the auth session endpoint.
|
||||||
* Create an API secret in your user account settings.
|
* Create an API secret in your user account settings.
|
||||||
""".strip()
|
""".strip()
|
||||||
|
|
||||||
|
|
||||||
class APITokenAuth(HttpBearer):
|
class APIKeyAuth(APIKeyHeader):
|
||||||
openapi_name = "api token authentication"
|
param_name = "X-API-Key"
|
||||||
|
|
||||||
|
openapi_name = "api key authentication"
|
||||||
openapi_description = description
|
openapi_description = description
|
||||||
|
|
||||||
def __init__(self, logged_in=False, superuser=False, permissions: set[str] = None, is_readonly=False):
|
def __init__(self, logged_in=False, superuser=False, permissions: set[str] = None, is_readonly=False):
|
||||||
|
@ -52,32 +54,32 @@ class APITokenAuth(HttpBearer):
|
||||||
engine = import_module(settings.SESSION_ENGINE)
|
engine = import_module(settings.SESSION_ENGINE)
|
||||||
self.SessionStore = engine.SessionStore
|
self.SessionStore = engine.SessionStore
|
||||||
|
|
||||||
def _authenticate(self, request, token) -> APIAuthDetails:
|
def _authenticate(self, request, key) -> APIAuthDetails:
|
||||||
request.user = AnonymousUser()
|
request.user = AnonymousUser()
|
||||||
request.user_permissions = SimpleLazyObject(lambda: UserPermissionsMiddleware.get_user_permissions(request))
|
request.user_permissions = SimpleLazyObject(lambda: UserPermissionsMiddleware.get_user_permissions(request))
|
||||||
request.user_space_accesses = lazy(UserPermissionsMiddleware.get_user_space_accesses, dict)(request)
|
request.user_space_accesses = lazy(UserPermissionsMiddleware.get_user_space_accesses, dict)(request)
|
||||||
|
|
||||||
if token == "anonymous":
|
if key == "anonymous":
|
||||||
return APIAuthDetails(
|
return APIAuthDetails(
|
||||||
key_type=APIKeyType.ANONYMOUS,
|
key_type=APIKeyType.ANONYMOUS,
|
||||||
readonly=True,
|
readonly=True,
|
||||||
)
|
)
|
||||||
elif token.startswith("session:"):
|
elif key.startswith("session:"):
|
||||||
session = self.SessionStore(token.removeprefix("session:"))
|
session = self.SessionStore(key.removeprefix("session:"))
|
||||||
print('session is empty:', request.session.is_empty())
|
print('session is empty:', request.session.is_empty())
|
||||||
user = auth_get_user(FakeRequest(session=session))
|
user = auth_get_user(FakeRequest(session=session))
|
||||||
if not user.is_authenticated:
|
if not user.is_authenticated:
|
||||||
raise APITokenInvalid
|
raise APIKeyInvalid
|
||||||
request.user = user
|
request.user = user
|
||||||
return APIAuthDetails(
|
return APIAuthDetails(
|
||||||
key_type=APIKeyType.SESSION,
|
key_type=APIKeyType.SESSION,
|
||||||
readonly=False,
|
readonly=False,
|
||||||
)
|
)
|
||||||
elif token.startswith("secret:"):
|
elif key.startswith("secret:"):
|
||||||
try:
|
try:
|
||||||
secret = Secret.objects.get_by_secret(token.removeprefix("secret:")).get()
|
secret = Secret.objects.get_by_secret(key.removeprefix("secret:")).get()
|
||||||
except Secret.DoesNotExist:
|
except Secret.DoesNotExist:
|
||||||
raise APITokenInvalid
|
raise APIKeyInvalid
|
||||||
|
|
||||||
# get user permissions and restrict them based on scopes
|
# get user permissions and restrict them based on scopes
|
||||||
user_permissions: UserPermissions = UserPermissions.get_for_user(secret.user)
|
user_permissions: UserPermissions = UserPermissions.get_for_user(secret.user)
|
||||||
|
@ -95,10 +97,10 @@ class APITokenAuth(HttpBearer):
|
||||||
key_type=APIKeyType.SESSION,
|
key_type=APIKeyType.SESSION,
|
||||||
readonly=secret.readonly
|
readonly=secret.readonly
|
||||||
)
|
)
|
||||||
raise APITokenInvalid
|
raise APIKeyInvalid
|
||||||
|
|
||||||
def authenticate(self, request, token):
|
def authenticate(self, request, key):
|
||||||
auth_result = self._authenticate(request, token)
|
auth_result = self._authenticate(request, key)
|
||||||
if self.logged_in and not request.user.is_authenticated:
|
if self.logged_in and not request.user.is_authenticated:
|
||||||
raise APIPermissionDenied('You need to be signed in for this request.')
|
raise APIPermissionDenied('You need to be signed in for this request.')
|
||||||
if self.superuser and not request.user.is_superuser:
|
if self.superuser and not request.user.is_superuser:
|
||||||
|
|
|
@ -22,9 +22,9 @@ class APIUnauthorized(CustomAPIException):
|
||||||
detail = "Authorization is required for this endpoint."
|
detail = "Authorization is required for this endpoint."
|
||||||
|
|
||||||
|
|
||||||
class APITokenInvalid(CustomAPIException):
|
class APIKeyInvalid(CustomAPIException):
|
||||||
status_code = 401
|
status_code = 401
|
||||||
detail = "Invalid API token."
|
detail = "Invalid API key."
|
||||||
|
|
||||||
|
|
||||||
class APIPermissionDenied(CustomAPIException):
|
class APIPermissionDenied(CustomAPIException):
|
||||||
|
|
|
@ -3,7 +3,7 @@ from ninja.openapi.docs import DocsBase
|
||||||
from ninja.operation import Operation
|
from ninja.operation import Operation
|
||||||
from ninja.schema import NinjaGenerateJsonSchema
|
from ninja.schema import NinjaGenerateJsonSchema
|
||||||
|
|
||||||
from c3nav.api.auth import APITokenAuth
|
from c3nav.api.auth import APIKeyAuth
|
||||||
from c3nav.api.exceptions import CustomAPIException
|
from c3nav.api.exceptions import CustomAPIException
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ ninja_api = c3navAPI(
|
||||||
docs_url="/",
|
docs_url="/",
|
||||||
docs=SwaggerAndRedoc(),
|
docs=SwaggerAndRedoc(),
|
||||||
|
|
||||||
auth=APITokenAuth(),
|
auth=APIKeyAuth(),
|
||||||
|
|
||||||
openapi_extra={
|
openapi_extra={
|
||||||
"tags": [
|
"tags": [
|
||||||
|
|
|
@ -2,7 +2,7 @@ from django.urls import Resolver404, resolve
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from ninja import Router as APIRouter
|
from ninja import Router as APIRouter
|
||||||
|
|
||||||
from c3nav.api.auth import APITokenAuth, auth_permission_responses
|
from c3nav.api.auth import APIKeyAuth, auth_permission_responses
|
||||||
from c3nav.api.exceptions import API404
|
from c3nav.api.exceptions import API404
|
||||||
from c3nav.editor.api.base import api_etag_with_update_cache_key
|
from c3nav.editor.api.base import api_etag_with_update_cache_key
|
||||||
from c3nav.editor.api.geometries import get_level_geometries_result, get_space_geometries_result
|
from c3nav.editor.api.geometries import get_level_geometries_result, get_space_geometries_result
|
||||||
|
@ -12,7 +12,7 @@ from c3nav.mapdata.api.base import api_etag
|
||||||
from c3nav.mapdata.models import Source
|
from c3nav.mapdata.models import Source
|
||||||
from c3nav.mapdata.schemas.responses import WithBoundsSchema
|
from c3nav.mapdata.schemas.responses import WithBoundsSchema
|
||||||
|
|
||||||
editor_api_router = APIRouter(tags=["editor"], auth=APITokenAuth(permissions={"editor_access"}))
|
editor_api_router = APIRouter(tags=["editor"], auth=APIKeyAuth(permissions={"editor_access"}))
|
||||||
|
|
||||||
|
|
||||||
@editor_api_router.get('/bounds/', summary="boundaries",
|
@editor_api_router.get('/bounds/', summary="boundaries",
|
||||||
|
|
|
@ -9,13 +9,13 @@ from ninja import Schema, UploadedFile
|
||||||
from ninja.pagination import paginate
|
from ninja.pagination import paginate
|
||||||
from pydantic import PositiveInt, field_validator
|
from pydantic import PositiveInt, field_validator
|
||||||
|
|
||||||
from c3nav.api.auth import APITokenAuth, auth_permission_responses, auth_responses, validate_responses
|
from c3nav.api.auth import APIKeyAuth, auth_permission_responses, auth_responses, validate_responses
|
||||||
from c3nav.api.exceptions import API404, APIConflict, APIRequestValidationFailed
|
from c3nav.api.exceptions import API404, APIConflict, APIRequestValidationFailed
|
||||||
from c3nav.mesh.dataformats import BoardType, ChipType, FirmwareImage
|
from c3nav.mesh.dataformats import BoardType, ChipType, FirmwareImage
|
||||||
from c3nav.mesh.messages import MeshMessageType
|
from c3nav.mesh.messages import MeshMessageType
|
||||||
from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, NodeMessage
|
from c3nav.mesh.models import FirmwareBuild, FirmwareVersion, NodeMessage
|
||||||
|
|
||||||
mesh_api_router = APIRouter(tags=["mesh"], auth=APITokenAuth(permissions={"mesh_control"}))
|
mesh_api_router = APIRouter(tags=["mesh"], auth=APIKeyAuth(permissions={"mesh_control"}))
|
||||||
|
|
||||||
|
|
||||||
class FirmwareBuildSchema(Schema):
|
class FirmwareBuildSchema(Schema):
|
||||||
|
|
|
@ -9,7 +9,7 @@ from ninja import Router as APIRouter
|
||||||
from ninja import Schema
|
from ninja import Schema
|
||||||
from pydantic import PositiveInt
|
from pydantic import PositiveInt
|
||||||
|
|
||||||
from c3nav.api.auth import APITokenAuth, auth_responses, validate_responses
|
from c3nav.api.auth import APIKeyAuth, auth_responses, validate_responses
|
||||||
from c3nav.api.exceptions import APIRequestValidationFailed
|
from c3nav.api.exceptions import APIRequestValidationFailed
|
||||||
from c3nav.api.utils import NonEmptyStr
|
from c3nav.api.utils import NonEmptyStr
|
||||||
from c3nav.mapdata.api.base import api_stats_clean_location_value
|
from c3nav.mapdata.api.base import api_stats_clean_location_value
|
||||||
|
@ -171,7 +171,7 @@ def get_request_pk(location):
|
||||||
return location.slug if isinstance(location, Position) else location.pk
|
return location.slug if isinstance(location, Position) else location.pk
|
||||||
|
|
||||||
|
|
||||||
@routing_api_router.post('/route/', summary="query route", auth=APITokenAuth(is_readonly=True),
|
@routing_api_router.post('/route/', summary="query route", auth=APIKeyAuth(is_readonly=True),
|
||||||
description="query route between two locations",
|
description="query route between two locations",
|
||||||
response={200: RouteResponse | NoRouteResponse, **validate_responses, **auth_responses})
|
response={200: RouteResponse | NoRouteResponse, **validate_responses, **auth_responses})
|
||||||
# todo: route failure responses
|
# todo: route failure responses
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
(function () {
|
(function () {
|
||||||
|
|
||||||
class C3NavApi {
|
class C3NavApi {
|
||||||
token = 'anonymous';
|
key = 'anonymous';
|
||||||
|
|
||||||
constructor(base ) {
|
constructor(base ) {
|
||||||
this.base = base;
|
this.base = base;
|
||||||
|
@ -11,7 +11,7 @@
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
this.token = data.token
|
this.key = data.key
|
||||||
})
|
})
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
throw err;
|
throw err;
|
||||||
|
@ -36,7 +36,7 @@
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${this.token}`,
|
'X-API-Key': this.key,
|
||||||
'Accept': 'application/json'
|
'Accept': 'application/json'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -48,7 +48,7 @@
|
||||||
credentials: 'omit',
|
credentials: 'omit',
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Authorization': `Bearer ${this.token}`,
|
'Authorization': `Bearer ${this.key}`,
|
||||||
'Accept': 'application/json',
|
'Accept': 'application/json',
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue