diff --git a/passbook/api/v2/urls.py b/passbook/api/v2/urls.py index 4c8ae10ff..a84f98ca9 100644 --- a/passbook/api/v2/urls.py +++ b/passbook/api/v2/urls.py @@ -21,6 +21,7 @@ from passbook.policies.api import PolicyBindingViewSet, PolicyViewSet from passbook.policies.dummy.api import DummyPolicyViewSet from passbook.policies.expiry.api import PasswordExpiryPolicyViewSet from passbook.policies.expression.api import ExpressionPolicyViewSet +from passbook.policies.group_membership.api import GroupMembershipPolicyViewSet from passbook.policies.hibp.api import HaveIBeenPwendPolicyViewSet from passbook.policies.password.api import PasswordPolicyViewSet from passbook.policies.reputation.api import ReputationPolicyViewSet @@ -71,9 +72,10 @@ router.register("sources/oauth", OAuthSourceViewSet) router.register("policies/all", PolicyViewSet) router.register("policies/bindings", PolicyBindingViewSet) router.register("policies/expression", ExpressionPolicyViewSet) +router.register("policies/group_membership", GroupMembershipPolicyViewSet) router.register("policies/haveibeenpwned", HaveIBeenPwendPolicyViewSet) +router.register("policies/password_expiry", PasswordExpiryPolicyViewSet) router.register("policies/password", PasswordPolicyViewSet) -router.register("policies/passwordexpiry", PasswordExpiryPolicyViewSet) router.register("policies/reputation", ReputationPolicyViewSet) router.register("providers/all", ProviderViewSet) diff --git a/passbook/core/forms/groups.py b/passbook/core/forms/groups.py index 46577d957..3c8dae507 100644 --- a/passbook/core/forms/groups.py +++ b/passbook/core/forms/groups.py @@ -2,6 +2,7 @@ from django import forms from django.contrib.admin.widgets import FilteredSelectMultiple +from passbook.admin.fields import CodeMirrorWidget, YAMLField from passbook.core.models import Group, User @@ -34,4 +35,8 @@ class GroupForm(forms.ModelForm): fields = ["name", "parent", "members", "attributes"] widgets = { "name": forms.TextInput(), + "attributes": CodeMirrorWidget, + } + field_classes = { + "attributes": YAMLField, } diff --git a/passbook/policies/expression/tests/test_evaluator.py b/passbook/policies/expression/tests.py similarity index 100% rename from passbook/policies/expression/tests/test_evaluator.py rename to passbook/policies/expression/tests.py diff --git a/passbook/policies/expression/tests/__init__.py b/passbook/policies/group_membership/__init__.py similarity index 100% rename from passbook/policies/expression/tests/__init__.py rename to passbook/policies/group_membership/__init__.py diff --git a/passbook/policies/group_membership/api.py b/passbook/policies/group_membership/api.py new file mode 100644 index 000000000..e712dc19c --- /dev/null +++ b/passbook/policies/group_membership/api.py @@ -0,0 +1,23 @@ +"""Group Membership Policy API""" +from rest_framework.serializers import ModelSerializer +from rest_framework.viewsets import ModelViewSet + +from passbook.policies.forms import GENERAL_SERIALIZER_FIELDS +from passbook.policies.group_membership.models import GroupMembershipPolicy + + +class GroupMembershipPolicySerializer(ModelSerializer): + """Group Membership Policy Serializer""" + + class Meta: + model = GroupMembershipPolicy + fields = GENERAL_SERIALIZER_FIELDS + [ + "group", + ] + + +class GroupMembershipPolicyViewSet(ModelViewSet): + """Group Membership Policy Viewset""" + + queryset = GroupMembershipPolicy.objects.all() + serializer_class = GroupMembershipPolicySerializer diff --git a/passbook/policies/group_membership/apps.py b/passbook/policies/group_membership/apps.py new file mode 100644 index 000000000..95ea0edb2 --- /dev/null +++ b/passbook/policies/group_membership/apps.py @@ -0,0 +1,11 @@ +"""passbook Group Membership policy app config""" + +from django.apps import AppConfig + + +class PassbookPoliciesGroupMembershipConfig(AppConfig): + """passbook Group Membership policy app config""" + + name = "passbook.policies.group_membership" + label = "passbook_policies_group_membership" + verbose_name = "passbook Policies.Group Membership" diff --git a/passbook/policies/group_membership/forms.py b/passbook/policies/group_membership/forms.py new file mode 100644 index 000000000..af2368694 --- /dev/null +++ b/passbook/policies/group_membership/forms.py @@ -0,0 +1,20 @@ +"""passbook Group Membership Policy forms""" + +from django import forms + +from passbook.policies.forms import GENERAL_FIELDS +from passbook.policies.group_membership.models import GroupMembershipPolicy + + +class GroupMembershipPolicyForm(forms.ModelForm): + """GroupMembershipPolicy Form""" + + class Meta: + + model = GroupMembershipPolicy + fields = GENERAL_FIELDS + [ + "group", + ] + widgets = { + "name": forms.TextInput(), + } diff --git a/passbook/policies/group_membership/migrations/0001_initial.py b/passbook/policies/group_membership/migrations/0001_initial.py new file mode 100644 index 000000000..5c8325ae7 --- /dev/null +++ b/passbook/policies/group_membership/migrations/0001_initial.py @@ -0,0 +1,47 @@ +# Generated by Django 3.0.7 on 2020-07-01 19:01 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("passbook_policies", "0002_auto_20200528_1647"), + ("passbook_core", "0003_default_user"), + ] + + operations = [ + migrations.CreateModel( + name="GroupMembershipPolicy", + 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_policies.Policy", + ), + ), + ( + "group", + models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="passbook_core.Group", + ), + ), + ], + options={ + "verbose_name": "Group Membership Policy", + "verbose_name_plural": "Group Membership Policies", + }, + bases=("passbook_policies.policy",), + ), + ] diff --git a/passbook/policies/group_membership/migrations/__init__.py b/passbook/policies/group_membership/migrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/passbook/policies/group_membership/models.py b/passbook/policies/group_membership/models.py new file mode 100644 index 000000000..730b49a9e --- /dev/null +++ b/passbook/policies/group_membership/models.py @@ -0,0 +1,23 @@ +"""user field matcher models""" +from django.db import models +from django.utils.translation import gettext as _ + +from passbook.core.models import Group +from passbook.policies.models import Policy +from passbook.policies.types import PolicyRequest, PolicyResult + + +class GroupMembershipPolicy(Policy): + """Check that the user is member of the selected group.""" + + group = models.ForeignKey(Group, null=True, blank=True, on_delete=models.SET_NULL) + + form = "passbook.policies.group_membership.forms.GroupMembershipPolicyForm" + + def passes(self, request: PolicyRequest) -> PolicyResult: + return PolicyResult(self.group.user_set.filter(pk=request.user.pk).exists()) + + class Meta: + + verbose_name = _("Group Membership Policy") + verbose_name_plural = _("Group Membership Policies") diff --git a/passbook/policies/group_membership/tests.py b/passbook/policies/group_membership/tests.py new file mode 100644 index 000000000..d0e7c43ea --- /dev/null +++ b/passbook/policies/group_membership/tests.py @@ -0,0 +1,32 @@ +"""evaluator tests""" +from django.test import TestCase +from guardian.shortcuts import get_anonymous_user + +from passbook.core.models import Group +from passbook.policies.group_membership.models import GroupMembershipPolicy +from passbook.policies.types import PolicyRequest + + +class TestGroupMembershipPolicy(TestCase): + """GroupMembershipPolicy tests""" + + def setUp(self): + self.request = PolicyRequest(user=get_anonymous_user()) + + def test_invalid(self): + """user not in group""" + group = Group.objects.create(name="test") + policy: GroupMembershipPolicy = GroupMembershipPolicy.objects.create( + group=group + ) + self.assertFalse(policy.passes(self.request).passing) + + def test_valid(self): + """user in group""" + group = Group.objects.create(name="test") + group.user_set.add(get_anonymous_user()) + group.save() + policy: GroupMembershipPolicy = GroupMembershipPolicy.objects.create( + group=group + ) + self.assertTrue(policy.passes(self.request).passing) diff --git a/passbook/policies/password/api.py b/passbook/policies/password/api.py index db5e1bd58..6124d4bf1 100644 --- a/passbook/policies/password/api.py +++ b/passbook/policies/password/api.py @@ -1,4 +1,4 @@ -"""Source API Views""" +"""Password Policy API Views""" from rest_framework.serializers import ModelSerializer from rest_framework.viewsets import ModelViewSet @@ -22,7 +22,7 @@ class PasswordPolicySerializer(ModelSerializer): class PasswordPolicyViewSet(ModelViewSet): - """Source Viewset""" + """Password Policy Viewset""" queryset = PasswordPolicy.objects.all() serializer_class = PasswordPolicySerializer diff --git a/passbook/root/settings.py b/passbook/root/settings.py index 0fccf7ce1..2ac5249f0 100644 --- a/passbook/root/settings.py +++ b/passbook/root/settings.py @@ -86,6 +86,7 @@ INSTALLED_APPS = [ "passbook.policies.expression.apps.PassbookPolicyExpressionConfig", "passbook.policies.hibp.apps.PassbookPolicyHIBPConfig", "passbook.policies.password.apps.PassbookPoliciesPasswordConfig", + "passbook.policies.group_membership.apps.PassbookPoliciesGroupMembershipConfig", "passbook.policies.reputation.apps.PassbookPolicyReputationConfig", "passbook.providers.app_gw.apps.PassbookApplicationApplicationGatewayConfig", "passbook.providers.oauth.apps.PassbookProviderOAuthConfig", diff --git a/passbook/stages/otp_time/migrations/0002_auto_20200701_1900.py b/passbook/stages/otp_time/migrations/0002_auto_20200701_1900.py new file mode 100644 index 000000000..ab7bbb259 --- /dev/null +++ b/passbook/stages/otp_time/migrations/0002_auto_20200701_1900.py @@ -0,0 +1,23 @@ +# Generated by Django 3.0.7 on 2020-07-01 19:00 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("passbook_stages_otp_time", "0001_initial"), + ] + + operations = [ + migrations.AlterField( + model_name="otptimestage", + name="digits", + field=models.IntegerField( + choices=[ + (6, "6 digits, widely compatible"), + (8, "8 digits, not compatible with apps like Google Authenticator"), + ] + ), + ), + ] diff --git a/swagger.yaml b/swagger.yaml index a8aae399c..10eb109b4 100755 --- a/swagger.yaml +++ b/swagger.yaml @@ -1222,6 +1222,133 @@ paths: required: true type: string format: uuid + /policies/group_membership/: + get: + operationId: policies_group_membership_list + description: Group Membership Policy 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/GroupMembershipPolicy' + tags: + - policies + post: + operationId: policies_group_membership_create + description: Group Membership Policy Viewset + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/GroupMembershipPolicy' + responses: + '201': + description: '' + schema: + $ref: '#/definitions/GroupMembershipPolicy' + tags: + - policies + parameters: [] + /policies/group_membership/{policy_uuid}/: + get: + operationId: policies_group_membership_read + description: Group Membership Policy Viewset + parameters: [] + responses: + '200': + description: '' + schema: + $ref: '#/definitions/GroupMembershipPolicy' + tags: + - policies + put: + operationId: policies_group_membership_update + description: Group Membership Policy Viewset + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/GroupMembershipPolicy' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/GroupMembershipPolicy' + tags: + - policies + patch: + operationId: policies_group_membership_partial_update + description: Group Membership Policy Viewset + parameters: + - name: data + in: body + required: true + schema: + $ref: '#/definitions/GroupMembershipPolicy' + responses: + '200': + description: '' + schema: + $ref: '#/definitions/GroupMembershipPolicy' + tags: + - policies + delete: + operationId: policies_group_membership_delete + description: Group Membership Policy Viewset + parameters: [] + responses: + '204': + description: '' + tags: + - policies + parameters: + - name: policy_uuid + in: path + description: A UUID string identifying this Group Membership Policy. + required: true + type: string + format: uuid /policies/haveibeenpwned/: get: operationId: policies_haveibeenpwned_list @@ -1352,7 +1479,7 @@ paths: /policies/password/: get: operationId: policies_password_list - description: Source Viewset + description: Password Policy Viewset parameters: - name: ordering in: query @@ -1401,7 +1528,7 @@ paths: - policies post: operationId: policies_password_create - description: Source Viewset + description: Password Policy Viewset parameters: - name: data in: body @@ -1419,7 +1546,7 @@ paths: /policies/password/{policy_uuid}/: get: operationId: policies_password_read - description: Source Viewset + description: Password Policy Viewset parameters: [] responses: '200': @@ -1430,7 +1557,7 @@ paths: - policies put: operationId: policies_password_update - description: Source Viewset + description: Password Policy Viewset parameters: - name: data in: body @@ -1446,7 +1573,7 @@ paths: - policies patch: operationId: policies_password_partial_update - description: Source Viewset + description: Password Policy Viewset parameters: - name: data in: body @@ -1462,7 +1589,7 @@ paths: - policies delete: operationId: policies_password_delete - description: Source Viewset + description: Password Policy Viewset parameters: [] responses: '204': @@ -1476,9 +1603,9 @@ paths: required: true type: string format: uuid - /policies/passwordexpiry/: + /policies/password_expiry/: get: - operationId: policies_passwordexpiry_list + operationId: policies_password_expiry_list description: Password Expiry Viewset parameters: - name: ordering @@ -1527,7 +1654,7 @@ paths: tags: - policies post: - operationId: policies_passwordexpiry_create + operationId: policies_password_expiry_create description: Password Expiry Viewset parameters: - name: data @@ -1543,9 +1670,9 @@ paths: tags: - policies parameters: [] - /policies/passwordexpiry/{policy_uuid}/: + /policies/password_expiry/{policy_uuid}/: get: - operationId: policies_passwordexpiry_read + operationId: policies_password_expiry_read description: Password Expiry Viewset parameters: [] responses: @@ -1556,7 +1683,7 @@ paths: tags: - policies put: - operationId: policies_passwordexpiry_update + operationId: policies_password_expiry_update description: Password Expiry Viewset parameters: - name: data @@ -1572,7 +1699,7 @@ paths: tags: - policies patch: - operationId: policies_passwordexpiry_partial_update + operationId: policies_password_expiry_partial_update description: Password Expiry Viewset parameters: - name: data @@ -1588,7 +1715,7 @@ paths: tags: - policies delete: - operationId: policies_passwordexpiry_delete + operationId: policies_password_expiry_delete description: Password Expiry Viewset parameters: [] responses: @@ -5661,6 +5788,23 @@ definitions: title: Expression type: string minLength: 1 + GroupMembershipPolicy: + type: object + properties: + pk: + title: Policy uuid + type: string + format: uuid + readOnly: true + name: + title: Name + type: string + x-nullable: true + group: + title: Group + type: string + format: uuid + x-nullable: true HaveIBeenPwendPolicy: type: object properties: