Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Consolidate list remote users api #12321

Merged
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
70 changes: 70 additions & 0 deletions kolibri/core/auth/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
from rest_framework import status
from rest_framework import views
from rest_framework import viewsets
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.exceptions import ValidationError as RestValidationError
from rest_framework.mixins import CreateModelMixin
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
Expand Down Expand Up @@ -73,23 +75,29 @@
from kolibri.core.api import ReadOnlyValuesViewset
from kolibri.core.api import ValuesViewset
from kolibri.core.api import ValuesViewsetOrderingFilter
from kolibri.core.auth.constants import user_kinds
from kolibri.core.auth.constants.demographics import NOT_SPECIFIED
from kolibri.core.auth.permissions.general import _user_is_admin_for_own_facility
from kolibri.core.auth.permissions.general import DenyAll
from kolibri.core.auth.utils.users import get_remote_users_info
from kolibri.core.device.permissions import IsSuperuser
from kolibri.core.device.utils import allow_guest_access
from kolibri.core.device.utils import allow_other_browsers_to_connect
from kolibri.core.device.utils import APP_AUTH_TOKEN_COOKIE_NAME
from kolibri.core.device.utils import is_full_facility_import
from kolibri.core.device.utils import valid_app_key_on_request
from kolibri.core.discovery.utils.network.client import NetworkClient
from kolibri.core.discovery.utils.network.errors import NetworkLocationResponseFailure
from kolibri.core.logger.models import UserSessionLog
from kolibri.core.mixins import BulkCreateMixin
from kolibri.core.mixins import BulkDeleteMixin
from kolibri.core.query import annotate_array_aggregate
from kolibri.core.query import SQCount
from kolibri.core.serializers import HexOnlyUUIDField
from kolibri.core.utils.pagination import ValuesViewsetPageNumberPagination
from kolibri.core.utils.urls import reverse_path
from kolibri.plugins.app.utils import interface
from kolibri.utils.urls import validator

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -1066,3 +1074,65 @@ def get_session_response(self, request):

response = Response(session)
return response


class RemoteFacilityUserViewset(views.APIView):
def get(self, request):
baseurl = request.query_params.get("baseurl", "")
try:
validator(baseurl)
except ValidationError as e:
raise RestValidationError(detail=str(e))
username = request.query_params.get("username", None)
facility = request.query_params.get("facility", None)
if username is None or facility is None:
raise RestValidationError(detail="Both username and facility are required")
client = NetworkClient.build_for_address(baseurl)
url = reverse_path("kolibri:core:publicsearchuser-list")
try:
response = client.get(
url, params={"facility": facility, "search": username}
)
return Response(response.json())
except NetworkLocationResponseFailure:
return Response({})
except Exception as e:
raise RestValidationError(detail=str(e))


class RemoteFacilityUserAuthenticatedViewset(views.APIView):
def post(self, request):
"""
If the request is done by an admin user it will return a list of the users of the
facility

:param baseurl: First part of the url of the server that's going to be requested
:param facility_id: Id of the facility to authenticate and get the list of users
:param username: Username of the user that's going to authenticate
:param password: Password of the user that's going to authenticate
:return: List of the users of the facility.
"""
baseurl = request.data.get("baseurl", "")
try:
validator(baseurl)
except ValidationError as e:
raise RestValidationError(detail=str(e))
username = request.data.get("username", None)
facility_id = request.data.get("facility_id", None)
password = request.data.get("password", None)
if username is None or facility_id is None:
raise RestValidationError(detail="Both username and facility are required")

try:
facility_info = get_remote_users_info(
baseurl, facility_id, username, password
)
except AuthenticationFailed:
raise PermissionDenied()

