diff --git a/passbook/admin/views/users.py b/passbook/admin/views/users.py index 11ce42733..9644853da 100644 --- a/passbook/admin/views/users.py +++ b/passbook/admin/views/users.py @@ -107,7 +107,9 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: """Create token for user and return link""" 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}) link = request.build_absolute_uri( reverse("passbook_flows:default-recovery") + f"?{querystring}" diff --git a/passbook/api/v2/urls.py b/passbook/api/v2/urls.py index bc23fff29..49503cced 100644 --- a/passbook/api/v2/urls.py +++ b/passbook/api/v2/urls.py @@ -12,6 +12,7 @@ from passbook.core.api.groups import GroupViewSet from passbook.core.api.propertymappings import PropertyMappingViewSet from passbook.core.api.providers import ProviderViewSet from passbook.core.api.sources import SourceViewSet +from passbook.core.api.tokens import TokenViewSet from passbook.core.api.users import UserViewSet from passbook.crypto.api import CertificateKeyPairViewSet from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet @@ -49,9 +50,12 @@ from passbook.stages.user_write.api import UserWriteStageViewSet router = routers.DefaultRouter() router.register("root/messages", MessagesViewSet, basename="messages") + router.register("core/applications", ApplicationViewSet) router.register("core/groups", GroupViewSet) router.register("core/users", UserViewSet) +router.register("core/tokens", TokenViewSet) + router.register("outposts/outposts", OutpostViewSet) router.register("outposts/proxy", OutpostConfigViewSet) diff --git a/passbook/core/api/tokens.py b/passbook/core/api/tokens.py new file mode 100644 index 000000000..5fde0f55d --- /dev/null +++ b/passbook/core/api/tokens.py @@ -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 diff --git a/passbook/core/migrations/0013_auto_20201003_2132.py b/passbook/core/migrations/0013_auto_20201003_2132.py new file mode 100644 index 000000000..6aa8ed732 --- /dev/null +++ b/passbook/core/migrations/0013_auto_20201003_2132.py @@ -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")}, + ), + ] diff --git a/passbook/core/models.py b/passbook/core/models.py index 02f5323c4..dae7a07f8 100644 --- a/passbook/core/models.py +++ b/passbook/core/models.py @@ -292,17 +292,20 @@ class ExpiringModel(models.Model): class TokenIntents(models.TextChoices): """Intents a Token can be created for.""" - # Single user token + # Single use token INTENT_VERIFICATION = "verification" # Allow access to API INTENT_API = "api" + INTENT_RECOVERY = "recovery" + class Token(ExpiringModel): """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) + identifier = models.TextField() intent = models.TextField( choices=TokenIntents.choices, default=TokenIntents.INTENT_VERIFICATION ) @@ -318,6 +321,7 @@ class Token(ExpiringModel): verbose_name = _("Token") verbose_name_plural = _("Tokens") + unique_together = (("identifier", "user"),) class PropertyMapping(models.Model): diff --git a/passbook/outposts/models.py b/passbook/outposts/models.py index 21a53bc02..afabdbcbf 100644 --- a/passbook/outposts/models.py +++ b/passbook/outposts/models.py @@ -148,6 +148,11 @@ class Outpost(models.Model): assign_perm(code_name, user, model) return user + @property + def token_identifier(self) -> str: + """Get Token identifier""" + return f"pb-outpost-{self.pk}-api" + @property def token(self) -> Token: """Get/create token for auto-generated user""" @@ -156,6 +161,7 @@ class Outpost(models.Model): return token.first() return Token.objects.create( user=self.user, + identifier=self.token_identifier, intent=TokenIntents.INTENT_API, description=f"Autogenerated by passbook for Outpost {self.name}", expiring=False, diff --git a/passbook/recovery/management/commands/create_recovery_key.py b/passbook/recovery/management/commands/create_recovery_key.py index 83791ff0b..ff2a6486a 100644 --- a/passbook/recovery/management/commands/create_recovery_key.py +++ b/passbook/recovery/management/commands/create_recovery_key.py @@ -8,7 +8,7 @@ from django.utils.timezone import now from django.utils.translation import gettext as _ 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 LOGGER = get_logger() @@ -47,6 +47,8 @@ class Command(BaseCommand): token = Token.objects.create( expires=expiry, user=user, + identifier="recovery", + intent=TokenIntents.INTENT_RECOVERY, description=f"Recovery Token generated by {getuser()} on {_now}", ) self.stdout.write( diff --git a/swagger.yaml b/swagger.yaml index ee748eabd..56483228f 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -343,6 +343,131 @@ paths: required: true type: string 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/: get: operationId: core_users_list @@ -5956,6 +6081,34 @@ definitions: attributes: title: Attributes 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: required: - username