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) # Deprecate restart/start/stop services (do touch wsgi.py and fuck celery)
# orchestrate async stdout stderr (inspired on pangea managemengt commands) # orchestrate async stdout stderr (inspired on pangea managemengt commands)
# orchestra-beat support for uwsgi cron orchestra-beat support for uwsgi cron
# message.log if len() == 1: return changeform
make django admin taskstate uncollapse fucking traceback, ( if exists ?) make django admin taskstate uncollapse fucking traceback, ( if exists ?)
# form for custom message on admin save "comment & save"? # 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: 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. * 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 import copy
from .backends import ServiceBackend, ServiceController, replace from .backends import ServiceBackend, ServiceController, replace
@ -35,19 +36,31 @@ class Operation():
@classmethod @classmethod
def execute(cls, operations, serialize=False, async=None): def execute(cls, operations, serialize=False, async=None):
from . import manager from . import manager
scripts, oserialize = manager.generate(operations) scripts, backend_serialize = manager.generate(operations)
return manager.execute(scripts, serialize=(serialize or oserialize), async=async) return manager.execute(scripts, serialize=(serialize or backend_serialize), async=async)
@classmethod @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) 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) return cls.execute(operations)
def preload_context(self): def preload_context(self):
""" """
Heuristic Heuristic: Running get_context will prevent most of related objects do not exist errors
Running get_context will prevent most of related objects do not exist errors
""" """
if self.action == self.DELETE: if self.action == self.DELETE:
if hasattr(self.backend, 'get_context'): if hasattr(self.backend, 'get_context'):

View File

@ -1,3 +1,4 @@
import time
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from django.apps import apps from django.apps import apps
@ -96,9 +97,22 @@ class Command(BaseCommand):
return return
break break
if not dry: if not dry:
logs = manager.execute(scripts, serialize=serialize) logs = manager.execute(scripts, serialize=serialize, async=True)
for log in logs: running = list(logs)
self.stdout.write(log.stdout) stdout = 0
self.stderr.write(log.stderr) 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: for log in logs:
self.stdout.write(' '.join((log.backend, log.state))) self.stdout.write(' '.join((log.backend, log.state)))

View File

@ -123,9 +123,14 @@ def execute(scripts, serialize=False, async=None):
'async': async, 'async': async,
} }
log = backend.create_log(*args, **kwargs) 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 kwargs['log'] = log
task = keep_log(backend.execute, log, operations) 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: if serialize:
# Execute one backend at a time, no need for threads # Execute one backend at a time, no need for threads
task(*args, **kwargs) task(*args, **kwargs)
@ -181,7 +186,7 @@ def collect(instance, action, **kwargs):
if update_fields is not None: if update_fields is not None:
# TODO remove this, django does not execute post_save if update_fields=[]... # TODO remove this, django does not execute post_save if update_fields=[]...
# Maybe open a ticket at Djangoproject ? # 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() # i.e. account.disable()
if update_fields != []: if update_fields != []:
execute = False execute = False

View File

@ -92,10 +92,15 @@ class BackendLog(models.Model):
def execution_time(self): def execution_time(self):
return (self.updated_at-self.created_at).total_seconds() 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): def backend_class(self):
return ServiceBackend.get_backend(self.backend) return ServiceBackend.get_backend(self.backend)
class BackendOperation(models.Model): class BackendOperation(models.Model):
""" """
Encapsulates an operation, storing its related object, the action and the backend. 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_display = ('plan', 'account_link')
list_filter = ('plan__name',) list_filter = ('plan__name',)
list_select_related = ('plan', 'account') list_select_related = ('plan', 'account')
search_fields = ('account__username', 'plan__name', 'id')
admin.site.register(Plan, PlanAdmin) admin.site.register(Plan, PlanAdmin)
admin.site.register(ContractedPlan, ContractedPlanAdmin) admin.site.register(ContractedPlan, ContractedPlanAdmin)

View File

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

View File