user_info = facility_info["user"]
roles = user_info["roles"]
admin_roles = (user_kinds.ADMIN, user_kinds.SUPERUSER)
if not any(role in roles for role in admin_roles):
return Response([user_info])
return Response(facility_info["users"])
12 changes: 12 additions & 0 deletions kolibri/core/auth/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from .api import IsPINValidView
from .api import LearnerGroupViewSet
from .api import MembershipViewSet
from .api import RemoteFacilityUserAuthenticatedViewset
from .api import RemoteFacilityUserViewset
from .api import RoleViewSet
from .api import SessionViewSet
from .api import SetNonSpecifiedPasswordView
Expand Down Expand Up @@ -55,5 +57,15 @@
IsPINValidView.as_view(),
name="ispinvalid",
),
re_path(
r"^remotefacilityuser$",
RemoteFacilityUserViewset.as_view(),
name="remotefacilityuser",
),
re_path(
r"^remotefacilityauthenticateduserinfo$",
RemoteFacilityUserAuthenticatedViewset.as_view(),
name="remotefacilityauthenticateduserinfo",
),
]
)
32 changes: 0 additions & 32 deletions kolibri/plugins/setup_wizard/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_protect
from rest_framework import decorators
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.exceptions import NotFound
from rest_framework.exceptions import PermissionDenied
from rest_framework.exceptions import ValidationError
Expand All @@ -14,7 +13,6 @@
from kolibri.core.auth.constants import user_kinds
from kolibri.core.auth.models import Facility
from kolibri.core.auth.models import FacilityUser
from kolibri.core.auth.utils.users import get_remote_users_info
from kolibri.core.device.models import DevicePermissions
from kolibri.core.discovery.utils.network.client import NetworkClient
from kolibri.core.discovery.utils.network.errors import NetworkLocationResponseFailure
Expand Down Expand Up @@ -145,33 +143,3 @@ def createsuperuser(self, request):

except ValidationError:
raise ValidationError(detail="duplicate", code="duplicate_username")

@decorators.action(methods=["post"], detail=False)
def listfacilitylearners(self, request):
"""
If the request is done by an admin user it will return a list of the users of the
facility

:param baseurl: First part of the url of the server that's going to be requested
:param facility_id: Id of the facility to authenticate and get the list of users
:param username: Username of the user that's going to authenticate
:param password: Password of the user that's going to authenticate
:return: List of the learners of the facility.
"""
facility_id = request.data.get("facility_id")
baseurl = request.data.get("baseurl")
password = request.data.get("password")
username = request.data.get("username")
try:
facility_info = get_remote_users_info(
baseurl, facility_id, username, password
)
except AuthenticationFailed:
raise PermissionDenied()
user_info = facility_info["user"]
roles = user_info["roles"]
admin_roles = (user_kinds.ADMIN, user_kinds.SUPERUSER)
if not any(role in roles for role in admin_roles):
raise PermissionDenied()
students = [u for u in facility_info["users"] if not u["roles"]]
return Response({"students": students, "admin": facility_info["user"]})
18 changes: 15 additions & 3 deletions kolibri/plugins/setup_wizard/assets/src/api.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import client from 'kolibri.client';
import urls from 'kolibri.urls';
import { Resource } from 'kolibri.lib.apiResource';

