Skip to content

Commit 63b19d7

Browse files
authored
Merge pull request #13759 from AlexVelezLl/handle-soft-delete-users-in-lod
Check unusable device on user_auth plugin
2 parents e4ce4c3 + af816b8 commit 63b19d7

File tree

13 files changed

+276
-29
lines changed

13 files changed

+276
-29
lines changed

kolibri/core/auth/management/commands/deprovision.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
from kolibri.core.auth.management.utils import confirm_or_exit
1212
from kolibri.core.auth.models import FacilityDataset
1313
from kolibri.core.auth.models import FacilityUser
14-
from kolibri.core.auth.utils.delete import DisablePostDeleteSignal
14+
from kolibri.core.auth.utils.deprovision import deprovision
15+
from kolibri.core.auth.utils.deprovision import get_deprovision_progress_total
1516
from kolibri.core.device.models import DevicePermissions
1617
from kolibri.core.device.models import DeviceSettings
1718
from kolibri.core.logger.models import AttemptLog
@@ -57,12 +58,10 @@ def add_arguments(self, parser):
5758
)
5859

5960
def deprovision(self):
60-
with DisablePostDeleteSignal(), self.start_progress(
61-
total=len(MODELS_TO_DELETE)
61+
with self.start_progress(
62+
total=get_deprovision_progress_total()
6263
) as progress_update:
63-
for Model in MODELS_TO_DELETE:
64-
Model.objects.all().delete()
65-
progress_update(1)
64+
deprovision(progress_update=progress_update)
6665

6766
def handle_async(self, *args, **options):
6867

kolibri/core/auth/management/utils.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -449,10 +449,12 @@ def _sync(self, sync_session_client, **options): # noqa: C901
449449

450450
if not no_provision:
451451
with self._lock():
452-
if user_id:
453-
provision_single_user_device(
454-
FacilityUser.objects.get(id=user_id)
455-
)
452+
try:
453+
user = FacilityUser.all_objects.get(id=user_id)
454+
except FacilityUser.DoesNotExist:
455+
user = None
456+
if user:
457+
provision_single_user_device(user)
456458
else:
457459
create_superuser_and_provision_device(
458460
username, dataset_id, noninteractive=noninteractive
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
from morango.models import Certificate
2+
from morango.models import DatabaseIDModel
3+
from morango.models import DatabaseMaxCounter
4+
from morango.models import DeletedModels
5+
from morango.models import HardDeletedModels
6+
from morango.models import Store
7+
8+
from kolibri.core.auth.models import FacilityDataset
9+
from kolibri.core.auth.models import FacilityUser
10+
from kolibri.core.auth.utils.delete import DisablePostDeleteSignal
11+
from kolibri.core.device.models import DevicePermissions
12+
from kolibri.core.device.models import DeviceSettings
13+
from kolibri.core.logger.models import AttemptLog
14+
from kolibri.core.logger.models import ContentSessionLog
15+
from kolibri.core.logger.models import ContentSummaryLog
16+
from kolibri.core.tasks.main import job_storage
17+
18+
MODELS_TO_DELETE = [
19+
AttemptLog,
20+
ContentSessionLog,
21+
ContentSummaryLog,
22+
FacilityUser,
23+
FacilityDataset,
24+
HardDeletedModels,
25+
Certificate,
26+
DatabaseIDModel,
27+
Store,
28+
DevicePermissions,
29+
DeletedModels,
30+
DeviceSettings,
31+
DatabaseMaxCounter,
32+
]
33+
34+
35+
def deprovision(progress_update=None):
36+
with DisablePostDeleteSignal():
37+
for Model in MODELS_TO_DELETE:
38+
Model.objects.all().delete()
39+
if progress_update:
40+
progress_update(1)
41+
42+
# Clear all completed, failed or cancelled jobs
43+
job_storage.clear()
44+
45+
46+
def get_deprovision_progress_total():
47+
return len(MODELS_TO_DELETE)

kolibri/core/device/soud.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from kolibri.core.auth.constants.morango_sync import PROFILE_FACILITY_DATA
2020
from kolibri.core.auth.constants.morango_sync import ScopeDefinitions
21+
from kolibri.core.auth.models import Facility
2122
from kolibri.core.auth.models import FacilityDataset
2223
from kolibri.core.auth.models import FacilityUser
2324
from kolibri.core.device.models import SyncQueue
@@ -49,7 +50,10 @@ def __init__(self, user_id, instance_id):
4950

5051
@cached_property
5152
def user(self):
52-
return FacilityUser.objects.get(id=self.user_id)
53+
try:
54+
return FacilityUser.all_objects.get(id=self.user_id)
55+
except FacilityUser.DoesNotExist:
56+
return None
5357

5458
@property
5559
def has_sync_queue(self):
@@ -382,13 +386,18 @@ def execute_sync(context):
382386
baseurl=context.network_location.base_url,
383387
keep_alive=True,
384388
noninteractive=True,
389+
no_provision=True,
385390
)
386391

387392
if sync_session_id:
388393
command = "resumesync"
389394
kwargs["id"] = sync_session_id
390395
else:
391-
kwargs["facility"] = context.user.facility_id
396+
kwargs["facility"] = (
397+
context.user.facility_id
398+
if context.user
399+
else Facility.get_default_facility().id
400+
)
392401

393402
sync_queue.status = SyncQueueStatus.Syncing
394403
sync_queue.save()

kolibri/core/device/tasks.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from kolibri.core.auth.models import Facility
1111
from kolibri.core.auth.models import FacilityUser
1212
from kolibri.core.auth.serializers import FacilitySerializer
13+
from kolibri.core.auth.utils.deprovision import deprovision
1314
from kolibri.core.device.models import DevicePermissions
1415
from kolibri.core.device.models import OSUser
1516
from kolibri.core.device.serializers import DeviceSerializerMixin
@@ -20,6 +21,7 @@
2021
from kolibri.core.device.utils import valid_app_key_on_request
2122
from kolibri.core.tasks.decorators import register_task
2223
from kolibri.core.tasks.permissions import FirstProvisioning
24+
from kolibri.core.tasks.permissions import IsDeviceUnusable
2325
from kolibri.core.tasks.utils import get_current_job
2426
from kolibri.core.tasks.validation import JobValidator
2527
from kolibri.core.utils.token_generator import TokenGenerator
@@ -29,6 +31,7 @@
2931
logger = logging.getLogger(__name__)
3032

3133
PROVISION_TASK_QUEUE = "device_provision"
34+
DEPROVISION_TASK_QUEUE = "device_deprovision"
3235

3336

3437
class DeviceProvisionValidator(DeviceSerializerMixin, JobValidator):
@@ -243,3 +246,15 @@ def provisiondevice(**data): # noqa C901
243246
updates["auth_token"] = TokenGenerator().make_token(superuser.id)
244247

245248
job.update_metadata(**updates)
249+
250+
251+
@register_task(
252+
permission_classes=[IsDeviceUnusable],
253+
cancellable=False,
254+
queue=DEPROVISION_TASK_QUEUE,
255+
)
256+
def deprovisiondevice():
257+
"""
258+
Task for deprovisioning a device.
259+
"""
260+
deprovision()

kolibri/core/device/utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
APP_KEY_COOKIE_NAME = "app_key_cookie"
3030
APP_AUTH_TOKEN_COOKIE_NAME = "app_auth_token_cookie"
3131

32+
DEVICE_UNUSABLE_NO_SUPERUSERS = "NO_SUPERUSERS"
33+
DEVICE_UNUSABLE_SUPERUSERS_SOFT_DELETED = "SUPERUSERS_SOFT_DELETED"
34+
3235

3336
class DeviceNotProvisioned(Exception):
3437
pass
@@ -494,3 +497,22 @@ def is_full_facility_import(dataset_id):
494497
.filter(scope_definition_id=ScopeDefinitions.FULL_FACILITY)
495498
.exists()
496499
)
500+
501+
502+
def get_device_unusable_reason():
503+
from kolibri.core.auth.models import FacilityUser
504+
505+
is_soud = get_device_setting("subset_of_users_device")
506+
if not is_soud:
507+
return None
508+
509+
superadmins = FacilityUser.all_objects.filter(devicepermissions__is_superuser=True)
510+
if not superadmins.exists():
511+
return DEVICE_UNUSABLE_NO_SUPERUSERS
512+
513+
non_soft_deleted_superadmins = FacilityUser.objects.filter(
514+
devicepermissions__is_superuser=True
515+
)
516+
if not non_soft_deleted_superadmins.exists():
517+
return DEVICE_UNUSABLE_SUPERUSERS_SOFT_DELETED
518+
return None

