Merge pull request #1075 from pierotofy/perms

Project Permission Management
pull/1077/head
Piero Toffanin 2021-10-19 16:50:43 -04:00 zatwierdzone przez GitHub
commit 9e4c626d42
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
16 zmienionych plików z 673 dodań i 37 usunięć

Wyświetl plik

@ -10,7 +10,7 @@ class UserSerializer(serializers.ModelSerializer):
model = User
fields = '__all__'
class UserViewSet(viewsets.ModelViewSet):
class AdminUserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
permission_classes = [IsAdminUser]
@ -34,7 +34,7 @@ class GroupSerializer(serializers.ModelSerializer):
model = Group
fields = '__all__'
class GroupViewSet(viewsets.ModelViewSet):
class AdminGroupViewSet(viewsets.ModelViewSet):
serializer_class = GroupSerializer
permission_classes = [IsAdminUser]

Wyświetl plik

@ -1,14 +1,19 @@
from guardian.shortcuts import get_perms
from guardian.shortcuts import get_perms, get_users_with_perms, assign_perm, remove_perm
from rest_framework import serializers, viewsets
from rest_framework.decorators import detail_route
from rest_framework.response import Response
from rest_framework import status
from django.db import transaction
from django.contrib.auth.models import User
from app import models
from .tasks import TaskIDsSerializer
from .common import get_and_check_project
from django.utils.translation import gettext as _
def normalized_perm_names(perms):
return list(map(lambda p: p.replace("_project", ""),perms))
class ProjectSerializer(serializers.ModelSerializer):
tasks = TaskIDsSerializer(many=True, read_only=True)
owner = serializers.HiddenField(
@ -19,7 +24,7 @@ class ProjectSerializer(serializers.ModelSerializer):
def get_permissions(self, obj):
if 'request' in self.context:
return list(map(lambda p: p.replace("_project", ""), get_perms(self.context['request'].user, obj)))
return normalized_perm_names(get_perms(self.context['request'].user, obj))
else:
# Cannot list permissions, no user is associated with request (happens when serializing ui test mocks)
return []
@ -60,3 +65,74 @@ class ProjectViewSet(viewsets.ModelViewSet):
return Response({'success': True, 'project': ProjectSerializer(new_project).data}, status=status.HTTP_200_OK)
else:
return Response({'error': _("Cannot duplicate project")}, status=status.HTTP_200_OK)
@detail_route(methods=['get'])
def permissions(self, request, pk=None):
project = get_and_check_project(request, pk, ('change_project', ))
result = []
perms = get_users_with_perms(project, attach_perms=True, with_group_users=False)
for user in perms:
result.append({'username': user.username,
'owner': project.owner == user,
'permissions': normalized_perm_names(perms[user])})
result.sort(key=lambda r: r['owner'], reverse=True)
return Response(result, status=status.HTTP_200_OK)
@detail_route(methods=['post'])
def edit(self, request, pk=None):
project = get_and_check_project(request, pk, ('change_project', ))
try:
with transaction.atomic():
project.name = request.data.get('name', '')
project.description = request.data.get('description', '')
project.save()
form_perms = request.data.get('permissions')
if form_perms is not None:
# Build perms map (ignore owners, empty usernames)
perms_map = {}
for perm in form_perms:
if not perm.get('owner') and perm.get('username'):
perms_map[perm['username']] = perm['permissions']
db_perms = get_users_with_perms(project, attach_perms=True, with_group_users=False)
# Check users to remove
for user in db_perms:
# Never modify owner permissions
if project.owner == user:
continue
if perms_map.get(user.username) is None:
for p in db_perms[user]:
remove_perm(p, user, project)
# Check users to add/edit
for username in perms_map:
for p in ["add", "change", "delete", "view"]:
perm = p + "_project"
user = User.objects.get(username=username)
# Skip owners
if project.owner == user:
continue
# Has permission in database but not in form?
if user.has_perm(perm, project) and not p in perms_map[username]:
remove_perm(perm, user, project)
# Has permission in form but not in database?
elif p in perms_map[username] and not user.has_perm(perm, project):
assign_perm(perm, user, project)
except User.DoesNotExist as e:
return Response({'error': _("Invalid user in permissions list")}, status=status.HTTP_400_BAD_REQUEST)
except AttributeError as e:
return Response({'error': _("Invalid permissions")}, status=status.HTTP_400_BAD_REQUEST)
return Response({'success': True}, status=status.HTTP_200_OK)

Wyświetl plik

@ -192,6 +192,7 @@ class Metadata(TaskNestedView):
info['statistics'][b]['max'] = hrange[1]
cmap_labels = {
"viridis": "Viridis",
"jet": "Jet",
"terrain": "Terrain",
"gist_earth": "Earth",
@ -208,7 +209,7 @@ class Metadata(TaskNestedView):
colormaps = []
algorithms = []
if tile_type in ['dsm', 'dtm']:
colormaps = ['jet', 'terrain', 'gist_earth', 'pastel1']
colormaps = ['viridis', 'jet', 'terrain', 'gist_earth', 'pastel1']
elif formula and bands:
colormaps = ['rdylgn', 'spectral', 'rdylgn_r', 'spectral_r', 'rplumbo', 'discrete_ndvi',
'better_discrete_ndvi']

Wyświetl plik

@ -6,25 +6,26 @@ from .projects import ProjectViewSet
from .tasks import TaskViewSet, TaskDownloads, TaskAssets, TaskAssetsImport
from .imageuploads import Thumbnail, ImageDownload
from .processingnodes import ProcessingNodeViewSet, ProcessingNodeOptionsView
from .admin import UserViewSet, GroupViewSet
from .admin import AdminUserViewSet, AdminGroupViewSet
from rest_framework_nested import routers
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 UsersList
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')
tasks_router = routers.NestedSimpleRouter(router, r'projects', lookup='project')
tasks_router.register(r'tasks', TaskViewSet, base_name='projects-tasks')
admin_router = routers.DefaultRouter()
admin_router.register(r'admin/users', UserViewSet, basename='user')
admin_router.register(r'admin/groups', GroupViewSet, basename='group')
admin_router.register(r'admin/users', AdminUserViewSet, base_name='admin-users')
admin_router.register(r'admin/groups', AdminGroupViewSet, base_name='admin-groups')
urlpatterns = [
url(r'processingnodes/options/$', ProcessingNodeOptionsView.as_view()),
@ -57,3 +58,7 @@ urlpatterns = [
url(r'^plugins/(?P<plugin_name>[^/.]+)/(.*)$', api_view_handler)
]
if settings.ENABLE_USERS_API:
urlpatterns.append(url(r'users', UsersList.as_view()))

24
app/api/users.py 100644
Wyświetl plik

@ -0,0 +1,24 @@
from django.contrib.auth.models import User
from rest_framework.views import APIView
from rest_framework import exceptions, permissions, parsers
from rest_framework.response import Response
class UsersList(APIView):
permission_classes = (permissions.IsAuthenticated,)
parser_classes = (parsers.JSONParser, parsers.FormParser,)
def get(self, request):
qs = User.objects.all()
search = self.request.query_params.get('search', None)
if search is not None:
qs = qs.filter(username__istartswith=search) | qs.filter(email__istartswith=search)
limit = self.request.query_params.get('limit', None)
if limit is not None:
try:
qs = qs[:abs(int(limit))]
except ValueError:
raise exceptions.ValidationError(detail="Invalid query parameters")
return Response([{'username': u.username, 'email': u.email} for u in qs])

Wyświetl plik

@ -0,0 +1,264 @@
import '../css/EditPermissionsPanel.scss';
import React from 'react';
import ErrorMessage from './ErrorMessage';
import PropTypes from 'prop-types';
import $ from 'jquery';
import { _, interpolate } from '../classes/gettext';
import update from 'immutability-helper';
import Css from '../classes/Css';
class EditPermissionsPanel extends React.Component {
static defaultProps = {
projectId: -1,
lazyLoad: true
};
static propTypes = {
projectId: PropTypes.number.isRequired,
lazyLoad: PropTypes.bool
};
constructor(props){
super(props);
this.state = {
error: "",
loading: false,
permissions: [],
validUsernames: {},
validatingUser: false,
validationUnavailable: false
};
this.backgroundFailedColor = Css.getValue('btn-danger', 'backgroundColor');
this.autocompleteBorderColor = Css.getValue('btn-default', 'backgroundColor');
this.backgroundColor = Css.getValue('theme-secondary', 'backgroundColor');
this.highlightColor = Css.getValue('theme-background-highlight', 'backgroundColor');
}
loadPermissions = () => {
this.setState({loading: true, permissions: []});
this.permsRequest =
$.getJSON(`/api/projects/${this.props.projectId}/permissions/`, json => {
let validUsernames = {};
json.forEach(p => validUsernames[p.username] = true);
this.setState({validUsernames, permissions: json});
})
.fail(() => {
this.setState({error: _("Cannot load permissions.")});
})
.always(() => {
this.setState({loading: false});
});
}
getPermissions = () => {
// Cleanup temporary objects then return
this.state.permissions.forEach(perm => delete(perm.autocomplete));
return this.state.permissions;
}
autocomplete = (perm) => {
if (this.validateReq){
this.validateReq.abort();
this.validateReq = null;
}
if (this.validateTimeout){
clearTimeout(this.validateTimeout);
this.validateTimeout = null;
}
// Empty case
if (perm.username === ""){
delete(perm.autocomplete);
this.setState({permissions: this.state.permissions});
return;
}
this.setState({validatingUser: true});
this.validateTimeout = setTimeout(() => {
this.validateReq = $.getJSON(`/api/users/?limit=30&search=${encodeURIComponent(perm.username)}`)
.done((json) => {
json.forEach(u => {
this.state.validUsernames[u.username] = true;
});
this.state.permissions.forEach(p => delete(p.autocomplete));
if (this.textFocused) perm.autocomplete = json;
this.setState({validUsernames: this.state.validUsernames, permissions: this.state.permissions});
}).fail(jqXHR => {
// Perhaps the user API is not enabled
if (jqXHR.statusText !== "abort"){
this.setState({validationUnavailable: true});
}
}).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 = perm => {
return e => {
perm.permissions = this.extendedPermissions(e.target.value);
this.setState({permissions: this.state.permissions});
}
}
handleChangePermissionUser = perm => {
return e => {
perm.username = e.target.value;
this.autocomplete(perm);
// Update
this.setState({permissions: this.state.permissions});
};
}
simplifiedPermission = perms => {
// We simplify WebODM's internal permission model into
// a simpler read or read/write model.
if (perms.indexOf("change") !== -1) return "rw";
else if (perms.indexOf("view") !== -1) return "r";
else return "";
}
extendedPermissions = simPerm => {
if (simPerm == "rw"){
return ["add", "change", "delete", "view"];
}else if (simPerm == "r"){
return ["view"];
}else return [];
}
permissionLabel = simPerm => {
if (simPerm === "rw") return _("Read/Write");
else if (simPerm === "r") return _("Read");
else if (simPerm === "") return _("No Access");
}
allPermissions = () => {
return ["rw", "r"].map(p => { return {key: p, label: this.permissionLabel(p)}});
}
getColorFor = (username) => {
if (this.state.validationUnavailable || this.state.validatingUser || this.state.validUsernames[username]) return "";
else return this.backgroundFailedColor;
}
addNewPermission = () => {
this.setState(update(this.state, {
permissions: {$push: [{username: "", permissions: ["view"]}]}
}));
setTimeout(() => {
if (this.lastTextbox) this.lastTextbox.focus();
}, 0);
}
handleDeletePermission = perm => {
return () => {
this.setState(update(this.state, {
permissions: arr => arr.filter(p => p !== perm)
}));
}
}
acOnMouseEnter = e => {
e.target.style.backgroundColor = this.highlightColor;
}
acOnMouseLeave = e => {
e.target.style.backgroundColor = "";
}
acOnClick = (perm, acEntry) => {
return e => {
perm.username = acEntry.username;
delete(perm.autocomplete);
this.setState({permissions: this.state.permissions});
}
}
onFocus = e => {
this.textFocused = true;
}
onBlur = perm => {
return e => {
delete(perm.autocomplete);
this.setState({permissions: this.state.permissions});
this.textFocused = false;
}
}
render() {
const permissions = this.state.permissions.map((p, i) => <form autoComplete="off" key={i}>
<div className="permission">
<div className="username-container">
<i className="fa fa-user user-indicator"/>
<input
style={{color: this.getColorFor(p.username)}}
onChange={this.handleChangePermissionUser(p)}
type="text"
autoComplete="off"
onFocus={this.onFocus}
onBlur={this.onBlur(p)}
disabled={p.owner}
value={p.username}
className="form-control username"
placeholder={_("Username")}
ref={(domNode) => this.lastTextbox = domNode} />
{p.autocomplete && p.autocomplete.length > 0 ? <div className="autocomplete" style={{borderColor: this.autocompleteBorderColor, backgroundColor: this.backgroundColor}}>
{p.autocomplete.map(ac => <div key={ac.username} onMouseDown={this.acOnClick(p, ac)} className="ac-entry" onMouseEnter={this.acOnMouseEnter} onMouseLeave={this.acOnMouseLeave} style={{borderColor: this.autocompleteBorderColor}}>
<div className="ac-user">{ac.username}</div>
<div className="ac-email">{ac.email}</div>
</div>)}
</div> : ""}
</div>
<div className="remove">
{!p.owner ? <a onClick={this.handleDeletePermission(p)}><i className="fa fa-times"></i></a> : ""}
</div>
<div className="role-container">
<select disabled={p.owner} className="form-control" value={this.simplifiedPermission(p.permissions)} onChange={this.handleChangePermissionRole(p)}>
{this.allPermissions().map(p => <option key={p.key} value={p.key}>{p.label}</option>)}
</select>
</div>
</div>
</form>);
return (
<div className="edit-permissions-panel">
<div className="form-group">
<label className="col-sm-2 control-label">{_("Permissions")}</label>
<div className="col-sm-10">
<ErrorMessage bind={[this, 'error']} />
{this.state.loading ?
<i className="fa fa-circle-notch fa-spin fa-fw perms-loading"></i>
: [permissions, <div key="add-new">
<button onClick={this.addNewPermission} className="btn btn-default btn-sm add-new"><i className="fa fa-user-plus"></i></button>
</div>]}
</div>
</div>
</div>
);
}
}
export default EditPermissionsPanel;

Wyświetl plik

@ -2,6 +2,7 @@ import React from 'react';
import FormDialog from './FormDialog';
import PropTypes from 'prop-types';
import ErrorMessage from './ErrorMessage';
import EditPermissionsPanel from './EditPermissionsPanel';
import { _ } from '../classes/gettext';
class EditProjectDialog extends React.Component {
@ -16,6 +17,7 @@ class EditProjectDialog extends React.Component {
deleteWarning: _("All tasks, images and models associated with this project will be permanently deleted. Are you sure you want to continue?"),
show: false,
showDuplicate: false,
showPermissions: false,
onDuplicated: () => {}
};
@ -33,6 +35,7 @@ class EditProjectDialog extends React.Component {
deleteWarning: PropTypes.string,
show: PropTypes.bool,
showDuplicate: PropTypes.bool,
showPermissions: PropTypes.bool,
onDuplicated: PropTypes.func
};
@ -62,13 +65,20 @@ class EditProjectDialog extends React.Component {
}
getFormData(){
return {
const res = {
name: this.state.name,
descr: this.state.descr,
};
if (this.editPermissionsPanel){
res.permissions = this.editPermissionsPanel.getPermissions();
}
return res;
}
onShow(){
this.editPermissionsPanel.loadPermissions();
this.nameInput.focus();
}
@ -123,7 +133,7 @@ class EditProjectDialog extends React.Component {
getFormData={this.getFormData}
reset={this.reset}
onShow={this.onShow}
leftButtons={this.props.showDuplicate ? [<button disabled={this.duplicating} onClick={this.handleDuplicate} className="btn btn-default"><i className={"fa " + (this.state.duplicating ? "fa-circle-notch fa-spin fa-fw" : "fa-copy")}></i> Duplicate</button>] : undefined}
leftButtons={this.props.showDuplicate ? [<button key="duplicate" disabled={this.duplicating} onClick={this.handleDuplicate} className="btn btn-default"><i className={"fa " + (this.state.duplicating ? "fa-circle-notch fa-spin fa-fw" : "fa-copy")}></i> Duplicate</button>] : undefined}
ref={(domNode) => { this.dialog = domNode; }}>
<ErrorMessage bind={[this, "error"]} />
<div className="form-group">
@ -138,6 +148,12 @@ class EditProjectDialog extends React.Component {
<textarea className="form-control" rows="3" value={this.state.descr} onChange={this.handleChange('descr')} />
</div>
</div>
{this.props.showPermissions ?
<EditPermissionsPanel
projectId={this.props.projectId}
lazyLoad={true}
ref={(domNode) => { this.editPermissionsPanel = domNode; }} />
: ""}
</FormDialog>
);
}

Wyświetl plik

@ -112,7 +112,7 @@ class FormDialog extends React.Component {
this.serverRequest = this.props.saveAction(formData);
if (this.serverRequest){
this.serverRequest.fail(e => {
this.setState({error: e.message || (e.responseJSON || {}).detail || e.responseText || _("Could not apply changes")});
this.setState({error: e.message || (e.responseJSON || {}).detail || (e.responseJSON || {}).error || e.responseText || _("Could not apply changes")});
}).always(() => {
this.setState({saving: false});
this.serverRequest = null;
@ -144,7 +144,8 @@ class FormDialog extends React.Component {
if (this.props.deleteAction){
leftButtons.push(<button
disabled={this.state.deleting}
className="btn btn-danger"
className="btn btn-danger"
key="delete"
onClick={this.handleDelete}>
{this.state.deleting ?
<span>

Wyświetl plik

@ -126,7 +126,7 @@ class Map extends React.Component {
let metaUrl = url + "metadata";
if (type == "plant") metaUrl += "?formula=NDVI&bands=RGN&color_map=rdylgn";
if (type == "dsm" || type == "dtm") metaUrl += "?hillshade=6&color_map=jet";
if (type == "dsm" || type == "dtm") metaUrl += "?hillshade=6&color_map=viridis";
this.tileJsonRequests.push($.getJSON(metaUrl)
.done(mres => {

Wyświetl plik

@ -378,14 +378,15 @@ class ProjectListItem extends React.Component {
updateProject(project){
return $.ajax({
url: `/api/projects/${this.state.data.id}/`,
url: `/api/projects/${this.state.data.id}/edit/`,
contentType: 'application/json',
data: JSON.stringify({
name: project.name,
description: project.descr,
permissions: project.permissions
}),
dataType: 'json',
type: 'PATCH'
type: 'POST'
}).done(() => {
this.refresh();
});
@ -469,27 +470,31 @@ class ProjectListItem extends React.Component {
render() {
const { refreshing, data } = this.state;
const numTasks = data.tasks.length;
const canEdit = this.hasPermission("change");
return (
<li className={"project-list-item list-group-item " + (refreshing ? "refreshing" : "")}
href="javascript:void(0);"
ref={this.setRef("dropzone")}
>
<EditProjectDialog
ref={(domNode) => { this.editProjectDialog = domNode; }}
title={_("Edit Project")}
saveLabel={_("Save Changes")}
savingLabel={_("Saving changes...")}
saveIcon="far fa-edit"
showDuplicate={true}
onDuplicated={this.props.onProjectDuplicated}
projectName={data.name}
projectDescr={data.description}
projectId={data.id}
saveAction={this.updateProject}
deleteAction={this.hasPermission("delete") ? this.handleDelete : undefined}
/>
{canEdit ?
<EditProjectDialog
ref={(domNode) => { this.editProjectDialog = domNode; }}
title={_("Edit Project")}
saveLabel={_("Save Changes")}
savingLabel={_("Saving changes...")}
saveIcon="far fa-edit"
showDuplicate={true}
onDuplicated={this.props.onProjectDuplicated}
projectName={data.name}
projectDescr={data.description}
projectId={data.id}
saveAction={this.updateProject}
showPermissions={this.hasPermission("change")}
deleteAction={this.hasPermission("delete") ? this.handleDelete : undefined}
/>
: ""}
<div className="row no-margin">
<ErrorMessage bind={[this, 'error']} />
@ -541,9 +546,11 @@ class ProjectListItem extends React.Component {
</span>
: ""}
<i className='far fa-edit'>
</i> <a href="javascript:void(0);" onClick={this.handleEditProject}> {_("Edit")}
</a>
{canEdit ?
[<i key="edit-icon" className='far fa-edit'>
</i>,<a key="edit-text" href="javascript:void(0);" onClick={this.handleEditProject}> {_("Edit")}
</a>]
: ""}
</div>
</div>
<i className="drag-drop-icon fa fa-inbox"></i>

Wyświetl plik

@ -640,7 +640,7 @@ class TaskListItem extends React.Component {
if (task.last_error){
statusLabel = getStatusLabel(task.last_error, 'error');
}else if (!task.processing_node && !imported){
}else if (!task.processing_node && !imported && this.props.hasPermission("change")){
statusLabel = getStatusLabel(_("Set a processing node"));
statusIcon = "fa fa-hourglass-3";
showEditLink = true;
@ -679,7 +679,7 @@ class TaskListItem extends React.Component {
}
// Ability to change options
if (editable || (!task.processing_node)){
if (editable || (!task.processing_node && this.props.hasPermission("change"))){
taskActions.push(<li key="edit"><a href="javascript:void(0)" onClick={this.startEditing}><i className="glyphicon glyphicon-pencil"></i>{_("Edit")}</a></li>);
}

Wyświetl plik

@ -0,0 +1,77 @@
.edit-permissions-panel{
.perms-loading{
margin-top: 14px;
}
.permission{
position: relative;
margin-bottom: 4px;
display: flex;
}
.user-indicator{
position: absolute;
left: 12px;
top: 16px;
}
.remove{
position: relative;
top: 12px;
a{
position: absolute;
left: -36px;
top: -12px;
padding: 12px;
&:hover{
cursor: pointer;
}
}
}
.username{
padding-left: 30px;
}
.username-container{
flex-grow: 0.66;
position: relative;
.autocomplete{
left: 0;
top: 44px;
z-index: 2;
border-width: 1px;
border-style: solid;
width: 100%;
position: absolute;
border-radius: 2px;
max-height: 240px;
overflow-y: auto;
overflow-x: hidden;
.ac-entry{
padding: 10px 15px 10px 15px;
border-bottom-style: solid;
border-bottom-width: 1px;
&:hover{
cursor: pointer;
}
}
.ac-entry:last-child{
border: none;
}
.ac-email{
opacity: 0.8;
}
}
}
.role-container{
flex-grow: 0.33;
}
.add-new{
margin-top: 4px;
}
}

Wyświetl plik

@ -0,0 +1,107 @@
import logging
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APIClient
from app.models import Project
from .classes import BootTestCase
from guardian.shortcuts import get_perms
from webodm import settings
logger = logging.getLogger('app.logger')
class TestApiProjects(BootTestCase):
def setUp(self):
super().setUp()
def tearDown(self):
super().tearDown()
def test_project(self):
client = APIClient()
user = User.objects.get(username="testuser")
project = Project.objects.create(
owner=user,
name="test project"
)
# Cannot edit project (anonymous)
res = client.post("/api/projects/{}/edit/".format(project.id), {
'name': 'edited'
})
self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
client.login(username="testuser", password="test1234")
# Can edit project
res = client.post("/api/projects/{}/edit/".format(project.id), {
'name': 'edited'
})
self.assertEqual(res.status_code, status.HTTP_200_OK)
project.refresh_from_db()
self.assertEqual(project.name, 'edited')
self.assertEqual(project.description, '')
other_user = User.objects.get(username="testuser2")
other_client = APIClient()
other_client.login(username="testuser2", password="test1234")
# Other user cannot edit project
res = other_client.post("/api/projects/{}/edit/".format(project.id), {
'name': 'edited2'
})
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
# Other user cannot see project
res = other_client.get("/api/projects/{}/".format(project.id))
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
# Change permissions via API
res = client.post("/api/projects/{}/edit/".format(project.id), {
'permissions': [{'username': 'testuser2', 'permissions': ['view']}]
}, format="json")
self.assertEqual(res.status_code, status.HTTP_200_OK)
# Other user can see project
res = other_client.get("/api/projects/{}/".format(project.id))
self.assertEqual(res.status_code, status.HTTP_200_OK)
# Other user still cannot edit project
res = other_client.post("/api/projects/{}/edit/".format(project.id), {
'name': 'edited2'
})
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
# Change permissions again
res = client.post("/api/projects/{}/edit/".format(project.id), {
'permissions': [{'username': 'testuser2', 'permissions': ['view', 'add', 'change', 'delete']}]
}, format="json")
self.assertEqual(res.status_code, status.HTTP_200_OK)
# Other user can now edit
res = other_client.post("/api/projects/{}/edit/".format(project.id), {
'name': 'edited3'
})
self.assertEqual(res.status_code, status.HTTP_200_OK)
project.refresh_from_db()
self.assertEqual(project.name, 'edited3')
# Can remove permissions
res = client.post("/api/projects/{}/edit/".format(project.id), {
'permissions': []
}, format="json")
self.assertEqual(res.status_code, status.HTTP_200_OK)
# Other user cannot see project
res = other_client.get("/api/projects/{}/".format(project.id))
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
# Current user (owner) still has permissions
res = client.get("/api/projects/{}/".format(project.id))
self.assertEqual(res.status_code, status.HTTP_200_OK)
perms = get_perms(user, project)
self.assertEqual(len(perms), 4)

Wyświetl plik

@ -0,0 +1,52 @@
import logging
from django.contrib.auth.models import User
from rest_framework import status
from rest_framework.test import APIClient
from app.models import Project
from .classes import BootTestCase
from webodm import settings
logger = logging.getLogger('app.logger')
class TestApiUsers(BootTestCase):
def setUp(self):
super().setUp()
def tearDown(self):
super().tearDown()
def test_users(self):
client = APIClient()
user = User.objects.get(username="testuser")
# Cannot list users (anonymous)
res = client.get("/api/users/?limit=30")
self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
client.login(username="testuser", password="test1234")
# Can list users (authenticated)
res = client.get("/api/users/?limit=30")
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertTrue([u for u in res.data if u['username'] == 'testuser'])
self.assertTrue([u for u in res.data if u['username'] == 'testsuperuser'])
# Can search for users
res = client.get("/api/users/?search=super")
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertFalse([u for u in res.data if u['username'] == 'testuser'])
self.assertTrue([u for u in res.data if u['username'] == 'testsuperuser'])
# Can search for users and limit
res = client.get("/api/users/?search=super&limit=1")
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertFalse([u for u in res.data if u['username'] == 'testuser'])
self.assertTrue([u for u in res.data if u['username'] == 'testsuperuser'])
# Handle invalid limits
res = client.get("/api/users/?search=super&limit=-1")
self.assertEqual(res.status_code, status.HTTP_200_OK)
self.assertEqual(len(res.data), 1)

Wyświetl plik

@ -1,6 +1,6 @@
{
"name": "WebODM",
"version": "1.9.9",
"version": "1.9.10",
"description": "User-friendly, extendable application and API for processing aerial imagery.",
"main": "index.js",
"scripts": {

Wyświetl plik

@ -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)