/**
Expand Down Expand Up @@ -41,9 +43,19 @@ export const FacilityImportResource = new Resource({
return response.data;
});
},
listfacilitylearners(params) {
return this.postListEndpoint('listfacilitylearners', params).then(response => {
return response.data;
async listfacilitylearners(params) {
const { data } = await client({
url: urls['kolibri:core:remotefacilityauthenticateduserinfo'](),
method: 'POST',
data: params,
});

const admin = data.find(user => user.username === params.username);
const students = data.filter(user => !user.roles || !user.roles.length);

return {
admin,
students,
};
},
});
12 changes: 0 additions & 12 deletions kolibri/plugins/user_profile/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,9 @@

from .viewsets import LoginMergedUserViewset
from .viewsets import OnMyOwnSetupViewset
from .viewsets import RemoteFacilityUserAuthenticatedViewset
from .viewsets import RemoteFacilityUserViewset

urlpatterns = [
re_path(r"onmyownsetup", OnMyOwnSetupViewset.as_view(), name="onmyownsetup"),
re_path(
r"remotefacilityuser",
RemoteFacilityUserViewset.as_view(),
name="remotefacilityuser",
),
re_path(
r"remotefacilityauthenticateduserinfo",
RemoteFacilityUserAuthenticatedViewset.as_view(),
name="remotefacilityauthenticateduserinfo",
),
re_path(
r"loginmergeduser",
LoginMergedUserViewset.as_view(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,22 @@ import urls from 'kolibri.urls';
function remoteFacilityUserData(baseurl, facility_id, username, password, userAdmin = null) {
const params = {
baseurl: baseurl,
facility: facility_id,
facility_id: facility_id,
username: userAdmin === null ? username : userAdmin,
password: password,
};
return client({
url: urls['kolibri:kolibri.plugins.user_profile:remotefacilityauthenticateduserinfo'](),
url: urls['kolibri:core:remotefacilityauthenticateduserinfo'](),
method: 'POST',
data: params,
}).then(response => {
if (response.data.error) {
return 'error';
} else {
})
.then(response => {
const user_info = response.data.find(element => element.username === username);
return user_info;
}
});
})
.catch(() => {
return 'error';
});
}

const remoteFacilityUsers = function (baseurl, facility_id, username) {
Expand All @@ -29,7 +29,7 @@ const remoteFacilityUsers = function (baseurl, facility_id, username) {
username: username,
};
return client({
url: urls['kolibri:kolibri.plugins.user_profile:remotefacilityuser'](),
url: urls['kolibri:core:remotefacilityuser'](),
params: params,
}).then(response => {
let users = response.data;
Expand Down
13 changes: 7 additions & 6 deletions kolibri/plugins/user_profile/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,15 @@ def start_soud_sync(user_id):

class IsSelf(BasePermission):
def user_can_run_job(self, user, job):
return user.id == job.kwargs.get(
"local_user_id", None
) or user.id == job.kwargs.get("remote_user_pk", None)
# args[1] is the local_user_id argument
return user.id == job.args[1] or user.id == job.kwargs.get(
"remote_user_pk", None
)

def user_can_read_job(self, user, job):
return user.id == job.kwargs.get(
"local_user_id", None
) or user.id == job.kwargs.get("remote_user_pk", None)
return user.id == job.args[1] or user.id == job.kwargs.get(
"remote_user_pk", None
)


@register_task(
Expand Down
61 changes: 0 additions & 61 deletions kolibri/plugins/user_profile/viewsets.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,9 @@
import requests
from django.contrib.auth import login
from django.core.exceptions import ValidationError as DjangoValidationError
from rest_framework.exceptions import ValidationError
from rest_framework.response import Response
from rest_framework.views import APIView

from .utils import TokenGenerator
from kolibri.core.auth.models import FacilityUser
from kolibri.core.discovery.utils.network.client import NetworkClient
from kolibri.core.discovery.utils.network.errors import NetworkLocationResponseFailure
from kolibri.core.utils.urls import reverse_path
from kolibri.utils.urls import validator


class OnMyOwnSetupViewset(APIView):
Expand All @@ -29,60 +22,6 @@ def get(self, request, format=None):
)


class RemoteFacilityUserViewset(APIView):
def get(self, request):
baseurl = request.query_params.get("baseurl", "")
try:
validator(baseurl)
except DjangoValidationError as e:
raise ValidationError(detail=str(e))
username = request.query_params.get("username", None)
facility = request.query_params.get("facility", None)
if username is None or facility is None:
raise ValidationError(detail="Both username and facility are required")
client = NetworkClient.build_for_address(baseurl)
url = reverse_path("kolibri:core:publicsearchuser-list")
try:
response = client.get(
url, params={"facility": facility, "search": username}
)
return Response(response.json())
except NetworkLocationResponseFailure:
return Response({})
except Exception as e:
raise ValidationError(detail=str(e))


class RemoteFacilityUserAuthenticatedViewset(APIView):
def post(self, request, *args, **kwargs):
baseurl = request.data.get("baseurl", "")
try:
validator(baseurl)
except DjangoValidationError as e:
raise ValidationError(detail=str(e))
username = request.data.get("username", None)
facility = request.data.get("facility", None)
password = request.data.get("password", None)
if username is None or facility is None:
raise ValidationError(detail="Both username and facility are required")
client = NetworkClient.build_for_address(baseurl)
url = reverse_path("kolibri:core:publicuser-list")
params = {"facility": facility, "search": username}

# adding facility so auth works when learners can login without password:
username = "username={}&facility={}".format(username, facility)

auth = requests.auth.HTTPBasicAuth(username, password)
try:
response = client.get(url, params=params, verify=False, auth=auth)
return Response(response.json())
except NetworkLocationResponseFailure as e:
response = e.response
return Response({"error": response.json()["detail"]})
except Exception as e:
raise ValidationError(detail=str(e))


class LoginMergedUserViewset(APIView):
"""
Viewset to login into kolibri using the merged user,
Expand Down