Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions kolibri/core/auth/management/commands/deprovision.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):

Expand Down
10 changes: 6 additions & 4 deletions kolibri/core/auth/management/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 47 additions & 0 deletions kolibri/core/auth/utils/deprovision.py
Original file line number Diff line number Diff line change
@@ -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)
13 changes: 11 additions & 2 deletions kolibri/core/device/soud.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Expand Down
15 changes: 15 additions & 0 deletions kolibri/core/device/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -29,6 +31,7 @@
logger = logging.getLogger(__name__)

PROVISION_TASK_QUEUE = "device_provision"
DEPROVISION_TASK_QUEUE = "device_deprovision"


class DeviceProvisionValidator(DeviceSerializerMixin, JobValidator):
Expand Down Expand Up @@ -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()
22 changes: 22 additions & 0 deletions kolibri/core/device/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
10 changes: 10 additions & 0 deletions kolibri/core/tasks/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
12 changes: 1 addition & 11 deletions kolibri/core/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions kolibri/plugins/user_auth/assets/src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
22 changes: 16 additions & 6 deletions kolibri/plugins/user_auth/assets/src/views/AuthBase.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
<div class="main-cell table-cell">
<!-- remote access disabled -->
<div
v-if="!$store.getters.allowAccess"
v-if="!$store.getters.allowAccess || deviceUnusableReason"
class="box"
:style="{ backgroundColor: $themeTokens.surface }"
>
Expand All @@ -24,10 +24,16 @@
>
{{ logoText }}
</h1>
<p data-test="restrictedAccess">
{{ $tr('restrictedAccess') }}
</p>
<p>{{ $tr('restrictedAccessDescription') }}</p>
<template v-if="!$store.getters.allowAccess">
<p data-test="restrictedAccess">
{{ $tr('restrictedAccess') }}
</p>
<p>{{ $tr('restrictedAccessDescription') }}</p>
</template>
<DeviceUnusableMessage
v-else
:reason="deviceUnusableReason"
/>
</div>
<!-- remote access enabled -->
<div
Expand Down Expand Up @@ -193,10 +199,11 @@
import LanguageSwitcherFooter from '../views/LanguageSwitcherFooter';
import commonUserStrings from './commonUserStrings';
import getUrlParameter from './getUrlParameter';
import DeviceUnusableMessage from './DeviceUnusableMessage.vue';

export default {
name: 'AuthBase',
components: { CoreLogo, LanguageSwitcherFooter, PrivacyInfoModal },
components: { CoreLogo, LanguageSwitcherFooter, PrivacyInfoModal, DeviceUnusableMessage },
mixins: [commonCoreStrings, commonUserStrings],
setup() {
const { facilityConfig } = useFacilities();
Expand Down Expand Up @@ -273,6 +280,9 @@
showGuestAccess() {
return plugin_data.allowGuestAccess && !this.oidcProviderFlow;
},
deviceUnusableReason() {
return plugin_data.deviceUnusableReason;
},
versionMsg() {
return this.$tr('poweredBy', { version: __version });
},
Expand Down
Loading