Improved gran permissions support for systemusers
This commit is contained in:
parent
5c10c39157
commit
c5140ccbae
8
TODO.md
8
TODO.md
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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'):
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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',))
|
||||
|
|
|
@ -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"),
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
› <a href="{% url 'admin:app_list' app_label=app_label %}">{{ app_label|capfirst|escape }}</a>
|
||||
› <a href="{% url opts|admin_urlname:'changelist' %}">{{ opts.verbose_name_plural|capfirst }}</a>
|
||||
{% if obj %}
|
||||
› <a href="{% url opts|admin_urlname:'change' obj.pk %}">{{ obj }}</a>
|
||||
› {{ action_name }}
|
||||
{% elif add %}
|
||||
› <a href="../">{% trans "Add" %} {{ opts.verbose_name }}</a>
|
||||
› {{ action_name }}
|
||||
{% else %}
|
||||
› {{ 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 %}
|
||||
|
Loading…
Reference in a new issue