@ -68,9 +68,11 @@ class RateQuerySet(models.QuerySet):
class Rate(models.Model): class Rate(models.Model):
STEP_PRICE = 'STEP_PRICE' STEP_PRICE = 'STEP_PRICE'
MATCH_PRICE = 'MATCH_PRICE' MATCH_PRICE = 'MATCH_PRICE'
BEST_PRICE = 'BEST_PRICE'
RATE_METHODS = { RATE_METHODS = {
STEP_PRICE: rating.step_price, STEP_PRICE: rating.step_price,
MATCH_PRICE: rating.match_price, MATCH_PRICE: rating.match_price,
BEST_PRICE: rating.best_price,
} }
service = models.ForeignKey('services.Service', verbose_name=_("service"), 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.verbose_name = _("Match price")
match_price.help_text = _("Only <b>the rate</b> with a) inmediate inferior metric and b) lower price is applied. " 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.") "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 return
try: try:
bool(getattr(self, method)(obj)) bool(getattr(self, method)(obj))
except Exception as exception: except Exception as exc:
raise ValidationError(format_exception(exc)) raise ValidationError(format_exception(exc))
def validate_match(self, service): def validate_match(self, service):
@ -124,8 +124,8 @@ class ServiceHandler(plugins.Plugin, metaclass=plugins.PluginMount):
safe_locals = self.get_expression_context(instance) safe_locals = self.get_expression_context(instance)
try: try:
return eval(self.metric, safe_locals) return eval(self.metric, safe_locals)
except Exception as error: except Exception as exc:
raise type(error)("%s on '%s'" %(error, self.service)) raise type(exc)("%s on '%s'" %(exc, self.service))
def get_order_description(self, instance): def get_order_description(self, instance):
safe_locals = self.get_expression_context(instance) safe_locals = self.get_expression_context(instance)

View File

@ -1,25 +1,56 @@
import os
from django import forms from django import forms
from django.contrib import messages, admin from django.contrib import messages, admin
from django.core.exceptions import PermissionDenied from django.core.exceptions import PermissionDenied
from django.template.response import TemplateResponse
from django.utils.translation import ungettext, ugettext_lazy as _ from django.utils.translation import ungettext, ugettext_lazy as _
from orchestra.admin.decorators import action_with_confirmation 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): def grant_permission(modeladmin, request, queryset):
user = queryset.get() account_id = None
log = Operation.execute_action(user, 'grant_permission') for user in queryset:
# TODO 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.url_name = 'grant-permission'
grant_permission.verbose_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) self.append("rm -fr %(base_home)s" % context)
def grant_permission(self, user): def grant_permission(self, user):
# TODO
context = self.get_context(user) 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): def get_groups(self, user):
if user.is_main: if user.is_main:

View File

@ -1,6 +1,7 @@
import textwrap import textwrap
from django import forms from django import forms
from django.utils.translation import ngettext, ugettext_lazy as _
from orchestra.forms import UserCreationForm, UserChangeForm from orchestra.forms import UserCreationForm, UserChangeForm
@ -34,6 +35,8 @@ class SystemUserFormMixin(object):
self.fields['directory'].widget = forms.HiddenInput() self.fields['directory'].widget = forms.HiddenInput()
elif self.instance.pk and (self.instance.get_base_home() == self.instance.home): elif self.instance.pk and (self.instance.get_base_home() == self.instance.home):
self.fields['directory'].widget = forms.HiddenInput() 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: if not self.instance.pk or not self.instance.is_main:
# Some javascript for hidde home/directory inputs when convinient # Some javascript for hidde home/directory inputs when convinient
self.fields['shell'].widget.attrs = { self.fields['shell'].widget.attrs = {
@ -74,3 +77,23 @@ class SystemUserCreationForm(SystemUserFormMixin, UserCreationForm):
class SystemUserChangeForm(SystemUserFormMixin, UserChangeForm): class SystemUserChangeForm(SystemUserFormMixin, UserChangeForm):
pass 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 %}