enterprise: allow deletion/modification of users when in read-only mode (#12289)
* enterprise: allow deletion/modification of users when in read-only mode Signed-off-by: Jens Langhammer <jens@goauthentik.io> * actually 10.5+ Signed-off-by: Jens Langhammer <jens@goauthentik.io> * Apply suggestions from code review Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com> Signed-off-by: Jens L. <jens@beryju.org> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io> Signed-off-by: Jens L. <jens@beryju.org> Co-authored-by: Tana M Berry <tanamarieberry@yahoo.com>
This commit is contained in:
		@ -6,6 +6,7 @@ from django.http import HttpRequest, HttpResponse, JsonResponse
 | 
				
			|||||||
from django.urls import resolve
 | 
					from django.urls import resolve
 | 
				
			||||||
from structlog.stdlib import BoundLogger, get_logger
 | 
					from structlog.stdlib import BoundLogger, get_logger
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from authentik.core.api.users import UserViewSet
 | 
				
			||||||
from authentik.enterprise.api import LicenseViewSet
 | 
					from authentik.enterprise.api import LicenseViewSet
 | 
				
			||||||
from authentik.enterprise.license import LicenseKey
 | 
					from authentik.enterprise.license import LicenseKey
 | 
				
			||||||
from authentik.enterprise.models import LicenseUsageStatus
 | 
					from authentik.enterprise.models import LicenseUsageStatus
 | 
				
			||||||
@ -59,6 +60,9 @@ class EnterpriseMiddleware:
 | 
				
			|||||||
        # Flow executor is mounted as an API path but explicitly allowed
 | 
					        # Flow executor is mounted as an API path but explicitly allowed
 | 
				
			||||||
        if request.resolver_match._func_path == class_to_path(FlowExecutorView):
 | 
					        if request.resolver_match._func_path == class_to_path(FlowExecutorView):
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
 | 
					        # Always allow making changes to users, even in case the license has ben exceeded
 | 
				
			||||||
 | 
					        if request.resolver_match._func_path == class_to_path(UserViewSet):
 | 
				
			||||||
 | 
					            return True
 | 
				
			||||||
        # Only apply these restrictions to the API
 | 
					        # Only apply these restrictions to the API
 | 
				
			||||||
        if "authentik_api" not in request.resolver_match.app_names:
 | 
					        if "authentik_api" not in request.resolver_match.app_names:
 | 
				
			||||||
            return True
 | 
					            return True
 | 
				
			||||||
 | 
				
			|||||||
@ -215,3 +215,49 @@ class TestReadOnly(FlowTestCase):
 | 
				
			|||||||
            {"detail": "Request denied due to expired/invalid license.", "code": "denied_license"},
 | 
					            {"detail": "Request denied due to expired/invalid license.", "code": "denied_license"},
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.assertEqual(response.status_code, 400)
 | 
					        self.assertEqual(response.status_code, 400)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    @patch(
 | 
				
			||||||
 | 
					        "authentik.enterprise.license.LicenseKey.validate",
 | 
				
			||||||
 | 
					        MagicMock(
 | 
				
			||||||
 | 
					            return_value=LicenseKey(
 | 
				
			||||||
 | 
					                aud="",
 | 
				
			||||||
 | 
					                exp=expiry_valid,
 | 
				
			||||||
 | 
					                name=generate_id(),
 | 
				
			||||||
 | 
					                internal_users=100,
 | 
				
			||||||
 | 
					                external_users=100,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    @patch(
 | 
				
			||||||
 | 
					        "authentik.enterprise.license.LicenseKey.get_internal_user_count",
 | 
				
			||||||
 | 
					        MagicMock(return_value=1000),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    @patch(
 | 
				
			||||||
 | 
					        "authentik.enterprise.license.LicenseKey.get_external_user_count",
 | 
				
			||||||
 | 
					        MagicMock(return_value=1000),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    @patch(
 | 
				
			||||||
 | 
					        "authentik.enterprise.license.LicenseKey.record_usage",
 | 
				
			||||||
 | 
					        MagicMock(),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    def test_manage_users(self):
 | 
				
			||||||
 | 
					        """Test that managing users is still possible"""
 | 
				
			||||||
 | 
					        License.objects.create(key=generate_id())
 | 
				
			||||||
 | 
					        usage = LicenseUsage.objects.create(
 | 
				
			||||||
 | 
					            internal_user_count=100,
 | 
				
			||||||
 | 
					            external_user_count=100,
 | 
				
			||||||
 | 
					            status=LicenseUsageStatus.VALID,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        usage.record_date = now() - timedelta(weeks=THRESHOLD_READ_ONLY_WEEKS + 1)
 | 
				
			||||||
 | 
					        usage.save(update_fields=["record_date"])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        admin = create_test_admin_user()
 | 
				
			||||||
 | 
					        self.client.force_login(admin)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Reading is always allowed
 | 
				
			||||||
 | 
					        response = self.client.get(reverse("authentik_api:user-list"))
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # Writing should also be allowed
 | 
				
			||||||
 | 
					        response = self.client.patch(reverse("authentik_api:user-detail", kwargs={"pk": admin.pk}))
 | 
				
			||||||
 | 
					        self.assertEqual(response.status_code, 200)
 | 
				
			||||||
 | 
				
			|||||||
@ -101,7 +101,15 @@ The following events occur when a license expires or the internal/external user
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- After another 2 weeks, users get a warning banner
 | 
					- After another 2 weeks, users get a warning banner
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- After another 2 weeks, the authentik Enterprise instance becomes “read-only”
 | 
					- After another 2 weeks, the authentik Enterprise instance becomes "read-only"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    When an authentik instance is in read-only mode, the following actions are still possible:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    - Users can authenticate and authorize applications
 | 
				
			||||||
 | 
					    - Licenses can be modified
 | 
				
			||||||
 | 
					    - Users can be modified/deleted <span class="badge badge--version">authentik 2024.10.5+</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    After the violation is corrected (either the user count returns to be within the limits of the license or the license is renewed), authentik will return to the standard read-write mode and the notification will disappear.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### About users and licenses
 | 
					### About users and licenses
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user