core: Add Token identifier as sudo-primary key

This commit is contained in:
Jens Langhammer 2020-10-03 23:37:58 +02:00
parent b590589324
commit c5a6b4961f
8 changed files with 231 additions and 3 deletions

View File

@ -107,7 +107,9 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""Create token for user and return link""" """Create token for user and return link"""
super().get(request, *args, **kwargs) super().get(request, *args, **kwargs)
token = Token.objects.create(user=self.object) token, _ = Token.objects.get_or_create(
identifier="password-reset-temp", user=self.object
)
querystring = urlencode({"token": token.token_uuid}) querystring = urlencode({"token": token.token_uuid})
link = request.build_absolute_uri( link = request.build_absolute_uri(
reverse("passbook_flows:default-recovery") + f"?{querystring}" reverse("passbook_flows:default-recovery") + f"?{querystring}"

View File

@ -12,6 +12,7 @@ from passbook.core.api.groups import GroupViewSet
from passbook.core.api.propertymappings import PropertyMappingViewSet from passbook.core.api.propertymappings import PropertyMappingViewSet
from passbook.core.api.providers import ProviderViewSet from passbook.core.api.providers import ProviderViewSet
from passbook.core.api.sources import SourceViewSet from passbook.core.api.sources import SourceViewSet
from passbook.core.api.tokens import TokenViewSet
from passbook.core.api.users import UserViewSet from passbook.core.api.users import UserViewSet
from passbook.crypto.api import CertificateKeyPairViewSet from passbook.crypto.api import CertificateKeyPairViewSet
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
@ -49,9 +50,12 @@ from passbook.stages.user_write.api import UserWriteStageViewSet
router = routers.DefaultRouter() router = routers.DefaultRouter()
router.register("root/messages", MessagesViewSet, basename="messages") router.register("root/messages", MessagesViewSet, basename="messages")
router.register("core/applications", ApplicationViewSet) router.register("core/applications", ApplicationViewSet)
router.register("core/groups", GroupViewSet) router.register("core/groups", GroupViewSet)
router.register("core/users", UserViewSet) router.register("core/users", UserViewSet)
router.register("core/tokens", TokenViewSet)
router.register("outposts/outposts", OutpostViewSet) router.register("outposts/outposts", OutpostViewSet)
router.register("outposts/proxy", OutpostConfigViewSet) router.register("outposts/proxy", OutpostConfigViewSet)

View File

@ -0,0 +1,22 @@
"""Tokens API Viewset"""
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import ModelViewSet
from passbook.core.models import Token
class TokenSerializer(ModelSerializer):
"""Token Serializer"""
class Meta:
model = Token
fields = ["pk", "identifier", "intent", "user", "description"]
class TokenViewSet(ModelViewSet):
"""Token Viewset"""
queryset = Token.objects.all()
lookup_field = "identifier"
serializer_class = TokenSerializer

View File

@ -0,0 +1,35 @@
# Generated by Django 3.1.2 on 2020-10-03 21:32
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0012_auto_20201003_1737"),
]
operations = [
migrations.AddField(
model_name="token",
name="identifier",
field=models.TextField(default=""),
preserve_default=False,
),
migrations.AlterField(
model_name="token",
name="intent",
field=models.TextField(
choices=[
("verification", "Intent Verification"),
("api", "Intent Api"),
("recovery", "Intent Recovery"),
],
default="verification",
),
),
migrations.AlterUniqueTogether(
name="token",
unique_together={("identifier", "user")},
),
]

View File

@ -292,17 +292,20 @@ class ExpiringModel(models.Model):
class TokenIntents(models.TextChoices): class TokenIntents(models.TextChoices):
"""Intents a Token can be created for.""" """Intents a Token can be created for."""
# Single user token # Single use token
INTENT_VERIFICATION = "verification" INTENT_VERIFICATION = "verification"
# Allow access to API # Allow access to API
INTENT_API = "api" INTENT_API = "api"
INTENT_RECOVERY = "recovery"
class Token(ExpiringModel): class Token(ExpiringModel):
"""Token used to authenticate the User for API Access or confirm another Stage like Email.""" """Token used to authenticate the User for API Access or confirm another Stage like Email."""
token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4) token_uuid = models.UUIDField(primary_key=True, editable=False, default=uuid4)
identifier = models.TextField()
intent = models.TextField( intent = models.TextField(
choices=TokenIntents.choices, default=TokenIntents.INTENT_VERIFICATION choices=TokenIntents.choices, default=TokenIntents.INTENT_VERIFICATION
) )
@ -318,6 +321,7 @@ class Token(ExpiringModel):
verbose_name = _("Token") verbose_name = _("Token")
verbose_name_plural = _("Tokens") verbose_name_plural = _("Tokens")
unique_together = (("identifier", "user"),)
class PropertyMapping(models.Model): class PropertyMapping(models.Model):

View File

@ -148,6 +148,11 @@ class Outpost(models.Model):
assign_perm(code_name, user, model) assign_perm(code_name, user, model)
return user return user
@property
def token_identifier(self) -> str:
"""Get Token identifier"""
return f"pb-outpost-{self.pk}-api"
@property @property
def token(self) -> Token: def token(self) -> Token:
"""Get/create token for auto-generated user""" """Get/create token for auto-generated user"""
@ -156,6 +161,7 @@ class Outpost(models.Model):
return token.first() return token.first()
return Token.objects.create( return Token.objects.create(
user=self.user, user=self.user,
identifier=self.token_identifier,
intent=TokenIntents.INTENT_API, intent=TokenIntents.INTENT_API,
description=f"Autogenerated by passbook for Outpost {self.name}", description=f"Autogenerated by passbook for Outpost {self.name}",
expiring=False, expiring=False,

View File

@ -8,7 +8,7 @@ from django.utils.timezone import now
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from structlog import get_logger from structlog import get_logger
from passbook.core.models import Token, User from passbook.core.models import Token, TokenIntents, User
from passbook.lib.config import CONFIG from passbook.lib.config import CONFIG
LOGGER = get_logger() LOGGER = get_logger()
@ -47,6 +47,8 @@ class Command(BaseCommand):
token = Token.objects.create( token = Token.objects.create(
expires=expiry, expires=expiry,
user=user, user=user,
identifier="recovery",
intent=TokenIntents.INTENT_RECOVERY,
description=f"Recovery Token generated by {getuser()} on {_now}", description=f"Recovery Token generated by {getuser()} on {_now}",
) )
self.stdout.write( self.stdout.write(

View File

@ -343,6 +343,131 @@ paths:
required: true required: true
type: string type: string
format: uuid format: uuid
/core/tokens/:
get:
operationId: core_tokens_list
description: Token Viewset
parameters:
- name: ordering
in: query
description: Which field to use when ordering the results.
required: false
type: string
- name: search
in: query
description: A search term.
required: false
type: string
- name: limit
in: query
description: Number of results to return per page.
required: false
type: integer
- name: offset
in: query
description: The initial index from which to return the results.
required: false
type: integer
responses:
'200':
description: ''
schema:
required:
- count
- results
type: object
properties:
count:
type: integer
next:
type: string
format: uri
x-nullable: true
previous:
type: string
format: uri
x-nullable: true
results:
type: array
items:
$ref: '#/definitions/Token'
tags:
- core
post:
operationId: core_tokens_create
description: Token Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Token'
responses:
'201':
description: ''
schema:
$ref: '#/definitions/Token'
tags:
- core
parameters: []
/core/tokens/{identifier}/:
get:
operationId: core_tokens_read
description: Token Viewset
parameters: []
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Token'
tags:
- core
put:
operationId: core_tokens_update
description: Token Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Token'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Token'
tags:
- core
patch:
operationId: core_tokens_partial_update
description: Token Viewset
parameters:
- name: data
in: body
required: true
schema:
$ref: '#/definitions/Token'
responses:
'200':
description: ''
schema:
$ref: '#/definitions/Token'
tags:
- core
delete:
operationId: core_tokens_delete
description: Token Viewset
parameters: []
responses:
'204':
description: ''
tags:
- core
parameters:
- name: identifier
in: path
required: true
type: string
/core/users/: /core/users/:
get: get:
operationId: core_users_list operationId: core_users_list
@ -5956,6 +6081,34 @@ definitions:
attributes: attributes:
title: Attributes title: Attributes
type: string type: string
Token:
required:
- identifier
- user
type: object
properties:
pk:
title: Token uuid
type: string
format: uuid
readOnly: true
identifier:
title: Identifier
type: string
minLength: 1
intent:
title: Intent
type: string
enum:
- verification
- api
- recovery
user:
title: User
type: integer
description:
title: Description
type: string
User: User:
required: required:
- username - username