Commit 604e194c authored by Benjamin Fischer's avatar Benjamin Fischer
Browse files

vispa/usermanagement: more updates for controller/UI

- handle various join-procedures (request/password)
- implemented group-group membership (parent direction, including joining)
- a lot of smaller fixes
parent 4978f4f1
......@@ -36,6 +36,7 @@ class Rights(int):
"member",
"manager",
"admin",
"forbidden"
]
@staticmethod
......@@ -90,6 +91,13 @@ class Rights(int):
for i, name in enumerate(Rights.levels):
setattr(Rights, name, Rights(i))
class PermissionException(AjaxException):
def __init__(self, obj):
super(PermissionException, self).__init__(
"Insufficient permissions to access %s \"%s\" with this operation"
% (obj.__class__.__name__.lower(), obj.name), 403
)
class BaseController(object):
def __init__(self, table, rights={}, assocs=[]):
self._table = table
......@@ -113,10 +121,13 @@ class BaseController(object):
for tar, name, right in izip(tars, names, izip(*rights)):
obj = tar._table.get_by_name(DB(), name)
if obj is None: # TODO: this is a info leak (about existence)
raise AjaxException(404)
raise AjaxException(
"There is no %s \"%s\""
% (tar._table.__name__.lower(), name), 404
)
good = [g and Rights(r).test(obj) for g, r in zip(good, right)]
if True not in good:
raise AjaxException(403)
raise PermissionException(obj)
objs.append(obj)
return objs
......@@ -226,23 +237,31 @@ class AssociationToGroupController(AssociationController):
def add(self, name, assoc_name, password=""):
try:
base, target = self._rga("add", name, assoc_name)
func = getattr(base, "add_%s" % self._assoc)
func(target, confirmed=0)
except AjaxException as e:
if e.code != 403:
raise e
# now try special cases
conf = True
except PermissionException:
base, target = self._rga("add_self", name, assoc_name)
func = getattr(base, "add_%s" % self._assoc)
conft = Group_User_Assoc if self._table.__name__ == "User" else Group_Group_Assoc
if base.privacy == Group.PUBLIC:
func(target, conft.CONFIRMED)
elif base.privacy == Group.PROTECTED:
conf = True
elif base.password and password:
if not sha256_crypt.verify(password, base.password):
raise AjaxException(403)
func(target, conft.CONFIRMED)
elif base.privacy == Group.PRIVATE:
func(target, conft.UNCONFIRMED)
conf = True
elif base.privacy == Group.PROTECTED:
conf = False
else:
raise PermissionException(base)
func = getattr(base, "add_%s" % self._assoc)
conft = Group_User_Assoc if self._table.__name__ == "User" else Group_Group_Assoc
try:
func(target, conft.CONFIRMED if conf else conft.UNCONFIRMED)
except Exception as e:
if not conf:
raise
if "already in group" not in e.message: # TODO: this is fugly
raise
func = getattr(base, "confirm_%s" % self._assoc)
func(target)
@cherrypy.expose
def confirm(self, name, assoc_name):
......@@ -324,14 +343,18 @@ class UMAjaxController(AbstractController):
])
self.group = TableController(table=Group, rights={
"list": (Rights.public,),
"info": (Rights.member,),
"list": (Rights.protected,),
"info": (Rights.protected,),
"create": (Rights.admin,),
"delete": (Rights.admin,),
"update_status": (Rights.admin,),
"__default__": (Rights.manager,),
}, assocs=[
AssociationController(table=User, assoc="manager", rights=rights_manager),
AssociationToGroupController(table=Group, assoc="parent_group", rights={
"list": (Rights.member,),
"__default__": (Rights.forbidden,),
}),
] + [
AssociationToGroupController(table=t, assoc=a, rights=Rights.extend(rights_assoc, {
"list": (Rights.public,),
......@@ -340,7 +363,7 @@ class UMAjaxController(AbstractController):
"add_self": (Rights.none, Rights.manager),
})) for t, a in [
(User, "user"),
(Group, "child_group")
(Group, "child_group"),
]
], setters=[
"privacy",
......
......@@ -15,8 +15,9 @@
.vispa-mainview.core-usermanagementview .vispa-view-nav>.btn-group>.btn{width:50%}
.vispa-mainview.core-usermanagementview .vispa-view-content .input-group{margin-bottom:.25em}
.vispa-mainview.core-usermanagementview .vispa-view-content .vispa-fix-form>button.form-control{border-radius:0 4px 4px 0}
.vispa-mainview.core-usermanagementview .vispa-view-content .item-display .item-list .dropdown-menu{right:0;left:auto}
.vispa-mainview.core-usermanagementview .vispa-view-content .item-list .dropdown-menu{right:0;left:auto}
.vispa-mainview.core-usermanagementview .vispa-view-content .item-list .list-group-item.can-browse{cursor:pointer}
.vispa-mainview.core-usermanagementview .vispa-view-content .label+.label{margin-left:.3em}
.workspace-add-dialog>.modal-body .input-group,.workspace-add-dialog>.modal-body h4{margin-bottom:.3em}
.workspace-add-dialog>.modal-body>.input-group>.input-group-addon{min-width:5em}
.workspace-add-dialog>.modal-body>div>.input-group>.input-group-addon{min-width:7em}
......
......@@ -70,12 +70,15 @@
.vispa-fix-form > button.form-control {
border-radius: 0px 4px 4px 0px;
}
.item-display .item-list .dropdown-menu {
right: 0;
left: auto;
.item-list .dropdown-menu {
right: 0;
left: auto;
}
.item-list .list-group-item.can-browse {
cursor: pointer;
cursor: pointer;
}
.label + .label {
margin-left: 0.3em;
}
}
}
......
<button
v-if="info.member == 1"
class="btn btn-xs btn-warning disabled"
disabled
>
<i class="fa fa-question"></i>
Unconfirmed
</button>
<button
v-if="info.member > 0 && $root.isAdmin"
class="btn btn-xs btn-success"
@click="join"
>
<i class="fa fa-user-plus"></i>
Join (admin)
</button>
<button
v-if="info.member == 2 && info.privacy < 2 && !$root.isAdmin"
class="btn btn-xs btn-success"
@click="join"
>
<i class="fa fa-user-plus"></i>
{{ "Join" + (info.privacy ? " (request)" : "") }}
</button>
<button
v-if="info.member > 0 && info.password && !$root.isAdmin"
class="btn btn-xs btn-success"
@click="password"
>
<i class="fa fa-user-plus"></i>
Join (password)
</button>
<button
v-if="info.member < 2"
class="btn btn-xs btn-danger"
@click="leave"
>
<i class="fa fa-user-times"></i>
Leave
</button>
......@@ -2,12 +2,43 @@
<partial name="header"></partial>
<div v-if="info">
<h4>Created: {{created}}</h4>
<div v-if="managing">
<h4>Privacy</h4>
<vs-group :value.sync="privacy">
<vs-radio :value="0">public</vs-radio>
<vs-radio :value="1">protected</vs-radio>
<vs-radio :value="2">private</vs-radio>
<div>
<h4>Membership:
<span
:class="{
'label': true,
'label-success': info.member == 0,
'label-warning': info.member == 1,
'label-danger': info.member == 2,
}"
>
{{ memberOptions[info.member] }}
</span>
<span
v-if="info.manager"
class="label label-primary"
>
manager
</span>
<div class="btn-group pull-right">
<group-control :info="info"></group-control>
</div>
</h4>
</div>
<div>
<h4>{{
"Privacy" + (managing ? "" : ": " + privacyOptions[info.privacy])
}}</h4>
<vs-group
v-if="managing"
:value.sync="privacy"
>
<vs-radio
v-for="(value, label) in privacyOptions"
:value="value"
>
{{label}}
</vs-radio>
</vs-group>
</div>
<div v-if="isAdmin">
......@@ -18,11 +49,13 @@
<vs-radio :value="2">deleted</vs-radio>
</vs-group>
</div>
<div v-if="managing">
<h4>Password</h4>
<div class="input-group">
<div>
<h4>{{
"Password" + (managing ? "" : ": " + passwordStatus)
}}</h4>
<div v-if="managing" class="input-group">
<span class="input-group-addon">
{{ info.password ? "active" : "not used" }}
{{ passwordStatus }}
</span>
<input
type="password"
......@@ -47,23 +80,40 @@
</div>
</div>
<name-list
v-if="managing"
name="manager"
title="Managers"
url="group/manager"
:can-add="managing"
:can-mod-item="managing"
></name-list>
<member-list
v-if="canViewMembers"
name="user"
title="Users"
url="group/user"
:can-add="managing"
:can-mod-item="managing"
></member-list>
<member-list
v-if="canViewMembers"
name="group"
title="Child Groups"
url="group/child_group"
:can-add="managing"
:can-mod-item="managing"
:can-browse="true"
suggestion-id="um-suggestion-group"
></member-list>
<component
:is="managing ? 'parent-list' : 'member-list'"
v-if="canViewMembers"
name="group"
title="Parent Groups"
url="group/parent_group"
:can-add="false"
:can-mod-item="false"
:can-browse="true"
suggestion-id="um-suggestion-group"
></component>
</div>
<li class="list-group-item">
<span>
{{info.name}}
<span
v-if="info.manager"
class="label label-primary"
>
Manager
</span>
<span
v-if="info.privacy !== undefined"
:class="{
'label': true,
'label-success': info.privacy === 0,
'label-warning': info.privacy === 1,
'label-danger': info.privacy === 2,
}">
{{ ["Public","Protected","Private"][info.privacy] }}
</span>
<span
v-if="info.password"
class="label label-info">
has password
</span>
<div class="btn-group pull-right" @click.stop>
<group-control
:info="info"
></group-control>
</div>
</span>
</li>
<li :class="{
'list-group-item' : true,
'list-group-item': true,
'list-group-item-warning': !info.confirmed
}">
{{info.name}}
......
<div class="item-list">
<h2>
{{title | capitalize}}
{{title}}
<i
v-show="refreshing"
class="fa fa-refresh fa-spin"
......@@ -30,29 +30,33 @@
@click="toggle"
>
<i :class="(targetExists ? 'fa-minus' : 'fa-plus') | iconPrefix"></i>
{{targetExists ? 'Remove' : 'Add'}}</button>
{{ targetExists ? 'Remove' : 'Add' }}</button>
</span>
</div>
<div class="input-group" >
<span class="input-group-addon">Filter</span>
<item-filter
:name="name"
:can-add="canAdd"
v-ref:filter
></item-filter>
</div>
<ul class="list-group">
<item
v-for="info in filtered | orderBy 'name'"
:url="itemUrl"
:info="info"
:name="name"
:can-mod-item="canModItem"
:class="{
'can-browse': canBrowse,
}"
@click.self="canBrowse && $dispatch('go', name, info.name)"
></item>
<li v-show="numFiltered" class="list-group-item">
<i>Filteted out {{numFilteredInfo}}</i>
<li v-if="empty" class="list-group-item">
<i>Empty</i>
</li>
<li v-if="numFiltered" class="list-group-item">
<i>Filtered out {{numFilteredInfo}}</i>
</li>
</ul>
</div>
......@@ -23,7 +23,31 @@
v-show="nameNew === false"
>
{{info.name}}
<div class="btn-group pull-right">
<span
v-if="info.manager"
class="label label-primary"
>
Manager
</span>
<span
v-if="info.privacy !== undefined"
:class="{
'label': true,
'label-success': info.privacy === 0,
'label-warning': info.privacy === 1,
'label-danger': info.privacy === 2,
}">
{{ ["Public","Protected","Private"][info.privacy] }}
</span>
<span
v-if="info.password"
class="label label-info">
has password
</span>
<div class="btn-group pull-right" @click.stop>
<group-control
:info="info"
></group-control>
<button
v-if="canModItem || info.manager"
class="btn btn-info btn-xs"
......@@ -33,17 +57,12 @@
Rename
</button>
<button
v-if="canModItem || info.manager"
class="btn btn-danger btn-xs"
@click="remove">
<i class="fa fa-trash"></i>
Remove
</button>
</div>
<span
v-if="info.manager"
class="badge"
>
Manager
</span>
</span>
</li>
......@@ -16,7 +16,7 @@
</span>
<template v-if="!noNav[sec]">
<nav-item
v-for="info in list[sec] | limitBy maxNav"
v-for="info in list[sec] | orderBy 'name' | limitBy maxNav"
:title="info.name"
:active="target == info.name"
@clicked="$dispatch('go', sec, info.name)"
......
<input
type="text"
class="form-control"
v-model="text">
<vs-select
v-show="name != 'role'"
class="input-group-btn vispa-fix-form"
:value.sync="filter"
:options="filterOptions"
></vs-select>
......@@ -2,6 +2,7 @@
{{info.name}}
<div class="btn-group pull-right">
<button
v-if="canModItem"
class="btn btn-danger btn-xs"
@click="remove">
<i class="fa fa-trash"></i>
......
<nav-item
:active="isCurrent"
@clicked="$dispatch('go', name, '')"
>
<span slot="title">
{{title | capitalize}}
<i
v-show="refreshing"
class="fa fa-refresh fa-spin"
></i>
</span>
<nav-item
v-for="info in list"
:title="info.name"
:active="selected == info.name"
@clicked="$dispatch('go', name, info.name)"
></nav-item>
<nav-item
v-show="canAdd"
><a slot="content">
<span
v-show="!adding"
@click="startAdd"
>
<i class="fa fa-plus"></i>
<i>Add new {{name}}</i>
</span>
<span
class="input-group"
v-show="adding"
>
<input
v-model="addName"
:placeholder="'name of the new '+name"
type="text"
class="form-control"
@keyup.enter="doAdd"
v-el:name
>
<span class="input-group-btn">
<button
:class="{
'btn': true,
'btn-default': true,
'disabled': !addName
}"
@click="doAdd"
>
Add
</button>
</span>
</span>
</a>
</nav-item>
<nav-item
v-show="!canAdd && !list.length"
><i slot="title">No content</i></nav-item>
</nav-item>
......@@ -12,19 +12,23 @@
</div>
</div>
<name-list
v-if="managing"
name="manager"
title="Managers"
url="project/manager"
:can-add="managing"
:can-mod-item="managing"
></name-list>
<member-list
name="user"
title="Users"
url="project/user"
:can-add="managing"
:can-mod-item="managing"
></member-list>
<member-list
name="group"
title="Groups"
url="project/group"
:can-add="managing"
:can-mod-item="managing"
......
<div class="item-list">
<h2>
{{title}}
<i
v-show="refreshing"
class="fa fa-refresh fa-spin"
></i>
</h2>
<div
class="input-group"
v-if="canAdd"
>
<input
type="text"
class="form-control"
:placeholder="'name of the '+name+' to add/remove'"
v-model="target"
:list="suggestionId"
@keyup.enter="toggle"
>
<span class="input-group-btn">
<button
:class="{
'btn': true,
'btn-danger': targetExists,
'btn-success': !targetExists,
'disabled': !target,
}"
type="button"
@click="toggle"
>{{targetExists ? 'Remove' : 'Add'}}</button>
</span>
</div>
<div class="input-group" >
<span class="input-group-addon">Filter</span>
<item-filter
:name="name"
:can-add="canAdd"
:merged-roles="mergedRoles"
v-ref:filter
></item-filter>
</div>
<ul class="list-group">
<item
v-for="info in filtered | orderBy 'name'"
:info="info"
:can-mod-item="canModItem"
:url="itemUrl"
:all-roles="allRoles"
:class="{
'can-browse': canBrowse,
}"
@click.self="canBrowse && $dispatch('go', name, info.name)"
></item>
<li v-show="numFiltered" class="list-group-item">