diff --git a/kolibri/core/auth/management/commands/deprovision.py b/kolibri/core/auth/management/commands/deprovision.py index e25d68e43c1..66cdda298ff 100644 --- a/kolibri/core/auth/management/commands/deprovision.py +++ b/kolibri/core/auth/management/commands/deprovision.py @@ -11,7 +11,8 @@ from kolibri.core.auth.management.utils import confirm_or_exit from kolibri.core.auth.models import FacilityDataset from kolibri.core.auth.models import FacilityUser -from kolibri.core.auth.utils.delete import DisablePostDeleteSignal +from kolibri.core.auth.utils.deprovision import deprovision +from kolibri.core.auth.utils.deprovision import get_deprovision_progress_total from kolibri.core.device.models import DevicePermissions from kolibri.core.device.models import DeviceSettings from kolibri.core.logger.models import AttemptLog @@ -57,12 +58,10 @@ def add_arguments(self, parser): ) def deprovision(self): - with DisablePostDeleteSignal(), self.start_progress( - total=len(MODELS_TO_DELETE) + with self.start_progress( + total=get_deprovision_progress_total() ) as progress_update: - for Model in MODELS_TO_DELETE: - Model.objects.all().delete() - progress_update(1) + deprovision(progress_update=progress_update) def handle_async(self, *args, **options): diff --git a/kolibri/core/auth/management/utils.py b/kolibri/core/auth/management/utils.py index a969626c93c..a04184464d0 100644 --- a/kolibri/core/auth/management/utils.py +++ b/kolibri/core/auth/management/utils.py @@ -449,10 +449,12 @@ def _sync(self, sync_session_client, **options): # noqa: C901 if not no_provision: with self._lock(): - if user_id: - provision_single_user_device( - FacilityUser.objects.get(id=user_id) - ) + try: + user = FacilityUser.all_objects.get(id=user_id) + except FacilityUser.DoesNotExist: + user = None + if user: + provision_single_user_device(user) else: create_superuser_and_provision_device( username, dataset_id, noninteractive=noninteractive diff --git a/kolibri/core/auth/utils/deprovision.py b/kolibri/core/auth/utils/deprovision.py new file mode 100644 index 00000000000..90dab0162f6 --- /dev/null +++ b/kolibri/core/auth/utils/deprovision.py @@ -0,0 +1,47 @@ +from morango.models import Certificate +from morango.models import DatabaseIDModel +from morango.models import DatabaseMaxCounter +from morango.models import DeletedModels +from morango.models import HardDeletedModels +from morango.models import Store + +from kolibri.core.auth.models import FacilityDataset +from kolibri.core.auth.models import FacilityUser +from kolibri.core.auth.utils.delete import DisablePostDeleteSignal +from kolibri.core.device.models import DevicePermissions +from kolibri.core.device.models import DeviceSettings +from kolibri.core.logger.models import AttemptLog +from kolibri.core.logger.models import ContentSessionLog +from kolibri.core.logger.models import ContentSummaryLog +from kolibri.core.tasks.main import job_storage + +MODELS_TO_DELETE = [ + AttemptLog, + ContentSessionLog, + ContentSummaryLog, + FacilityUser, + FacilityDataset, + HardDeletedModels, + Certificate, + DatabaseIDModel, + Store, + DevicePermissions, + DeletedModels, + DeviceSettings, + DatabaseMaxCounter, +] + + +def deprovision(progress_update=None): + with DisablePostDeleteSignal(): + for Model in MODELS_TO_DELETE: + Model.objects.all().delete() + if progress_update: + progress_update(1) + + # Clear all completed, failed or cancelled jobs + job_storage.clear() + + +def get_deprovision_progress_total(): + return len(MODELS_TO_DELETE) diff --git a/kolibri/core/device/soud.py b/kolibri/core/device/soud.py index 4f2d10fece6..f23ebf8de1d 100644 --- a/kolibri/core/device/soud.py +++ b/kolibri/core/device/soud.py @@ -18,6 +18,7 @@ from kolibri.core.auth.constants.morango_sync import PROFILE_FACILITY_DATA from kolibri.core.auth.constants.morango_sync import ScopeDefinitions +from kolibri.core.auth.models import Facility from kolibri.core.auth.models import FacilityDataset from kolibri.core.auth.models import FacilityUser from kolibri.core.device.models import SyncQueue @@ -49,7 +50,10 @@ def __init__(self, user_id, instance_id): @cached_property def user(self): - return FacilityUser.objects.get(id=self.user_id) + try: + return FacilityUser.all_objects.get(id=self.user_id) + except FacilityUser.DoesNotExist: + return None @property def has_sync_queue(self): @@ -382,13 +386,18 @@ def execute_sync(context): baseurl=context.network_location.base_url, keep_alive=True, noninteractive=True, + no_provision=True, ) if sync_session_id: command = "resumesync" kwargs["id"] = sync_session_id else: - kwargs["facility"] = context.user.facility_id + kwargs["facility"] = ( + context.user.facility_id + if context.user + else Facility.get_default_facility().id + ) sync_queue.status = SyncQueueStatus.Syncing sync_queue.save() diff --git a/kolibri/core/device/tasks.py b/kolibri/core/device/tasks.py index 4fd33f36ade..5c6e7d6c4e1 100644 --- a/kolibri/core/device/tasks.py +++ b/kolibri/core/device/tasks.py @@ -10,6 +10,7 @@ from kolibri.core.auth.models import Facility from kolibri.core.auth.models import FacilityUser from kolibri.core.auth.serializers import FacilitySerializer +from kolibri.core.auth.utils.deprovision import deprovision from kolibri.core.device.models import DevicePermissions from kolibri.core.device.models import OSUser from kolibri.core.device.serializers import DeviceSerializerMixin @@ -20,6 +21,7 @@ from kolibri.core.device.utils import valid_app_key_on_request from kolibri.core.tasks.decorators import register_task from kolibri.core.tasks.permissions import FirstProvisioning +from kolibri.core.tasks.permissions import IsDeviceUnusable from kolibri.core.tasks.utils import get_current_job from kolibri.core.tasks.validation import JobValidator from kolibri.core.utils.token_generator import TokenGenerator @@ -29,6 +31,7 @@ logger = logging.getLogger(__name__) PROVISION_TASK_QUEUE = "device_provision" +DEPROVISION_TASK_QUEUE = "device_deprovision" class DeviceProvisionValidator(DeviceSerializerMixin, JobValidator): @@ -243,3 +246,15 @@ def provisiondevice(**data): # noqa C901 updates["auth_token"] = TokenGenerator().make_token(superuser.id) job.update_metadata(**updates) + + +@register_task( + permission_classes=[IsDeviceUnusable], + cancellable=False, + queue=DEPROVISION_TASK_QUEUE, +) +def deprovisiondevice(): + """ + Task for deprovisioning a device. + """ + deprovision() diff --git a/kolibri/core/device/utils.py b/kolibri/core/device/utils.py index 7209bd878e2..b22813f86a1 100644 --- a/kolibri/core/device/utils.py +++ b/kolibri/core/device/utils.py @@ -29,6 +29,9 @@ APP_KEY_COOKIE_NAME = "app_key_cookie" APP_AUTH_TOKEN_COOKIE_NAME = "app_auth_token_cookie" +DEVICE_UNUSABLE_NO_SUPERUSERS = "NO_SUPERUSERS" +DEVICE_UNUSABLE_SUPERUSERS_SOFT_DELETED = "SUPERUSERS_SOFT_DELETED" + class DeviceNotProvisioned(Exception): pass @@ -494,3 +497,22 @@ def is_full_facility_import(dataset_id): .filter(scope_definition_id=ScopeDefinitions.FULL_FACILITY) .exists() ) + + +def get_device_unusable_reason(): + from kolibri.core.auth.models import FacilityUser + + is_soud = get_device_setting("subset_of_users_device") + if not is_soud: + return None + + superadmins = FacilityUser.all_objects.filter(devicepermissions__is_superuser=True) + if not superadmins.exists(): + return DEVICE_UNUSABLE_NO_SUPERUSERS + + non_soft_deleted_superadmins = FacilityUser.objects.filter( + devicepermissions__is_superuser=True + ) + if not non_soft_deleted_superadmins.exists(): + return DEVICE_UNUSABLE_SUPERUSERS_SOFT_DELETED + return None diff --git a/kolibri/core/tasks/permissions.py b/kolibri/core/tasks/permissions.py index 110c3469791..38ae4e06b49 100644 --- a/kolibri/core/tasks/permissions.py +++ b/kolibri/core/tasks/permissions.py @@ -197,3 +197,13 @@ def user_can_run_job(self, user, job): def user_can_read_job(self, user, job): return True + + +class IsDeviceUnusable(BasePermission): + def user_can_run_job(self, user, job): + from kolibri.core.device.utils import get_device_unusable_reason + + return get_device_unusable_reason() is not None + + def user_can_read_job(self, user, job): + return True diff --git a/kolibri/core/views.py b/kolibri/core/views.py index b810a27ed34..273c68eab49 100644 --- a/kolibri/core/views.py +++ b/kolibri/core/views.py @@ -128,23 +128,13 @@ def get(self, request): return RootURLRedirectView.as_view()(request) -device_is_provisioned = False - - -def is_provisioned(): - # First check if the device has been provisioned - global device_is_provisioned - device_is_provisioned = device_is_provisioned or device_provisioned() - return device_is_provisioned - - class RootURLRedirectView(View): def get(self, request): """ Redirects user based on the highest role they have for which a redirect is defined. """ # If it has not been provisioned and we have something that can handle setup, redirect there. - if not is_provisioned() and SetupHook.provision_url: + if not device_provisioned() and SetupHook.provision_url: return redirect(SetupHook.provision_url()) if request.user.is_authenticated: diff --git a/kolibri/plugins/user_auth/assets/src/constants.js b/kolibri/plugins/user_auth/assets/src/constants.js index f47d7372a9a..b8a50d2c6ce 100644 --- a/kolibri/plugins/user_auth/assets/src/constants.js +++ b/kolibri/plugins/user_auth/assets/src/constants.js @@ -9,3 +9,8 @@ export const ComponentMap = { export const pageNameToModuleMap = { [ComponentMap.SIGN_IN]: 'signIn', }; + +export const DeviceUnusableReason = { + NO_SUPERUSERS: 'NO_SUPERUSERS', + SUPERUSERS_SOFT_DELETED: 'SUPERUSERS_SOFT_DELETED', +}; diff --git a/kolibri/plugins/user_auth/assets/src/views/AuthBase.vue b/kolibri/plugins/user_auth/assets/src/views/AuthBase.vue index 3d9900b0ac8..f9c0ba8b068 100644 --- a/kolibri/plugins/user_auth/assets/src/views/AuthBase.vue +++ b/kolibri/plugins/user_auth/assets/src/views/AuthBase.vue @@ -6,7 +6,7 @@
@@ -24,10 +24,16 @@ > {{ logoText }} -

- {{ $tr('restrictedAccess') }} -

-

{{ $tr('restrictedAccessDescription') }}

+ +
+ +
+ + +

{{ strings.unknownIssueNotice$() }}

+ +
+ + + + + diff --git a/kolibri/plugins/user_auth/kolibri_plugin.py b/kolibri/plugins/user_auth/kolibri_plugin.py index 72803616c5b..bcf909e2377 100644 --- a/kolibri/plugins/user_auth/kolibri_plugin.py +++ b/kolibri/plugins/user_auth/kolibri_plugin.py @@ -1,5 +1,6 @@ from kolibri.core.auth.constants.user_kinds import ANONYMOUS from kolibri.core.device.utils import get_device_setting +from kolibri.core.device.utils import get_device_unusable_reason from kolibri.core.device.utils import is_landing_page from kolibri.core.device.utils import LANDING_PAGE_LEARN from kolibri.core.hooks import NavigationHook @@ -28,6 +29,7 @@ def plugin_data(self): return { "oidcProviderEnabled": OIDCProviderHook.is_enabled(), "allowGuestAccess": get_device_setting("allow_guest_access"), + "deviceUnusableReason": get_device_unusable_reason(), } diff --git a/packages/kolibri-common/utils/syncTaskUtils.js b/packages/kolibri-common/utils/syncTaskUtils.js index 08abf409513..cf07b57327b 100644 --- a/packages/kolibri-common/utils/syncTaskUtils.js +++ b/packages/kolibri-common/utils/syncTaskUtils.js @@ -25,6 +25,7 @@ export const TaskTypes = { EXPORTUSERSTOCSV: 'kolibri.core.auth.tasks.exportuserstocsv', IMPORTLODUSER: 'kolibri.core.auth.tasks.peeruserimport', PROVISIONDEVICE: 'kolibri.core.device.tasks.provisiondevice', + DEPROVISIONDEVICE: 'kolibri.core.device.tasks.deprovisiondevice', }; // identical to facility constants.js