kolibri/core/tasks/permissions.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,3 +197,13 @@ def user_can_run_job(self, user, job):
197197

198198
def user_can_read_job(self, user, job):
199199
return True
200+
201+
202+
class IsDeviceUnusable(BasePermission):
203+
def user_can_run_job(self, user, job):
204+
from kolibri.core.device.utils import get_device_unusable_reason
205+
206+
return get_device_unusable_reason() is not None
207+
208+
def user_can_read_job(self, user, job):
209+
return True

kolibri/core/views.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -128,23 +128,13 @@ def get(self, request):
128128
return RootURLRedirectView.as_view()(request)
129129

130130

131-
device_is_provisioned = False
132-
133-
134-
def is_provisioned():
135-
# First check if the device has been provisioned
136-
global device_is_provisioned
137-
device_is_provisioned = device_is_provisioned or device_provisioned()
138-
return device_is_provisioned
139-
140-
141131
class RootURLRedirectView(View):
142132
def get(self, request):
143133
"""
144134
Redirects user based on the highest role they have for which a redirect is defined.
145135
"""
146136
# If it has not been provisioned and we have something that can handle setup, redirect there.
147-
if not is_provisioned() and SetupHook.provision_url:
137+
if not device_provisioned() and SetupHook.provision_url:
148138
return redirect(SetupHook.provision_url())
149139

