Improved gran permissions support for systemusers

This commit is contained in:
Marc Aymerich 2015-05-08 14:05:57 +00:00
parent 5c10c39157
commit c5140ccbae
15 changed files with 211 additions and 36 deletions

View file

@ -346,10 +346,12 @@ TODO mount the filesystem with "nosuid" option
# Deprecate restart/start/stop services (do touch wsgi.py and fuck celery)
# orchestrate async stdout stderr (inspired on pangea managemengt commands)
# orchestra-beat support for uwsgi cron
# message.log if len() == 1: return changeform
orchestra-beat support for uwsgi cron
make django admin taskstate uncollapse fucking traceback, ( if exists ?)
# form for custom message on admin save "comment & save"?
# backend.context and backned.instance provided when an action is called? like forms.cleaned_data: do it on manager.generation(backend.context = backend.get_context()) or in backend.__getattr__ ? also backend.head,tail,content switching on manager.generate()?
# replace return_code by exit_code everywhere

View file

@ -1,4 +1,4 @@
This is a simlified clone of [django-mailer](https://github.com/pinax/django-mailer).
This is a simplified clone of [django-mailer](https://github.com/pinax/django-mailer).
Using `orchestra.contrib.mailer.backends.EmailBackend` as your email backend will have the following effects:
* E-mails sent with Django's `send_mass_mail()` will be queued and sent by an out-of-band perioic task.

View file

@ -1,3 +1,4 @@
import collections
import copy
from .backends import ServiceBackend, ServiceController, replace
@ -35,19 +36,31 @@ class Operation():
@classmethod
def execute(cls, operations, serialize=False, async=None):
from . import manager
scripts, oserialize = manager.generate(operations)
return manager.execute(scripts, serialize=(serialize or oserialize), async=async)
scripts, backend_serialize = manager.generate(operations)
return manager.execute(scripts, serialize=(serialize or backend_serialize), async=async)
@classmethod
def execute_action(cls, instance, action):
def create_for_action(cls, instances, action):
if not isinstance(instances, collections.Iterable):
instances = [instances]
operations = []
for instance in instances:
backends = ServiceBackend.get_backends(instance=instance, action=action)
operations = [cls(backend_cls, instance, action) for backend_cls in backends]
for backend_cls in backends:
operations.append(
cls(backend_cls, instance, action)
)
return operations
@classmethod
def execute_action(cls, instances, action):
""" instances can be an object or an iterable for batch processing """
operations = cls.create_for_action(instances, action)
return cls.execute(operations)
def preload_context(self):
"""
Heuristic
Running get_context will prevent most of related objects do not exist errors
Heuristic: Running get_context will prevent most of related objects do not exist errors
"""
if self.action == self.DELETE:
if hasattr(self.backend, 'get_context'):

View file

@ -1,3 +1,4 @@
import time
from django.core.management.base import BaseCommand, CommandError
from django.apps import apps
@ -96,9 +97,22 @@ class Command(BaseCommand):
return
break
if not dry:
logs = manager.execute(scripts, serialize=serialize)
for log in logs:
self.stdout.write(log.stdout)
self.stderr.write(log.stderr)
logs = manager.execute(scripts, serialize=serialize, async=True)
running = list(logs)
stdout = 0
stderr = 0
while running:
for log in running:
cstdout = len(log.stdout)
cstderr = len(log.stderr)
if cstdout > stdout:
self.stdout.write(log.stdout[stdout:])
stdout = cstdout
if cstderr > stderr:
self.stderr.write(log.stderr[stderr:])
stderr = cstderr
if log.has_finished:
running.remove(log)
time.sleep(0.1)
for log in logs:
self.stdout.write(' '.join((log.backend, log.state)))

View file

@ -123,9 +123,14 @@ def execute(scripts, serialize=False, async=None):
'async': async,
}
log = backend.create_log(*args, **kwargs)
# TODO Perform this shit outside of the current transaction in a non-hacky way
#t = threading.Thread(target=backend.create_log, args=args, kwargs=kwargs)
#t.start()
#log = t.join()
# End of hack
kwargs['log'] = log
task = keep_log(backend.execute, log, operations)
logger.debug('%s is going to be executed on %s' % (backend, route.host))
logger.debug('%s is going to be executed on %s.' % (backend, route.host))
if serialize:
# Execute one backend at a time, no need for threads
task(*args, **kwargs)
@ -181,7 +186,7 @@ def collect(instance, action, **kwargs):
if update_fields is not None:
# TODO remove this, django does not execute post_save if update_fields=[]...
# Maybe open a ticket at Djangoproject ?
# "update_fileds=[]" is a convention for explicitly executing backend
# INITIAL INTENTION: "update_fileds=[]" is a convention for explicitly executing backend
# i.e. account.disable()
if update_fields != []:
execute = False

View file

@ -92,10 +92,15 @@ class BackendLog(models.Model):
def execution_time(self):
return (self.updated_at-self.created_at).total_seconds()
@property
def has_finished(self):
return self.state not in (self.STARTED, self.RECEIVED)
def backend_class(self):
return ServiceBackend.get_backend(self.backend)
class BackendOperation(models.Model):
"""
Encapsulates an operation, storing its related object, the action and the backend.

View file

@ -28,7 +28,7 @@ class ContractedPlanAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ('plan', 'account_link')
list_filter = ('plan__name',)
list_select_related = ('plan', 'account')
search_fields = ('account__username', 'plan__name', 'id')
admin.site.register(Plan, PlanAdmin)
admin.site.register(ContractedPlan, ContractedPlanAdmin)

View file

@ -1,6 +1,6 @@
from django.apps import AppConfig
from orchestra.core import administration, accounts
from orchestra.core import administration, accounts, services
from orchestra.core.translations import ModelTranslation
@ -11,5 +11,6 @@ class PlansConfig(AppConfig):
def ready(self):
from .models import Plan, ContractedPlan
accounts.register(ContractedPlan, icon='ContractedPack.png')
services.register(ContractedPlan, menu=False, dashboard=False)
administration.register(Plan, icon='Pack.png')
ModelTranslation.register(Plan, ('verbose_name',))

View file

@ -68,9 +68,11 @@ class RateQuerySet(models.QuerySet):
class Rate(models.Model):
STEP_PRICE = 'STEP_PRICE'
MATCH_PRICE = 'MATCH_PRICE'
BEST_PRICE = 'BEST_PRICE'
RATE_METHODS = {
STEP_PRICE: rating.step_price,
MATCH_PRICE: rating.match_price,
BEST_PRICE: rating.best_price,
}
service = models.ForeignKey('services.Service', verbose_name=_("service"),

View file

@ -152,3 +152,9 @@ def match_price(rates, metric):
match_price.verbose_name = _("Match price")
match_price.help_text = _("Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. "
"Nominal price will be used when initial block is missing.")
def best_price(rates, metric):
pass
best_price.verbose_name = _("Best price")
best_price.help_text = _("Produces the best possible price given all active rating lines.")

View file

@ -49,7 +49,7 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
return
try:
bool(getattr(self, method)(obj))
except Exception as exception:
except Exception as exc:
raise ValidationError(format_exception(exc))
def validate_match(self, service):
@ -124,8 +124,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
safe_locals = self.get_expression_context(instance)
try:
return eval(self.metric, safe_locals)
except Exception as error:
raise type(error)("%s on '%s'" %(error, self.service))
except Exception as exc:
raise type(exc)("%s on '%s'" %(exc, self.service))
def get_order_description(self, instance):
safe_locals = self.get_expression_context(instance)

View file

@ -1,25 +1,56 @@
import os
from django import forms
from django.contrib import messages, admin
from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse
from django.utils.translation import ungettext, ugettext_lazy as _
from orchestra.admin.decorators import action_with_confirmation
from orchestra.contrib.orchestration import Operation
from orchestra.contrib.orchestration.middlewares import OperationsMiddleware
from .forms import GrantPermissionForm
class GrantPermissionForm(forms.Form):
base_path = forms.ChoiceField(label=_("Grant access to"), choices=(('hola', 'hola'),),
help_text=_("User will be granted access to this directory."))
path_extension = forms.CharField(label='', required=False)
read_only = forms.BooleanField(label=_("Read only"), initial=False, required=False,
help_text=_("Designates whether the permissions granted will be read-only or read/write."))
@action_with_confirmation(extra_context=dict(form=GrantPermissionForm()))
def grant_permission(modeladmin, request, queryset):
user = queryset.get()
log = Operation.execute_action(user, 'grant_permission')
# TODO
account_id = None
for user in queryset:
account_id = account_id or user.account_id
if user.account_id != account_id:
messages.error("Users from the same account should be selected.")
user = queryset[0]
if request.method == 'POST':
form = GrantPermissionForm(user, request.POST)
if form.is_valid():
cleaned_data = form.cleaned_data
to = os.path.join(cleaned_data['base_path'], cleaned_data['path_extension'])
ro = cleaned_data['read_only']
for user in queryset:
user.grant_to = to
user.grant_ro = ro
OperationsMiddleware.collect('grant_permission', instance=user)
context = {
'type': _("read-only") if ro else _("read-write"),
'to': to,
}
msg = _("Granted %(type)s permissions on %(to)s") % context
modeladmin.log_change(request, user, msg)
return
opts = modeladmin.model._meta
app_label = opts.app_label
context = {
'title': _("Grant permission"),
'action_name': _("Grant permission"),
'action_value': 'grant_permission',
'queryset': queryset,
'opts': opts,
'obj': user,
'app_label': app_label,
'action_checkbox_name': admin.helpers.ACTION_CHECKBOX_NAME,
'form': GrantPermissionForm(user),
}
return TemplateResponse(request, 'admin/systemusers/systemuser/grant_permission.html',
context, current_app=modeladmin.admin_site.name)
grant_permission.url_name = 'grant-permission'
grant_permission.verbose_name = _("Grant permission")

View file

@ -66,8 +66,16 @@ class UNIXUserBackend(ServiceController):
self.append("rm -fr %(base_home)s" % context)
def grant_permission(self, user):
# TODO
context = self.get_context(user)
# TODO setacl
context.update({
'to': user.grant_to,
'ro': user.grant_ro,
})
if user.ro:
self.append('echo "acl add read permissions for %(user)s to %(to)s"' % context)
else:
self.append('echo "acl add read-write permissions for %(user)s to %(to)s"' % context)
def get_groups(self, user):
if user.is_main:

View file

@ -1,6 +1,7 @@
import textwrap
from django import forms
from django.utils.translation import ngettext, ugettext_lazy as _
from orchestra.forms import UserCreationForm, UserChangeForm
@ -34,6 +35,8 @@ class SystemUserFormMixin(object):
self.fields['directory'].widget = forms.HiddenInput()
elif self.instance.pk and (self.instance.get_base_home() == self.instance.home):
self.fields['directory'].widget = forms.HiddenInput()
else:
self.fields['directory'].widget = forms.TextInput(attrs={'size':'70'})
if not self.instance.pk or not self.instance.is_main:
# Some javascript for hidde home/directory inputs when convinient
self.fields['shell'].widget.attrs = {
@ -74,3 +77,23 @@ class SystemUserCreationForm(SystemUserFormMixin, UserCreationForm):
class SystemUserChangeForm(SystemUserFormMixin, UserChangeForm):
pass
class GrantPermissionForm(forms.Form):
base_path = forms.ChoiceField(label=_("Grant access to"), choices=(),
help_text=_("User will be granted access to this directory."))
path_extension = forms.CharField(label=_("Path extension"), required=False, initial='',
widget=forms.TextInput(attrs={'size':'70'}), help_text=_("Relative to chosen home."))
read_only = forms.BooleanField(label=_("Read only"), initial=False, required=False,
help_text=_("Designates whether the permissions granted will be read-only or read/write."))
def __init__(self, *args, **kwargs):
instance = args[0]
super_args = []
if len(args) > 1:
super_args.append(args[1])
super(GrantPermissionForm, self).__init__(*super_args, **kwargs)
related_users = type(instance).objects.filter(account=instance.account_id)
self.fields['base_path'].choices = (
(user.get_base_home(), user.get_base_home()) for user in related_users
)

View file

@ -0,0 +1,65 @@
{% extends "admin/base_site.html" %}
{% load i18n l10n %}
{% load url from future %}
{% load admin_urls static utils %}
{% block extrastyle %}
{{ block.super }}
<link rel="stylesheet" type="text/css" href="{% static "admin/css/forms.css" %}" />
<link rel="stylesheet" type="text/css" href="{% static "orchestra/css/hide-inline-id.css" %}" />
{% endblock %}
{% block breadcrumbs %}
<div class="breadcrumbs">
<a href="{% url 'admin:index' %}">{% trans 'Home' %}</a>
&rsaquo; <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
&rsaquo; <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
{% if obj %}
&rsaquo; <a href="{% url opts|admin_urlname:'change' obj.pk %}">{{ obj }}</a>
&rsaquo; {{ action_name }}
{% elif add %}
&rsaquo; <a href="../">{% trans "Add" %} {{ opts.verbose_name }}</a>
&rsaquo; {{ action_name }}
{% else %}
&rsaquo; {{ action_name }} multiple objects
{% endif %}
</div>
{% endblock %}
{% block content %}
<div>
<div style="margin:20px;">
Grant permissions to these system users: {% for user in queryset %}{{ user.username }}{% if not forloop.last %},{% endif %}{% endfor %}.
<ul>{{ display_objects | unordered_list }}</ul>
<form action="" method="post">{% csrf_token %}
<fieldset class="module aligned wide">
<div class="form-row ">
<div class="field-box field-home">
{{ form.path_extension.errors }}
<label for="{{ form.base_path.id_for_label }}">{{ form.base_path.label }}:</label>
{{ form.base_path }}
<p class="help">{{ form.base_path.help_text|safe }}</p>
</div>
<div class="field-box field-home">
{{ form.path_extension.errors }}
<label for="{{ form.path_extension.id_for_label }}"></label>
{{ form.path_extension }}
<p class="help">{{ form.path_extension.help_text|safe }}</p>
</div>
</div>
<div class="form-row ">
{{ form.read_only }} <label for="{{ form.read_only.id_for_label }}" class="vCheckboxLabel">{{ form.read_only.label }}</label>
<p class="help">{{ form.read_only.help_text|safe }}</p>
</div>
</fieldset>
<div>
{% for obj in queryset %}
<input type="hidden" name="{{ action_checkbox_name }}" value="{{ obj.pk|unlocalize }}" />
{% endfor %}
<input type="hidden" name="action" value="{{ action_value }}" />
<input type="hidden" name="post" value="{{ post_value|default:'generic_confirmation' }}" />
<input type="submit" value="{{ submit_value|default:_("Save") }}" />
</div>
</form>
{% endblock %}