diff --git a/app/api/projects.py b/app/api/projects.py index 7cf279e0..1e5c9112 100644 --- a/app/api/projects.py +++ b/app/api/projects.py @@ -70,9 +70,9 @@ class ProjectViewSet(viewsets.ModelViewSet): result = [] - perms = get_users_with_perms(project, attach_perms=True) + perms = get_users_with_perms(project, attach_perms=True, with_group_users=False) for user in perms: - result.append({'user': user.username, + result.append({'username': user.username, 'owner': project.owner == user, 'permissions': normalized_perm_names(perms[user])}) diff --git a/app/api/urls.py b/app/api/urls.py index b5d6e7f7..767cffbd 100644 --- a/app/api/urls.py +++ b/app/api/urls.py @@ -12,12 +12,16 @@ from rest_framework_jwt.views import obtain_jwt_token from .tiler import TileJson, Bounds, Metadata, Tiles, Export from .potree import Scene, CameraView from .workers import CheckTask, GetTaskResult +from .users import UserViewSet +from webodm import settings router = routers.DefaultRouter() router.register(r'projects', ProjectViewSet) router.register(r'processingnodes', ProcessingNodeViewSet) router.register(r'presets', PresetViewSet, base_name='presets') +if settings.ENABLE_USERS_API: + router.register(r'users', UserViewSet, base_name='users') tasks_router = routers.NestedSimpleRouter(router, r'projects', lookup='project') tasks_router.register(r'tasks', TaskViewSet, base_name='projects-tasks') diff --git a/app/api/users.py b/app/api/users.py new file mode 100644 index 00000000..0de0f2db --- /dev/null +++ b/app/api/users.py @@ -0,0 +1,33 @@ +from django.contrib.auth.models import User +from rest_framework import serializers, viewsets, mixins, status, exceptions, permissions + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['username', 'email'] + +class UserViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): + serializer_class = UserSerializer + permission_classes = [permissions.IsAuthenticated] + + # Disable pagination when not requesting any page + def paginate_queryset(self, queryset): + if self.paginator and self.request.query_params.get(self.paginator.page_query_param, None) is None: + return None + return super().paginate_queryset(queryset) + + def get_queryset(self): + queryset = User.objects.all() + + search = self.request.query_params.get('search', None) + if search is not None: + queryset = queryset.filter(username__istartswith=search) | queryset.filter(email__istartswith=search) + + limit = self.request.query_params.get('limit', None) + if limit is not None: + try: + queryset = queryset[:abs(int(limit))] + except ValueError: + raise exceptions.ValidationError(detail="Invalid query parameters") + + return queryset \ No newline at end of file diff --git a/app/static/app/js/components/EditPermissionsPanel.jsx b/app/static/app/js/components/EditPermissionsPanel.jsx index 92d796f5..c4ccd395 100644 --- a/app/static/app/js/components/EditPermissionsPanel.jsx +++ b/app/static/app/js/components/EditPermissionsPanel.jsx @@ -25,7 +25,8 @@ class EditPermissionsPanel extends React.Component { error: "", loading: false, permissions: [], - validUsernames: [] + validUsernames: {}, + validatingUser: false }; this.backgroundFailedColor = Css.getValue('btn-danger', 'backgroundColor'); @@ -36,7 +37,8 @@ class EditPermissionsPanel extends React.Component { this.permsRequest = $.getJSON(`/api/projects/${this.props.projectId}/permissions/`, json => { - let validUsernames = json.map(p => p.user); + let validUsernames = {}; + json.forEach(p => validUsernames[p.username] = true); this.setState({validUsernames, permissions: json}); }) .fail(() => { @@ -47,12 +49,43 @@ class EditPermissionsPanel extends React.Component { }); } + validateUsername = (username) => { + if (this.validateReq){ + this.validateReq.abort(); + this.validateReq = null; + } + + if (this.validateTimeout){ + clearTimeout(this.validateTimeout); + this.validateTimeout = null; + } + + this.setState({validatingUser: true}); + + this.validateTimeout = setTimeout(() => { + this.validateReq = $.getJSON(`/api/users/?limit=30&search=${encodeURIComponent(username)}`) + .done((json) => { + json.forEach(u => { + this.state.validUsernames[u.username] = true; + }); + + this.setState({validUsernames: this.state.validUsernames}); + }).fail(() => { + + }).always(() => { + this.setState({validatingUser: false}); + }); + }, 300); + } + componentDidMount(){ if (!this.props.lazyLoad) this.loadPermissions(); } componentWillUnmount(){ if (this.permsRequest) this.permsRequest.abort(); + if (this.validateReq) this.validateReq.abort(); + if (this.validateTimeout) clearTimeout(this.validateTimeout); } handleChangePermissionRole = e => { @@ -61,7 +94,9 @@ class EditPermissionsPanel extends React.Component { handleChangePermissionUser = perm => { return e => { - perm.user = e.target.value; + perm.username = e.target.value; + + this.validateUsername(perm.username); // Update this.setState({permissions: this.state.permissions}); @@ -87,13 +122,13 @@ class EditPermissionsPanel extends React.Component { } getColorFor = (username) => { - if (this.state.validUsernames.indexOf(username) !== -1) return ""; + if (this.state.validatingUser || this.state.validUsernames[username]) return ""; else return this.backgroundFailedColor; } addNewPermission = () => { this.setState(update(this.state, { - permissions: {$push: [{user: "", permissions: ["view"]}]} + permissions: {$push: [{username: "", permissions: ["view"]}]} })); setTimeout(() => { @@ -110,18 +145,19 @@ class EditPermissionsPanel extends React.Component { } render() { - const permissions = this.state.permissions.map((p, i) =>
+ const permissions = this.state.permissions.map((p, i) =>
this.lastTextbox = domNode} />
@@ -133,7 +169,7 @@ class EditPermissionsPanel extends React.Component {
-
); + ); return (
diff --git a/webodm/settings.py b/webodm/settings.py index e9551635..f9674a19 100644 --- a/webodm/settings.py +++ b/webodm/settings.py @@ -69,6 +69,12 @@ SINGLE_USER_MODE = False # URL to redirect to if there are no processing nodes when visiting the dashboard PROCESSING_NODES_ONBOARDING = None +# Enable the /api/users endpoint which is used for autocompleting +# usernames when handling project permissions. This can be disabled +# for security reasons if you don't want to let authenticated users +# retrieve the user list. +ENABLE_USERS_API = True + # Enable desktop mode. In desktop mode some styling changes # are applied to make the application look nicer on desktop # as well as disabling certain features (e.g. sharing)