150140
if request.user.is_authenticated:

kolibri/plugins/user_auth/assets/src/constants.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,8 @@ export const ComponentMap = {
99
export const pageNameToModuleMap = {
1010
[ComponentMap.SIGN_IN]: 'signIn',
1111
};
12+
13+
export const DeviceUnusableReason = {
14+
NO_SUPERUSERS: 'NO_SUPERUSERS',
15+
SUPERUSERS_SOFT_DELETED: 'SUPERUSERS_SOFT_DELETED',
16+
};

kolibri/plugins/user_auth/assets/src/views/AuthBase.vue

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<div class="main-cell table-cell">
77
<!-- remote access disabled -->
88
<div
9-
v-if="!$store.getters.allowAccess"
9+
v-if="!$store.getters.allowAccess || deviceUnusableReason"
1010
class="box"
1111
:style="{ backgroundColor: $themeTokens.surface }"
1212
>
@@ -24,10 +24,16 @@
2424
>
2525
{{ logoText }}
2626
</h1>
27-
<p data-test="restrictedAccess">
28-
{{ $tr('restrictedAccess') }}
29-
</p>
30-
<p>{{ $tr('restrictedAccessDescription') }}</p>
27+
<template v-if="!$store.getters.allowAccess">
28+
<p data-test="restrictedAccess">
29+
{{ $tr('restrictedAccess') }}
30+
</p>
31+
<p>{{ $tr('restrictedAccessDescription') }}</p>
32+
</template>
33+
<DeviceUnusableMessage
34+
v-else
35+
:reason="deviceUnusableReason"
36+
/>
3137
</div>
3238
<!-- remote access enabled -->
3339
<div
@@ -193,10 +199,11 @@
193199
import LanguageSwitcherFooter from '../views/LanguageSwitcherFooter';
194200
import commonUserStrings from './commonUserStrings';
195201
import getUrlParameter from './getUrlParameter';
202+
import DeviceUnusableMessage from './DeviceUnusableMessage.vue';
196203
197204
export default {
198205
name: 'AuthBase',
199-
components: { CoreLogo, LanguageSwitcherFooter, PrivacyInfoModal },
206+
components: { CoreLogo, LanguageSwitcherFooter, PrivacyInfoModal, DeviceUnusableMessage },
200207
mixins: [commonCoreStrings, commonUserStrings],
201208
setup() {
202209
const { facilityConfig } = useFacilities();
@@ -273,6 +280,9 @@
273280
showGuestAccess() {
274281
return plugin_data.allowGuestAccess && !this.oidcProviderFlow;
275282
},
283+
deviceUnusableReason() {
284+
return plugin_data.deviceUnusableReason;
285+
},
276286
versionMsg() {
277287
return this.$tr('poweredBy', { version: __version });
278288
},

0 commit comments

Comments
 (0)