commit
5596caedbc
|
@ -5,8 +5,9 @@ from django.views.generic import TemplateView
|
||||||
|
|
||||||
from passbook import __version__
|
from passbook import __version__
|
||||||
from passbook.admin.mixins import AdminRequiredMixin
|
from passbook.admin.mixins import AdminRequiredMixin
|
||||||
from passbook.core.models import Application, Policy, Provider, Source, User
|
from passbook.core.models import Application, Provider, Source, User
|
||||||
from passbook.flows.models import Flow, Stage
|
from passbook.flows.models import Flow, Stage
|
||||||
|
from passbook.policies.models import Policy
|
||||||
from passbook.root.celery import CELERY_APP
|
from passbook.root.celery import CELERY_APP
|
||||||
from passbook.stages.invitation.models import Invitation
|
from passbook.stages.invitation.models import Invitation
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,10 @@ from django.views.generic.detail import DetailView
|
||||||
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
from guardian.mixins import PermissionListMixin, PermissionRequiredMixin
|
||||||
|
|
||||||
from passbook.admin.forms.policies import PolicyTestForm
|
from passbook.admin.forms.policies import PolicyTestForm
|
||||||
from passbook.core.models import Policy
|
|
||||||
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
from passbook.lib.utils.reflection import all_subclasses, path_to_class
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
from passbook.policies.engine import PolicyEngine
|
from passbook.policies.engine import PolicyEngine
|
||||||
|
from passbook.policies.models import Policy
|
||||||
|
|
||||||
|
|
||||||
class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
class PolicyListView(LoginRequiredMixin, PermissionListMixin, ListView):
|
||||||
|
|
|
@ -16,7 +16,7 @@ from guardian.mixins import (
|
||||||
)
|
)
|
||||||
|
|
||||||
from passbook.admin.forms.users import UserForm
|
from passbook.admin.forms.users import UserForm
|
||||||
from passbook.core.models import Nonce, User
|
from passbook.core.models import Token, User
|
||||||
from passbook.lib.views import CreateAssignPermView
|
from passbook.lib.views import CreateAssignPermView
|
||||||
|
|
||||||
|
|
||||||
|
@ -92,12 +92,12 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
|
||||||
permission_required = "passbook_core.reset_user_password"
|
permission_required = "passbook_core.reset_user_password"
|
||||||
|
|
||||||
def get(self, request, *args, **kwargs):
|
def get(self, request, *args, **kwargs):
|
||||||
"""Create nonce for user and return link"""
|
"""Create token for user and return link"""
|
||||||
super().get(request, *args, **kwargs)
|
super().get(request, *args, **kwargs)
|
||||||
# TODO: create plan for user, get token
|
# TODO: create plan for user, get token
|
||||||
nonce = Nonce.objects.create(user=self.object)
|
token = Token.objects.create(user=self.object)
|
||||||
link = request.build_absolute_uri(
|
link = request.build_absolute_uri(
|
||||||
reverse("passbook_flows:default-recovery", kwargs={"nonce": nonce.uuid})
|
reverse("passbook_flows:default-recovery", kwargs={"token": token.uuid})
|
||||||
)
|
)
|
||||||
messages.success(
|
messages.success(
|
||||||
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})
|
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
"""permission classes for django restframework"""
|
"""permission classes for django restframework"""
|
||||||
from rest_framework.permissions import BasePermission, DjangoObjectPermissions
|
from rest_framework.permissions import BasePermission, DjangoObjectPermissions
|
||||||
|
|
||||||
from passbook.core.models import PolicyModel
|
|
||||||
from passbook.policies.engine import PolicyEngine
|
from passbook.policies.engine import PolicyEngine
|
||||||
|
from passbook.policies.models import PolicyBindingModel
|
||||||
|
|
||||||
|
|
||||||
class CustomObjectPermissions(DjangoObjectPermissions):
|
class CustomObjectPermissions(DjangoObjectPermissions):
|
||||||
|
@ -24,8 +24,7 @@ class PolicyPermissions(BasePermission):
|
||||||
|
|
||||||
policy_engine: PolicyEngine
|
policy_engine: PolicyEngine
|
||||||
|
|
||||||
def has_object_permission(self, request, view, obj: PolicyModel) -> bool:
|
def has_object_permission(self, request, view, obj: PolicyBindingModel) -> bool:
|
||||||
# if not obj.po
|
|
||||||
self.policy_engine = PolicyEngine(obj.policies, request.user, request)
|
self.policy_engine = PolicyEngine(obj.policies, request.user, request)
|
||||||
self.policy_engine.request.obj = obj
|
self.policy_engine.request.obj = obj
|
||||||
return self.policy_engine.build().passing
|
return self.policy_engine.build().passing
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
"""api v2 urls"""
|
"""api v2 urls"""
|
||||||
from django.conf import settings
|
|
||||||
from django.conf.urls import url
|
from django.conf.urls import url
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
from drf_yasg import openapi
|
from drf_yasg import openapi
|
||||||
|
@ -19,6 +18,7 @@ from passbook.core.api.users import UserViewSet
|
||||||
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
|
from passbook.flows.api import FlowStageBindingViewSet, FlowViewSet, StageViewSet
|
||||||
from passbook.lib.utils.reflection import get_apps
|
from passbook.lib.utils.reflection import get_apps
|
||||||
from passbook.policies.api import PolicyBindingViewSet
|
from passbook.policies.api import PolicyBindingViewSet
|
||||||
|
from passbook.policies.dummy.api import DummyPolicyViewSet
|
||||||
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
|
from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet
|
||||||
from passbook.policies.expression.api import ExpressionPolicyViewSet
|
from passbook.policies.expression.api import ExpressionPolicyViewSet
|
||||||
from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
|
from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet
|
||||||
|
@ -31,6 +31,7 @@ from passbook.providers.saml.api import SAMLPropertyMappingViewSet, SAMLProvider
|
||||||
from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
from passbook.sources.ldap.api import LDAPPropertyMappingViewSet, LDAPSourceViewSet
|
||||||
from passbook.sources.oauth.api import OAuthSourceViewSet
|
from passbook.sources.oauth.api import OAuthSourceViewSet
|
||||||
from passbook.stages.captcha.api import CaptchaStageViewSet
|
from passbook.stages.captcha.api import CaptchaStageViewSet
|
||||||
|
from passbook.stages.dummy.api import DummyStageViewSet
|
||||||
from passbook.stages.email.api import EmailStageViewSet
|
from passbook.stages.email.api import EmailStageViewSet
|
||||||
from passbook.stages.identification.api import IdentificationStageViewSet
|
from passbook.stages.identification.api import IdentificationStageViewSet
|
||||||
from passbook.stages.invitation.api import InvitationStageViewSet, InvitationViewSet
|
from passbook.stages.invitation.api import InvitationStageViewSet, InvitationViewSet
|
||||||
|
@ -97,10 +98,6 @@ router.register("stages/user_write", UserWriteStageViewSet)
|
||||||
router.register("flows/instances", FlowViewSet)
|
router.register("flows/instances", FlowViewSet)
|
||||||
router.register("flows/bindings", FlowStageBindingViewSet)
|
router.register("flows/bindings", FlowStageBindingViewSet)
|
||||||
|
|
||||||
if settings.DEBUG:
|
|
||||||
from passbook.stages.dummy.api import DummyStageViewSet
|
|
||||||
from passbook.policies.dummy.api import DummyPolicyViewSet
|
|
||||||
|
|
||||||
router.register("stages/dummy", DummyStageViewSet)
|
router.register("stages/dummy", DummyStageViewSet)
|
||||||
router.register("policies/dummy", DummyPolicyViewSet)
|
router.register("policies/dummy", DummyPolicyViewSet)
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.test import TestCase
|
||||||
from guardian.shortcuts import get_anonymous_user
|
from guardian.shortcuts import get_anonymous_user
|
||||||
|
|
||||||
from passbook.audit.models import Event, EventAction
|
from passbook.audit.models import Event, EventAction
|
||||||
from passbook.core.models import Policy
|
from passbook.policies.dummy.models import DummyPolicy
|
||||||
|
|
||||||
|
|
||||||
class TestAuditEvent(TestCase):
|
class TestAuditEvent(TestCase):
|
||||||
|
@ -23,7 +23,7 @@ class TestAuditEvent(TestCase):
|
||||||
|
|
||||||
def test_new_with_uuid_model(self):
|
def test_new_with_uuid_model(self):
|
||||||
"""Create a new Event passing a model (with UUID PK) as kwarg"""
|
"""Create a new Event passing a model (with UUID PK) as kwarg"""
|
||||||
temp_model = Policy.objects.create()
|
temp_model = DummyPolicy.objects.create(name="test", result=True)
|
||||||
event = Event.new(EventAction.CUSTOM, model=temp_model)
|
event = Event.new(EventAction.CUSTOM, model=temp_model)
|
||||||
event.save() # We save to ensure nothing is un-saveable
|
event.save() # We save to ensure nothing is un-saveable
|
||||||
model_content_type = ContentType.objects.get_for_model(temp_model)
|
model_content_type = ContentType.objects.get_for_model(temp_model)
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
from rest_framework.serializers import ModelSerializer, SerializerMethodField
|
||||||
from rest_framework.viewsets import ReadOnlyModelViewSet
|
from rest_framework.viewsets import ReadOnlyModelViewSet
|
||||||
|
|
||||||
from passbook.core.models import Policy
|
|
||||||
from passbook.policies.forms import GENERAL_FIELDS
|
from passbook.policies.forms import GENERAL_FIELDS
|
||||||
|
from passbook.policies.models import Policy
|
||||||
|
|
||||||
|
|
||||||
class PolicySerializer(ModelSerializer):
|
class PolicySerializer(ModelSerializer):
|
||||||
|
|
|
@ -19,6 +19,7 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("auth", "0011_update_proxy_permissions"),
|
("auth", "0011_update_proxy_permissions"),
|
||||||
|
("passbook_policies", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -158,7 +159,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"policies",
|
"policies",
|
||||||
models.ManyToManyField(blank=True, to="passbook_core.Policy"),
|
models.ManyToManyField(blank=True, to="passbook_policies.Policy"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
options={"abstract": False,},
|
options={"abstract": False,},
|
||||||
|
@ -182,30 +183,6 @@ class Migration(migrations.Migration):
|
||||||
"verbose_name_plural": "Property Mappings",
|
"verbose_name_plural": "Property Mappings",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
|
||||||
name="DebugPolicy",
|
|
||||||
fields=[
|
|
||||||
(
|
|
||||||
"policy_ptr",
|
|
||||||
models.OneToOneField(
|
|
||||||
auto_created=True,
|
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
|
||||||
parent_link=True,
|
|
||||||
primary_key=True,
|
|
||||||
serialize=False,
|
|
||||||
to="passbook_core.Policy",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
("result", models.BooleanField(default=False)),
|
|
||||||
("wait_min", models.IntegerField(default=5)),
|
|
||||||
("wait_max", models.IntegerField(default=30)),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
"verbose_name": "Debug Policy",
|
|
||||||
"verbose_name_plural": "Debug Policies",
|
|
||||||
},
|
|
||||||
bases=("passbook_core.policy",),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Factor",
|
name="Factor",
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -217,7 +194,7 @@ class Migration(migrations.Migration):
|
||||||
parent_link=True,
|
parent_link=True,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
serialize=False,
|
serialize=False,
|
||||||
to="passbook_core.PolicyModel",
|
to="passbook_policies.PolicyBindingModel",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("name", models.TextField()),
|
("name", models.TextField()),
|
||||||
|
@ -226,7 +203,7 @@ class Migration(migrations.Migration):
|
||||||
("enabled", models.BooleanField(default=True)),
|
("enabled", models.BooleanField(default=True)),
|
||||||
],
|
],
|
||||||
options={"abstract": False,},
|
options={"abstract": False,},
|
||||||
bases=("passbook_core.policymodel",),
|
bases=("passbook_policies.policybindingmodel",),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Source",
|
name="Source",
|
||||||
|
@ -239,7 +216,7 @@ class Migration(migrations.Migration):
|
||||||
parent_link=True,
|
parent_link=True,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
serialize=False,
|
serialize=False,
|
||||||
to="passbook_core.PolicyModel",
|
to="passbook_policies.PolicyBindingModel",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("name", models.TextField()),
|
("name", models.TextField()),
|
||||||
|
@ -247,7 +224,7 @@ class Migration(migrations.Migration):
|
||||||
("enabled", models.BooleanField(default=True)),
|
("enabled", models.BooleanField(default=True)),
|
||||||
],
|
],
|
||||||
options={"abstract": False,},
|
options={"abstract": False,},
|
||||||
bases=("passbook_core.policymodel",),
|
bases=("passbook_policies.policybindingmodel",),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="Provider",
|
name="Provider",
|
||||||
|
@ -284,7 +261,7 @@ class Migration(migrations.Migration):
|
||||||
(
|
(
|
||||||
"expires",
|
"expires",
|
||||||
models.DateTimeField(
|
models.DateTimeField(
|
||||||
default=passbook.core.models.default_nonce_duration
|
default=passbook.core.models.default_token_duration
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("expiring", models.BooleanField(default=True)),
|
("expiring", models.BooleanField(default=True)),
|
||||||
|
@ -418,7 +395,7 @@ class Migration(migrations.Migration):
|
||||||
parent_link=True,
|
parent_link=True,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
serialize=False,
|
serialize=False,
|
||||||
to="passbook_core.PolicyModel",
|
to="passbook_policies.PolicyBindingModel",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("name", models.TextField()),
|
("name", models.TextField()),
|
||||||
|
@ -438,7 +415,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
options={"abstract": False,},
|
options={"abstract": False,},
|
||||||
bases=("passbook_core.policymodel",),
|
bases=("passbook_policies.policybindingmodel",),
|
||||||
),
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name="user",
|
model_name="user",
|
||||||
|
|
|
@ -6,11 +6,7 @@ from django.db import migrations
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_policies", "0003_auto_20200508_1642"),
|
|
||||||
("passbook_stages_password", "0001_initial"),
|
|
||||||
("passbook_core", "0012_delete_factor"),
|
("passbook_core", "0012_delete_factor"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = []
|
||||||
migrations.DeleteModel(name="DebugPolicy",),
|
|
||||||
]
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-05-16 14:07
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
import passbook.core.models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_core", "0014_delete_invitation"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Token",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"expires",
|
||||||
|
models.DateTimeField(
|
||||||
|
default=passbook.core.models.default_token_duration
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("expiring", models.BooleanField(default=True)),
|
||||||
|
("description", models.TextField(blank=True, default="")),
|
||||||
|
(
|
||||||
|
"user",
|
||||||
|
models.ForeignKey(
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="+",
|
||||||
|
to=settings.AUTH_USER_MODEL,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
options={"verbose_name": "Token", "verbose_name_plural": "Tokens",},
|
||||||
|
bases=(models.Model,),
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(name="Nonce",),
|
||||||
|
]
|
|
@ -0,0 +1,48 @@
|
||||||
|
# Generated by Django 3.0.5 on 2020-05-16 14:46
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("passbook_policies", "__first__"),
|
||||||
|
("passbook_core", "0015_auto_20200516_1407"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(model_name="policymodel", name="policies",),
|
||||||
|
migrations.RemoveField(model_name="application", name="policymodel_ptr",),
|
||||||
|
migrations.RemoveField(model_name="source", name="policymodel_ptr",),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="application",
|
||||||
|
name="policybindingmodel_ptr",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
default=None,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="passbook_policies.PolicyBindingModel",
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="source",
|
||||||
|
name="policybindingmodel_ptr",
|
||||||
|
field=models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
default=None,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="passbook_policies.PolicyBindingModel",
|
||||||
|
),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.DeleteModel(name="Policy",),
|
||||||
|
migrations.DeleteModel(name="PolicyModel",),
|
||||||
|
]
|
|
@ -22,15 +22,14 @@ from passbook.core.exceptions import PropertyMappingExpressionException
|
||||||
from passbook.core.signals import password_changed
|
from passbook.core.signals import password_changed
|
||||||
from passbook.core.types import UILoginButton, UIUserSettings
|
from passbook.core.types import UILoginButton, UIUserSettings
|
||||||
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
|
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
|
||||||
from passbook.policies.exceptions import PolicyException
|
from passbook.policies.models import PolicyBindingModel
|
||||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
NATIVE_ENVIRONMENT = NativeEnvironment()
|
NATIVE_ENVIRONMENT = NativeEnvironment()
|
||||||
|
|
||||||
|
|
||||||
def default_nonce_duration():
|
def default_token_duration():
|
||||||
"""Default duration a Nonce is valid"""
|
"""Default duration a Token is valid"""
|
||||||
return now() + timedelta(minutes=30)
|
return now() + timedelta(minutes=30)
|
||||||
|
|
||||||
|
|
||||||
|
@ -94,13 +93,7 @@ class Provider(ExportModelOperationsMixin("provider"), models.Model):
|
||||||
return super().__str__()
|
return super().__str__()
|
||||||
|
|
||||||
|
|
||||||
class PolicyModel(UUIDModel, CreatedUpdatedModel):
|
class Application(ExportModelOperationsMixin("application"), PolicyBindingModel):
|
||||||
"""Base model which can have policies applied to it"""
|
|
||||||
|
|
||||||
policies = models.ManyToManyField("Policy", blank=True)
|
|
||||||
|
|
||||||
|
|
||||||
class Application(ExportModelOperationsMixin("application"), PolicyModel):
|
|
||||||
"""Every Application which uses passbook for authentication/identification/authorization
|
"""Every Application which uses passbook for authentication/identification/authorization
|
||||||
needs an Application record. Other authentication types can subclass this Model to
|
needs an Application record. Other authentication types can subclass this Model to
|
||||||
add custom fields and other properties"""
|
add custom fields and other properties"""
|
||||||
|
@ -129,7 +122,7 @@ class Application(ExportModelOperationsMixin("application"), PolicyModel):
|
||||||
return self.name
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
class Source(ExportModelOperationsMixin("source"), PolicyModel):
|
class Source(ExportModelOperationsMixin("source"), PolicyBindingModel):
|
||||||
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
"""Base Authentication source, i.e. an OAuth Provider, SAML Remote or LDAP Server"""
|
||||||
|
|
||||||
name = models.TextField(help_text=_("Source's display Name."))
|
name = models.TextField(help_text=_("Source's display Name."))
|
||||||
|
@ -176,45 +169,26 @@ class UserSourceConnection(CreatedUpdatedModel):
|
||||||
unique_together = (("user", "source"),)
|
unique_together = (("user", "source"),)
|
||||||
|
|
||||||
|
|
||||||
class Policy(ExportModelOperationsMixin("policy"), UUIDModel, CreatedUpdatedModel):
|
class Token(ExportModelOperationsMixin("token"), UUIDModel):
|
||||||
"""Policies which specify if a user is authorized to use an Application. Can be overridden by
|
|
||||||
other types to add other fields, more logic, etc."""
|
|
||||||
|
|
||||||
name = models.TextField(blank=True, null=True)
|
|
||||||
negate = models.BooleanField(default=False)
|
|
||||||
order = models.IntegerField(default=0)
|
|
||||||
timeout = models.IntegerField(default=30)
|
|
||||||
|
|
||||||
objects = InheritanceManager()
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"Policy {self.name}"
|
|
||||||
|
|
||||||
def passes(self, request: PolicyRequest) -> PolicyResult:
|
|
||||||
"""Check if user instance passes this policy"""
|
|
||||||
raise PolicyException()
|
|
||||||
|
|
||||||
|
|
||||||
class Nonce(ExportModelOperationsMixin("nonce"), UUIDModel):
|
|
||||||
"""One-time link for password resets/sign-up-confirmations"""
|
"""One-time link for password resets/sign-up-confirmations"""
|
||||||
|
|
||||||
expires = models.DateTimeField(default=default_nonce_duration)
|
expires = models.DateTimeField(default=default_token_duration)
|
||||||
user = models.ForeignKey("User", on_delete=models.CASCADE)
|
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+")
|
||||||
expiring = models.BooleanField(default=True)
|
expiring = models.BooleanField(default=True)
|
||||||
description = models.TextField(default="", blank=True)
|
description = models.TextField(default="", blank=True)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_expired(self) -> bool:
|
def is_expired(self) -> bool:
|
||||||
"""Check if nonce is expired yet."""
|
"""Check if token is expired yet."""
|
||||||
return now() > self.expires
|
return now() > self.expires
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Nonce f{self.uuid.hex} {self.description} (expires={self.expires})"
|
return f"Token f{self.uuid.hex} {self.description} (expires={self.expires})"
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
|
||||||
verbose_name = _("Nonce")
|
verbose_name = _("Token")
|
||||||
verbose_name_plural = _("Nonces")
|
verbose_name_plural = _("Tokens")
|
||||||
|
|
||||||
|
|
||||||
class PropertyMapping(UUIDModel):
|
class PropertyMapping(UUIDModel):
|
||||||
|
|
|
@ -17,7 +17,7 @@ password_changed = Signal(providing_args=["user", "password"])
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
def invalidate_policy_cache(sender, instance, **_):
|
def invalidate_policy_cache(sender, instance, **_):
|
||||||
"""Invalidate Policy cache when policy is updated"""
|
"""Invalidate Policy cache when policy is updated"""
|
||||||
from passbook.core.models import Policy
|
from passbook.policies.models import Policy
|
||||||
from passbook.policies.process import cache_key
|
from passbook.policies.process import cache_key
|
||||||
|
|
||||||
if isinstance(instance, Policy):
|
if isinstance(instance, Policy):
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
from django.utils.timezone import now
|
from django.utils.timezone import now
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import Nonce
|
from passbook.core.models import Token
|
||||||
from passbook.root.celery import CELERY_APP
|
from passbook.root.celery import CELERY_APP
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
@CELERY_APP.task()
|
@CELERY_APP.task()
|
||||||
def clean_nonces():
|
def clean_tokens():
|
||||||
"""Remove expired nonces"""
|
"""Remove expired tokens"""
|
||||||
amount, _ = Nonce.objects.filter(expires__lt=now(), expiring=True).delete()
|
amount, _ = Token.objects.filter(expires__lt=now(), expiring=True).delete()
|
||||||
LOGGER.debug("Deleted expired nonces", amount=amount)
|
LOGGER.debug("Deleted expired tokens", amount=amount)
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_policies", "0003_auto_20200508_1642"),
|
# ("passbook_policies", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
|
@ -9,8 +9,7 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_policies", "0003_auto_20200508_1642"),
|
("passbook_policies", "0001_initial"),
|
||||||
("passbook_core", "0013_delete_debugpolicy"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -25,7 +24,7 @@ class Migration(migrations.Migration):
|
||||||
parent_link=True,
|
parent_link=True,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
serialize=False,
|
serialize=False,
|
||||||
to="passbook_core.Policy",
|
to="passbook_policies.Policy",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("result", models.BooleanField(default=False)),
|
("result", models.BooleanField(default=False)),
|
||||||
|
@ -36,6 +35,6 @@ class Migration(migrations.Migration):
|
||||||
"verbose_name": "Dummy Policy",
|
"verbose_name": "Dummy Policy",
|
||||||
"verbose_name_plural": "Dummy Policies",
|
"verbose_name_plural": "Dummy Policies",
|
||||||
},
|
},
|
||||||
bases=("passbook_core.policy",),
|
bases=("passbook_policies.policy",),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,7 +6,7 @@ from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import Policy
|
from passbook.policies.models import Policy
|
||||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
|
@ -7,7 +7,8 @@ from django.core.cache import cache
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import Policy, User
|
from passbook.core.models import User
|
||||||
|
from passbook.policies.models import Policy
|
||||||
from passbook.policies.process import PolicyProcess, cache_key
|
from passbook.policies.process import PolicyProcess, cache_key
|
||||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_core", "0001_initial"),
|
("passbook_policies", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -24,7 +24,7 @@ class Migration(migrations.Migration):
|
||||||
parent_link=True,
|
parent_link=True,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
serialize=False,
|
serialize=False,
|
||||||
to="passbook_core.Policy",
|
to="passbook_policies.Policy",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("deny_only", models.BooleanField(default=False)),
|
("deny_only", models.BooleanField(default=False)),
|
||||||
|
@ -34,6 +34,6 @@ class Migration(migrations.Migration):
|
||||||
"verbose_name": "Password Expiry Policy",
|
"verbose_name": "Password Expiry Policy",
|
||||||
"verbose_name_plural": "Password Expiry Policies",
|
"verbose_name_plural": "Password Expiry Policies",
|
||||||
},
|
},
|
||||||
bases=("passbook_core.policy",),
|
bases=("passbook_policies.policy",),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,7 +6,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 Policy
|
from passbook.policies.models import Policy
|
||||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_core", "0007_auto_20200217_1934"),
|
("passbook_policies", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -24,7 +24,7 @@ class Migration(migrations.Migration):
|
||||||
parent_link=True,
|
parent_link=True,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
serialize=False,
|
serialize=False,
|
||||||
to="passbook_core.Policy",
|
to="passbook_policies.Policy",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("expression", models.TextField()),
|
("expression", models.TextField()),
|
||||||
|
@ -33,6 +33,6 @@ class Migration(migrations.Migration):
|
||||||
"verbose_name": "Expression Policy",
|
"verbose_name": "Expression Policy",
|
||||||
"verbose_name_plural": "Expression Policies",
|
"verbose_name_plural": "Expression Policies",
|
||||||
},
|
},
|
||||||
bases=("passbook_core.policy",),
|
bases=("passbook_policies.policy",),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from passbook.core.models import Policy
|
|
||||||
from passbook.policies.expression.evaluator import Evaluator
|
from passbook.policies.expression.evaluator import Evaluator
|
||||||
|
from passbook.policies.models import Policy
|
||||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_core", "0001_initial"),
|
("passbook_policies", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -24,7 +24,7 @@ class Migration(migrations.Migration):
|
||||||
parent_link=True,
|
parent_link=True,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
serialize=False,
|
serialize=False,
|
||||||
to="passbook_core.Policy",
|
to="passbook_policies.Policy",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("allowed_count", models.IntegerField(default=0)),
|
("allowed_count", models.IntegerField(default=0)),
|
||||||
|
@ -33,6 +33,6 @@ class Migration(migrations.Migration):
|
||||||
"verbose_name": "Have I Been Pwned Policy",
|
"verbose_name": "Have I Been Pwned Policy",
|
||||||
"verbose_name_plural": "Have I Been Pwned Policies",
|
"verbose_name_plural": "Have I Been Pwned Policies",
|
||||||
},
|
},
|
||||||
bases=("passbook_core.policy",),
|
bases=("passbook_policies.policy",),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,7 +6,8 @@ from django.utils.translation import gettext as _
|
||||||
from requests import get
|
from requests import get
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import Policy, PolicyResult, User
|
from passbook.core.models import User
|
||||||
|
from passbook.policies.models import Policy, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
|
@ -10,11 +10,28 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
("passbook_core", "0011_auto_20200222_1822"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="Policy",
|
||||||
|
fields=[
|
||||||
|
("created", models.DateTimeField(auto_now_add=True)),
|
||||||
|
("last_updated", models.DateTimeField(auto_now=True)),
|
||||||
|
(
|
||||||
|
"uuid",
|
||||||
|
models.UUIDField(
|
||||||
|
default=uuid.uuid4,
|
||||||
|
editable=False,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("name", models.TextField(blank=True, null=True)),
|
||||||
|
("negate", models.BooleanField(default=False)),
|
||||||
|
("order", models.IntegerField(default=0)),
|
||||||
|
("timeout", models.IntegerField(default=30)),
|
||||||
|
],
|
||||||
|
options={"abstract": False,},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="PolicyBinding",
|
name="PolicyBinding",
|
||||||
fields=[
|
fields=[
|
||||||
|
@ -34,7 +51,7 @@ class Migration(migrations.Migration):
|
||||||
models.ForeignKey(
|
models.ForeignKey(
|
||||||
on_delete=django.db.models.deletion.CASCADE,
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
related_name="+",
|
related_name="+",
|
||||||
to="passbook_core.Policy",
|
to="passbook_policies.Policy",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
@ -60,7 +77,7 @@ class Migration(migrations.Migration):
|
||||||
models.ManyToManyField(
|
models.ManyToManyField(
|
||||||
related_name="_policybindingmodel_policies_+",
|
related_name="_policybindingmodel_policies_+",
|
||||||
through="passbook_policies.PolicyBinding",
|
through="passbook_policies.PolicyBinding",
|
||||||
to="passbook_core.Policy",
|
to="passbook_policies.Policy",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Generated by Django 3.0.3 on 2020-05-08 16:42
|
# Generated by Django 3.0.5 on 2020-05-16 15:16
|
||||||
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
@ -6,7 +6,6 @@ from django.db import migrations, models
|
||||||
class Migration(migrations.Migration):
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_core", "0011_auto_20200222_1822"),
|
|
||||||
("passbook_policies", "0002_auto_20200508_1230"),
|
("passbook_policies", "0002_auto_20200508_1230"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -18,7 +17,7 @@ class Migration(migrations.Migration):
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name="_policybindingmodel_policies_+",
|
related_name="_policybindingmodel_policies_+",
|
||||||
through="passbook_policies.PolicyBinding",
|
through="passbook_policies.PolicyBinding",
|
||||||
to="passbook_core.Policy",
|
to="passbook_policies.Policy",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]
|
]
|
|
@ -1,16 +1,18 @@
|
||||||
"""Policy base models"""
|
"""Policy base models"""
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from model_utils.managers import InheritanceManager
|
||||||
|
|
||||||
from passbook.core.models import Policy
|
from passbook.lib.models import CreatedUpdatedModel, UUIDModel
|
||||||
from passbook.lib.models import UUIDModel
|
from passbook.policies.exceptions import PolicyException
|
||||||
|
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
|
|
||||||
class PolicyBindingModel(models.Model):
|
class PolicyBindingModel(models.Model):
|
||||||
"""Base Model for objects that have policies applied to them."""
|
"""Base Model for objects that have policies applied to them."""
|
||||||
|
|
||||||
policies = models.ManyToManyField(
|
policies = models.ManyToManyField(
|
||||||
Policy, through="PolicyBinding", related_name="+", blank=True
|
"Policy", through="PolicyBinding", related_name="+", blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
|
@ -24,7 +26,7 @@ class PolicyBinding(UUIDModel):
|
||||||
|
|
||||||
enabled = models.BooleanField(default=True)
|
enabled = models.BooleanField(default=True)
|
||||||
|
|
||||||
policy = models.ForeignKey(Policy, on_delete=models.CASCADE, related_name="+")
|
policy = models.ForeignKey("Policy", on_delete=models.CASCADE, related_name="+")
|
||||||
target = models.ForeignKey(
|
target = models.ForeignKey(
|
||||||
PolicyBindingModel, on_delete=models.CASCADE, related_name="+"
|
PolicyBindingModel, on_delete=models.CASCADE, related_name="+"
|
||||||
)
|
)
|
||||||
|
@ -39,3 +41,22 @@ class PolicyBinding(UUIDModel):
|
||||||
|
|
||||||
verbose_name = _("Policy Binding")
|
verbose_name = _("Policy Binding")
|
||||||
verbose_name_plural = _("Policy Bindings")
|
verbose_name_plural = _("Policy Bindings")
|
||||||
|
|
||||||
|
|
||||||
|
class Policy(UUIDModel, CreatedUpdatedModel):
|
||||||
|
"""Policies which specify if a user is authorized to use an Application. Can be overridden by
|
||||||
|
other types to add other fields, more logic, etc."""
|
||||||
|
|
||||||
|
name = models.TextField(blank=True, null=True)
|
||||||
|
negate = models.BooleanField(default=False)
|
||||||
|
order = models.IntegerField(default=0)
|
||||||
|
timeout = models.IntegerField(default=30)
|
||||||
|
|
||||||
|
objects = InheritanceManager()
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"Policy {self.name}"
|
||||||
|
|
||||||
|
def passes(self, request: PolicyRequest) -> PolicyResult:
|
||||||
|
"""Check if user instance passes this policy"""
|
||||||
|
raise PolicyException()
|
||||||
|
|
|
@ -9,7 +9,7 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_core", "0001_initial"),
|
("passbook_policies", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -24,7 +24,7 @@ class Migration(migrations.Migration):
|
||||||
parent_link=True,
|
parent_link=True,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
serialize=False,
|
serialize=False,
|
||||||
to="passbook_core.Policy",
|
to="passbook_policies.Policy",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("amount_uppercase", models.IntegerField(default=0)),
|
("amount_uppercase", models.IntegerField(default=0)),
|
||||||
|
@ -41,6 +41,6 @@ class Migration(migrations.Migration):
|
||||||
"verbose_name": "Password Policy",
|
"verbose_name": "Password Policy",
|
||||||
"verbose_name_plural": "Password Policies",
|
"verbose_name_plural": "Password Policies",
|
||||||
},
|
},
|
||||||
bases=("passbook_core.policy",),
|
bases=("passbook_policies.policy",),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.db import models
|
||||||
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 Policy
|
from passbook.policies.models import Policy
|
||||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
|
@ -6,8 +6,9 @@ from typing import Optional
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import Policy, User
|
from passbook.core.models import User
|
||||||
from passbook.policies.exceptions import PolicyException
|
from passbook.policies.exceptions import PolicyException
|
||||||
|
from passbook.policies.models import Policy
|
||||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
|
@ -10,7 +10,7 @@ class Migration(migrations.Migration):
|
||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_core", "0001_initial"),
|
("passbook_policies", "0001_initial"),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ class Migration(migrations.Migration):
|
||||||
parent_link=True,
|
parent_link=True,
|
||||||
primary_key=True,
|
primary_key=True,
|
||||||
serialize=False,
|
serialize=False,
|
||||||
to="passbook_core.Policy",
|
to="passbook_policies.Policy",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
("check_ip", models.BooleanField(default=True)),
|
("check_ip", models.BooleanField(default=True)),
|
||||||
|
@ -54,7 +54,7 @@ class Migration(migrations.Migration):
|
||||||
"verbose_name": "Reputation Policy",
|
"verbose_name": "Reputation Policy",
|
||||||
"verbose_name_plural": "Reputation Policies",
|
"verbose_name_plural": "Reputation Policies",
|
||||||
},
|
},
|
||||||
bases=("passbook_core.policy",),
|
bases=("passbook_policies.policy",),
|
||||||
),
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name="UserReputation",
|
name="UserReputation",
|
||||||
|
|
|
@ -2,8 +2,9 @@
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from passbook.core.models import Policy, User
|
from passbook.core.models import User
|
||||||
from passbook.lib.utils.http import get_client_ip
|
from passbook.lib.utils.http import get_client_ip
|
||||||
|
from passbook.policies.models import Policy
|
||||||
from passbook.policies.types import PolicyRequest, PolicyResult
|
from passbook.policies.types import PolicyRequest, PolicyResult
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,9 +2,10 @@
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from passbook.core.models import Policy, User
|
from passbook.core.models import User
|
||||||
from passbook.policies.dummy.models import DummyPolicy
|
from passbook.policies.dummy.models import DummyPolicy
|
||||||
from passbook.policies.engine import PolicyEngine
|
from passbook.policies.engine import PolicyEngine
|
||||||
|
from passbook.policies.models import Policy
|
||||||
|
|
||||||
|
|
||||||
class PolicyTestEngine(TestCase):
|
class PolicyTestEngine(TestCase):
|
||||||
|
|
|
@ -11,6 +11,7 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_core", "0001_initial"),
|
("passbook_core", "0001_initial"),
|
||||||
|
("passbook_policies", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -87,7 +88,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"conditions",
|
"conditions",
|
||||||
models.ManyToManyField(blank=True, to="passbook_core.Policy"),
|
models.ManyToManyField(blank=True, to="passbook_policies.Policy"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
|
|
|
@ -8,14 +8,14 @@ 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 Nonce, User
|
from passbook.core.models import Token, User
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
LOGGER = get_logger()
|
LOGGER = get_logger()
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
"""Create Nonce used to recover access"""
|
"""Create Token used to recover access"""
|
||||||
|
|
||||||
help = _("Create a Key which can be used to restore access to passbook.")
|
help = _("Create a Key which can be used to restore access to passbook.")
|
||||||
|
|
||||||
|
@ -30,22 +30,22 @@ class Command(BaseCommand):
|
||||||
"user", action="store", help="Which user the Token gives access to."
|
"user", action="store", help="Which user the Token gives access to."
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_url(self, nonce: Nonce) -> str:
|
def get_url(self, token: Token) -> str:
|
||||||
"""Get full recovery link"""
|
"""Get full recovery link"""
|
||||||
path = reverse("passbook_recovery:use-nonce", kwargs={"uuid": str(nonce.uuid)})
|
path = reverse("passbook_recovery:use-token", kwargs={"uuid": str(token.uuid)})
|
||||||
return f"https://{CONFIG.y('domain')}{path}"
|
return f"https://{CONFIG.y('domain')}{path}"
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
"""Create Nonce used to recover access"""
|
"""Create Token used to recover access"""
|
||||||
duration = int(options.get("duration", 1))
|
duration = int(options.get("duration", 1))
|
||||||
delta = timedelta(days=duration * 365.2425)
|
delta = timedelta(days=duration * 365.2425)
|
||||||
_now = now()
|
_now = now()
|
||||||
expiry = _now + delta
|
expiry = _now + delta
|
||||||
user = User.objects.get(username=options.get("user"))
|
user = User.objects.get(username=options.get("user"))
|
||||||
nonce = Nonce.objects.create(
|
token = Token.objects.create(
|
||||||
expires=expiry,
|
expires=expiry,
|
||||||
user=user,
|
user=user,
|
||||||
description=f"Recovery Nonce generated by {getuser()} on {_now}",
|
description=f"Recovery Token generated by {getuser()} on {_now}",
|
||||||
)
|
)
|
||||||
self.stdout.write(
|
self.stdout.write(
|
||||||
(
|
(
|
||||||
|
@ -53,4 +53,4 @@ class Command(BaseCommand):
|
||||||
f" anyone to access passbook as {user}."
|
f" anyone to access passbook as {user}."
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
self.stdout.write(self.get_url(nonce))
|
self.stdout.write(self.get_url(token))
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.core.management import call_command
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
|
||||||
from passbook.core.models import Nonce, User
|
from passbook.core.models import Token, User
|
||||||
from passbook.lib.config import CONFIG
|
from passbook.lib.config import CONFIG
|
||||||
|
|
||||||
|
|
||||||
|
@ -19,17 +19,17 @@ class TestRecovery(TestCase):
|
||||||
"""Test creation of a new key"""
|
"""Test creation of a new key"""
|
||||||
CONFIG.update_from_dict({"domain": "testserver"})
|
CONFIG.update_from_dict({"domain": "testserver"})
|
||||||
out = StringIO()
|
out = StringIO()
|
||||||
self.assertEqual(len(Nonce.objects.all()), 0)
|
self.assertEqual(len(Token.objects.all()), 0)
|
||||||
call_command("create_recovery_key", "1", self.user.username, stdout=out)
|
call_command("create_recovery_key", "1", self.user.username, stdout=out)
|
||||||
self.assertIn("https://testserver/recovery/use-nonce/", out.getvalue())
|
self.assertIn("https://testserver/recovery/use-token/", out.getvalue())
|
||||||
self.assertEqual(len(Nonce.objects.all()), 1)
|
self.assertEqual(len(Token.objects.all()), 1)
|
||||||
|
|
||||||
def test_recovery_view(self):
|
def test_recovery_view(self):
|
||||||
"""Test recovery view"""
|
"""Test recovery view"""
|
||||||
out = StringIO()
|
out = StringIO()
|
||||||
call_command("create_recovery_key", "1", self.user.username, stdout=out)
|
call_command("create_recovery_key", "1", self.user.username, stdout=out)
|
||||||
nonce = Nonce.objects.first()
|
token = Token.objects.first()
|
||||||
self.client.get(
|
self.client.get(
|
||||||
reverse("passbook_recovery:use-nonce", kwargs={"uuid": str(nonce.uuid)})
|
reverse("passbook_recovery:use-token", kwargs={"uuid": str(token.uuid)})
|
||||||
)
|
)
|
||||||
self.assertEqual(int(self.client.session["_auth_user_id"]), nonce.user.pk)
|
self.assertEqual(int(self.client.session["_auth_user_id"]), token.user.pk)
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from passbook.recovery.views import UseNonceView
|
from passbook.recovery.views import UseTokenView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("use-nonce/<uuid:uuid>/", UseNonceView.as_view(), name="use-nonce"),
|
path("use-token/<uuid:uuid>/", UseTokenView.as_view(), name="use-token"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -6,19 +6,19 @@ from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
from django.views import View
|
from django.views import View
|
||||||
|
|
||||||
from passbook.core.models import Nonce
|
from passbook.core.models import Token
|
||||||
|
|
||||||
|
|
||||||
class UseNonceView(View):
|
class UseTokenView(View):
|
||||||
"""Use nonce to login"""
|
"""Use token to login"""
|
||||||
|
|
||||||
def get(self, request: HttpRequest, uuid: str) -> HttpResponse:
|
def get(self, request: HttpRequest, uuid: str) -> HttpResponse:
|
||||||
"""Check if nonce exists, log user in and delete nonce."""
|
"""Check if token exists, log user in and delete token."""
|
||||||
nonce: Nonce = get_object_or_404(Nonce, pk=uuid)
|
token: Token = get_object_or_404(Token, pk=uuid)
|
||||||
if nonce.is_expired:
|
if token.is_expired:
|
||||||
nonce.delete()
|
token.delete()
|
||||||
raise Http404
|
raise Http404
|
||||||
login(request, nonce.user, backend="django.contrib.auth.backends.ModelBackend")
|
login(request, token.user, backend="django.contrib.auth.backends.ModelBackend")
|
||||||
nonce.delete()
|
token.delete()
|
||||||
messages.warning(request, _("Used recovery-link to authenticate."))
|
messages.warning(request, _("Used recovery-link to authenticate."))
|
||||||
return redirect("passbook_core:overview")
|
return redirect("passbook_core:overview")
|
||||||
|
|
|
@ -228,8 +228,8 @@ USE_TZ = True
|
||||||
# Add a 10 minute timeout to all Celery tasks.
|
# Add a 10 minute timeout to all Celery tasks.
|
||||||
CELERY_TASK_SOFT_TIME_LIMIT = 600
|
CELERY_TASK_SOFT_TIME_LIMIT = 600
|
||||||
CELERY_BEAT_SCHEDULE = {
|
CELERY_BEAT_SCHEDULE = {
|
||||||
"clean_nonces": {
|
"clean_tokens": {
|
||||||
"task": "passbook.core.tasks.clean_nonces",
|
"task": "passbook.core.tasks.clean_tokens",
|
||||||
"schedule": crontab(minute="*/5"), # Run every 5 minutes
|
"schedule": crontab(minute="*/5"), # Run every 5 minutes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,7 +10,7 @@ from django.utils.translation import gettext as _
|
||||||
from django.views.generic import FormView
|
from django.views.generic import FormView
|
||||||
from structlog import get_logger
|
from structlog import get_logger
|
||||||
|
|
||||||
from passbook.core.models import Nonce
|
from passbook.core.models import Token
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
|
||||||
from passbook.flows.stage import AuthenticationStage
|
from passbook.flows.stage import AuthenticationStage
|
||||||
from passbook.stages.email.forms import EmailStageSendForm
|
from passbook.stages.email.forms import EmailStageSendForm
|
||||||
|
@ -38,9 +38,9 @@ class EmailStageView(FormView, AuthenticationStage):
|
||||||
|
|
||||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
if QS_KEY_TOKEN in request.GET:
|
if QS_KEY_TOKEN in request.GET:
|
||||||
nonce = get_object_or_404(Nonce, pk=request.GET[QS_KEY_TOKEN])
|
token = get_object_or_404(Token, pk=request.GET[QS_KEY_TOKEN])
|
||||||
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = nonce.user
|
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = token.user
|
||||||
nonce.delete()
|
token.delete()
|
||||||
messages.success(request, _("Successfully verified E-Mail."))
|
messages.success(request, _("Successfully verified E-Mail."))
|
||||||
return self.executor.stage_ok()
|
return self.executor.stage_ok()
|
||||||
return super().get(request, *args, **kwargs)
|
return super().get(request, *args, **kwargs)
|
||||||
|
@ -50,16 +50,16 @@ class EmailStageView(FormView, AuthenticationStage):
|
||||||
valid_delta = timedelta(
|
valid_delta = timedelta(
|
||||||
minutes=self.executor.current_stage.token_expiry + 1
|
minutes=self.executor.current_stage.token_expiry + 1
|
||||||
) # + 1 because django timesince always rounds down
|
) # + 1 because django timesince always rounds down
|
||||||
nonce = Nonce.objects.create(user=pending_user, expires=now() + valid_delta)
|
token = Token.objects.create(user=pending_user, expires=now() + valid_delta)
|
||||||
# Send mail to user
|
# Send mail to user
|
||||||
message = TemplateEmailMessage(
|
message = TemplateEmailMessage(
|
||||||
subject=_("passbook - Password Recovery"),
|
subject=_("passbook - Password Recovery"),
|
||||||
template_name=self.executor.current_stage.template,
|
template_name=self.executor.current_stage.template,
|
||||||
to=[pending_user.email],
|
to=[pending_user.email],
|
||||||
template_context={
|
template_context={
|
||||||
"url": self.get_full_url(**{QS_KEY_TOKEN: nonce.pk.hex}),
|
"url": self.get_full_url(**{QS_KEY_TOKEN: token.pk.hex}),
|
||||||
"user": pending_user,
|
"user": pending_user,
|
||||||
"expires": nonce.expires,
|
"expires": token.expires,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
send_mails(self.executor.current_stage, message)
|
send_mails(self.executor.current_stage, message)
|
||||||
|
|
|
@ -5,7 +5,7 @@ from django.core import mail
|
||||||
from django.shortcuts import reverse
|
from django.shortcuts import reverse
|
||||||
from django.test import Client, TestCase
|
from django.test import Client, TestCase
|
||||||
|
|
||||||
from passbook.core.models import Nonce, User
|
from passbook.core.models import Token, User
|
||||||
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
|
||||||
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
|
||||||
from passbook.flows.views import SESSION_KEY_PLAN
|
from passbook.flows.views import SESSION_KEY_PLAN
|
||||||
|
@ -77,7 +77,7 @@ class TestEmailStage(TestCase):
|
||||||
url = reverse(
|
url = reverse(
|
||||||
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
|
||||||
)
|
)
|
||||||
token = Nonce.objects.get(user=self.user)
|
token = Token.objects.get(user=self.user)
|
||||||
url += f"?{QS_KEY_TOKEN}={token.pk.hex}"
|
url += f"?{QS_KEY_TOKEN}={token.pk.hex}"
|
||||||
response = self.client.get(url)
|
response = self.client.get(url)
|
||||||
self.assertEqual(response.status_code, 302)
|
self.assertEqual(response.status_code, 302)
|
||||||
|
|
|
@ -11,7 +11,7 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_flows", "0001_initial"),
|
("passbook_flows", "0001_initial"),
|
||||||
("passbook_core", "0012_delete_factor"),
|
("passbook_policies", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
@ -39,7 +39,7 @@ class Migration(migrations.Migration):
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
"password_policies",
|
"password_policies",
|
||||||
models.ManyToManyField(blank=True, to="passbook_core.Policy"),
|
models.ManyToManyField(blank=True, to="passbook_policies.Policy"),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
|
|
|
@ -12,7 +12,7 @@ class Migration(migrations.Migration):
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
("passbook_flows", "0005_auto_20200512_1158"),
|
("passbook_flows", "0005_auto_20200512_1158"),
|
||||||
("passbook_policies", "0003_auto_20200508_1642"),
|
("passbook_policies", "0001_initial"),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
|
|
60
swagger.yaml
60
swagger.yaml
|
@ -154,7 +154,7 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- core
|
- core
|
||||||
parameters: []
|
parameters: []
|
||||||
/core/applications/{uuid}/:
|
/core/applications/{id}/:
|
||||||
get:
|
get:
|
||||||
operationId: core_applications_read
|
operationId: core_applications_read
|
||||||
description: Application Viewset
|
description: Application Viewset
|
||||||
|
@ -208,12 +208,11 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- core
|
- core
|
||||||
parameters:
|
parameters:
|
||||||
- name: uuid
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
description: A UUID string identifying this application.
|
description: A unique integer value identifying this application.
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: integer
|
||||||
format: uuid
|
|
||||||
/core/groups/:
|
/core/groups/:
|
||||||
get:
|
get:
|
||||||
operationId: core_groups_list
|
operationId: core_groups_list
|
||||||
|
@ -2658,7 +2657,7 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
parameters: []
|
parameters: []
|
||||||
/sources/all/{uuid}/:
|
/sources/all/{id}/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_all_read
|
operationId: sources_all_read
|
||||||
description: Source Viewset
|
description: Source Viewset
|
||||||
|
@ -2671,12 +2670,11 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
parameters:
|
parameters:
|
||||||
- name: uuid
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
description: A UUID string identifying this source.
|
description: A unique integer value identifying this source.
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: integer
|
||||||
format: uuid
|
|
||||||
/sources/ldap/:
|
/sources/ldap/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_ldap_list
|
operationId: sources_ldap_list
|
||||||
|
@ -2744,7 +2742,7 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
parameters: []
|
parameters: []
|
||||||
/sources/ldap/{uuid}/:
|
/sources/ldap/{id}/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_ldap_read
|
operationId: sources_ldap_read
|
||||||
description: LDAP Source Viewset
|
description: LDAP Source Viewset
|
||||||
|
@ -2798,12 +2796,11 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
parameters:
|
parameters:
|
||||||
- name: uuid
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
description: A UUID string identifying this LDAP Source.
|
description: A unique integer value identifying this LDAP Source.
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: integer
|
||||||
format: uuid
|
|
||||||
/sources/oauth/:
|
/sources/oauth/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_oauth_list
|
operationId: sources_oauth_list
|
||||||
|
@ -2871,7 +2868,7 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
parameters: []
|
parameters: []
|
||||||
/sources/oauth/{uuid}/:
|
/sources/oauth/{id}/:
|
||||||
get:
|
get:
|
||||||
operationId: sources_oauth_read
|
operationId: sources_oauth_read
|
||||||
description: Source Viewset
|
description: Source Viewset
|
||||||
|
@ -2925,12 +2922,11 @@ paths:
|
||||||
tags:
|
tags:
|
||||||
- sources
|
- sources
|
||||||
parameters:
|
parameters:
|
||||||
- name: uuid
|
- name: id
|
||||||
in: path
|
in: path
|
||||||
description: A UUID string identifying this Generic OAuth Source.
|
description: A unique integer value identifying this Generic OAuth Source.
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: integer
|
||||||
format: uuid
|
|
||||||
/stages/all/:
|
/stages/all/:
|
||||||
get:
|
get:
|
||||||
operationId: stages_all_list
|
operationId: stages_all_list
|
||||||
|
@ -4837,9 +4833,8 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
title: Uuid
|
title: ID
|
||||||
type: string
|
type: integer
|
||||||
format: uuid
|
|
||||||
readOnly: true
|
readOnly: true
|
||||||
name:
|
name:
|
||||||
title: Name
|
title: Name
|
||||||
|
@ -4878,8 +4873,8 @@ definitions:
|
||||||
policies:
|
policies:
|
||||||
type: array
|
type: array
|
||||||
items:
|
items:
|
||||||
type: string
|
type: integer
|
||||||
format: uuid
|
readOnly: true
|
||||||
uniqueItems: true
|
uniqueItems: true
|
||||||
Group:
|
Group:
|
||||||
required:
|
required:
|
||||||
|
@ -5610,9 +5605,8 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
title: Uuid
|
title: ID
|
||||||
type: string
|
type: integer
|
||||||
format: uuid
|
|
||||||
readOnly: true
|
readOnly: true
|
||||||
name:
|
name:
|
||||||
title: Name
|
title: Name
|
||||||
|
@ -5647,9 +5641,8 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
title: Uuid
|
title: ID
|
||||||
type: string
|
type: integer
|
||||||
format: uuid
|
|
||||||
readOnly: true
|
readOnly: true
|
||||||
name:
|
name:
|
||||||
title: Name
|
title: Name
|
||||||
|
@ -5743,9 +5736,8 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
pk:
|
pk:
|
||||||
title: Uuid
|
title: ID
|
||||||
type: string
|
type: integer
|
||||||
format: uuid
|
|
||||||
readOnly: true
|
readOnly: true
|
||||||
name:
|
name:
|
||||||
title: Name
|
title: Name
|
||||||
|
|
Reference in New Issue