3
0
Fork 0

fix: prevent delete for built-in roles

This commit is contained in:
Lance Edgar 2024-08-14 16:58:08 -05:00
parent 330ee324ba
commit bc49392140
8 changed files with 106 additions and 53 deletions

View file

@ -677,7 +677,6 @@ class Form:
# fill in the blanks if anything got missed
for key in fields:
if key not in schema:
log.warning("key '%s' not in schema", key)
node = colander.SchemaNode(colander.String(), name=key)
schema.add(node)

View file

@ -421,6 +421,8 @@ class Grid:
for i, record in enumerate(original_data):
original_record = record
record = dict(record)
# convert data if needed, for json compat
record = make_json_safe(record,
# TODO: is this a good idea?
@ -433,9 +435,11 @@ class Grid:
# add action urls to each record
for action in self.actions:
url = action.get_url(record, i)
key = f'_action_url_{action.key}'
record[key] = url
if key not in record:
url = action.get_url(original_record, i)
if url:
record[key] = url
data.append(record)

View file

@ -378,19 +378,23 @@
tag="a" href="${master.get_action_url('edit', instance)}"
icon-left="edit"
label="Edit This" />
<wutta-button once type="is-danger"
tag="a" href="${master.get_action_url('delete', instance)}"
icon-left="trash"
label="Delete This" />
% if instance_deletable:
<wutta-button once type="is-danger"
tag="a" href="${master.get_action_url('delete', instance)}"
icon-left="trash"
label="Delete This" />
% endif
% elif master.editing:
<wutta-button once
tag="a" href="${master.get_action_url('view', instance)}"
icon-left="eye"
label="View This" />
<wutta-button once type="is-danger"
tag="a" href="${master.get_action_url('delete', instance)}"
icon-left="trash"
label="Delete This" />
% if instance_deletable:
<wutta-button once type="is-danger"
tag="a" href="${master.get_action_url('delete', instance)}"
icon-left="trash"
label="Delete This" />
% endif
% elif master.deleting:
<wutta-button once
tag="a" href="${master.get_action_url('view', instance)}"

View file

@ -24,7 +24,8 @@
label="Actions"
v-slot="props">
% for action in grid.actions:
<a :href="props.row._action_url_${action.key}"
<a v-if="props.row._action_url_${action.key}"
:href="props.row._action_url_${action.key}"
class="${action.link_class}">
${action.render_icon_and_label()}
</a>

View file

@ -204,6 +204,8 @@ class MasterView(View):
i.e. it should have a :meth:`delete()` view. Default value is
``True``.
See also :meth:`is_deletable()`.
.. attribute:: form_fields
List of columns for the model form.
@ -458,6 +460,9 @@ class MasterView(View):
instance = self.get_instance()
instance_title = self.get_instance_title(instance)
if not self.is_deletable(instance):
return self.redirect(self.get_action_url('view', instance))
# nb. this form proper is not readonly..
form = self.make_model_form(instance,
cancel_url_fallback=self.get_action_url('view', instance),
@ -839,6 +844,10 @@ class MasterView(View):
defaults.update(context)
context = defaults
# add crud flags if we have an instance
if 'instance' in context:
context['instance_deletable'] = self.is_deletable(context['instance'])
# first try the template path most specific to this view
template_prefix = self.get_template_prefix()
mako_path = f'{template_prefix}/{template}.mako'
@ -986,26 +995,7 @@ class MasterView(View):
"""
query = self.get_query(session=session)
if query:
data = query.all()
# determine which columns are relevant for data set
if not columns:
columns = self.get_grid_columns()
if not columns:
model_class = self.get_model_class()
if model_class:
columns = get_model_fields(self.config, model_class)
if not columns:
raise ValueError("cannot determine columns for the grid")
columns = set(columns)
columns.update(self.get_model_key())
# prune data fields for which no column is defined
for i, record in enumerate(data):
data[i]= dict([(key, record[key])
for key in columns])
return data
return query.all()
return []
@ -1158,14 +1148,32 @@ class MasterView(View):
def get_action_url_delete(self, obj, i):
"""
Returns the "delete" grid action URL for the given object.
Returns the "delete" grid action URL for the given object, if
applicable.
Most typically this is like ``/widgets/XXX/delete`` where
``XXX`` represents the object's key/ID.
Calls :meth:`get_action_url()` under the hood.
This first calls :meth:`is_deletable()` and if that is false,
this method will return ``None``.
Calls :meth:`get_action_url()` to generate the true URL.
"""
return self.get_action_url('delete', obj)
if self.is_deletable(obj):
return self.get_action_url('delete', obj)
def is_deletable(self, obj):
"""
Returns a boolean indicating whether "delete" should be
allowed for the given model instance (and for current user).
By default this always return ``True``; subclass can override
if needed.
Note that the use of this method implies :attr:`deletable` is
true, so the method does not need to check that flag.
"""
return True
def make_model_form(self, model_instance=None, **kwargs):
"""

View file

@ -69,6 +69,21 @@ class RoleView(MasterView):
# notes
g.set_renderer('notes', self.grid_render_notes)
def is_deletable(self, role):
""" """
session = self.app.get_session(role)
auth = self.app.get_auth_handler()
# prevent delete for built-in roles
if role is auth.get_role_administrator(session):
return False
if role is auth.get_role_authenticated(session):
return False
if role is auth.get_role_anonymous(session):
return False
return True
def configure_form(self, f):
""" """
super().configure_form(f)