Fixes on systemusers ACL permissions

This commit is contained in:
Marc Aymerich 2015-05-11 14:05:39 +00:00
parent 2877f64d9d
commit 20ccadf3e1
15 changed files with 201 additions and 133 deletions

View file

@ -359,3 +359,10 @@ resorce monitoring more efficient, less mem an better queries for calc current d
ciphers=['arcfour128', 'aes256']
http://paramiko-docs.readthedocs.org/en/latest/api/transport.html
* Grant and revoke permissions
setfacl: /home/marcay//logs: Operation not permitted

View file

@ -6,7 +6,6 @@ import socket
import sys
import select
import paramiko
from celery.datastructures import ExceptionInfo
from django.conf import settings as djsettings
@ -25,11 +24,12 @@ def Paramiko(backend, log, server, cmds, async=False):
"""
Executes cmds to remote server using Pramaiko
"""
import paramiko
script = '\n'.join(cmds)
script = script.replace('\r', '')
log.state = log.STARTED
log.script = script
log.save(update_fields=('script', 'state'))
log.save(update_fields=('script', 'state', 'updated_at'))
if not cmds:
return
channel = None
@ -48,7 +48,7 @@ def Paramiko(backend, log, server, cmds, async=False):
logger.error('%s timed out on %s' % (backend, addr))
log.state = log.TIMEOUT
log.stderr = str(e)
log.save(update_fields=['state', 'stderr'])
log.save(update_fields=('state', 'stderr', 'updated_at'))
return
paramiko_connections[addr] = ssh
transport = ssh.get_transport()
@ -73,7 +73,7 @@ def Paramiko(backend, log, server, cmds, async=False):
while part:
log.stderr += part
part = channel.recv_stderr(1024).decode('utf-8')
log.save(update_fields=['stdout', 'stderr'])
log.save(update_fields=('stdout', 'stderr', 'updated_at'))
if channel.exit_status_ready():
if second:
break
@ -95,7 +95,7 @@ def Paramiko(backend, log, server, cmds, async=False):
finally:
if log.state == log.STARTED:
log.state = log.ABORTED
log.save(update_fields=['state'])
log.save(update_fields=('state', 'updated_at'))
if channel is not None:
channel.close()
@ -108,25 +108,25 @@ def OpenSSH(backend, log, server, cmds, async=False):
script = script.replace('\r', '')
log.state = log.STARTED
log.script = script
log.save(update_fields=('script', 'state'))
log.save(update_fields=('script', 'state', 'updated_at'))
if not cmds:
return
channel = None
ssh = None
try:
ssh = sshrun(server.get_address(), script, executable=backend.script_executable,
persist=True, async=async)
persist=True, async=async, silent=True)
logger.debug('%s running on %s' % (backend, server))
if async:
second = False
for state in ssh:
log.stdout += state.stdout.decode('utf8')
log.stderr += state.stderr.decode('utf8')
log.save()
log.save(update_fields=('stdout', 'stderr', 'updated_at'))
log.exit_code = state.exit_code
else:
log.stdout = ssh.stdout
log.stderr = ssh.stderr
log.stdout = ssh.stdout.decode('utf8')
log.stderr = ssh.stderr.decode('utf8')
log.exit_code = ssh.exit_code
log.state = log.SUCCESS if log.exit_code == 0 else log.FAILURE
logger.debug('%s execution state on %s is %s' % (backend, server, log.state))
@ -140,7 +140,7 @@ def OpenSSH(backend, log, server, cmds, async=False):
finally:
if log.state == log.STARTED:
log.state = log.ABORTED
log.save(update_fields=['state'])
log.save(update_fields=('state', 'updated_at'))
def SSH(*args, **kwargs):
@ -150,12 +150,11 @@ def SSH(*args, **kwargs):
def Python(backend, log, server, cmds, async=False):
# TODO collect stdout?
script = [ str(cmd.func.__name__) + str(cmd.args) for cmd in cmds ]
script = json.dumps(script, indent=4).replace('"', '')
log.state = log.STARTED
log.script = '\n'.join([log.script, script])
log.save(update_fields=('script', 'state'))
log.script = '\n'.join((log.script, script))
log.save(update_fields=('script', 'state', 'updated_at'))
try:
for cmd in cmds:
with CaptureStdout() as stdout:
@ -163,7 +162,7 @@ def Python(backend, log, server, cmds, async=False):
for line in stdout:
log.stdout += line + '\n'
if async:
log.save(update_fields=['stdout'])
log.save(update_fields=('stdout', 'updated_at'))
except:
log.exit_code = 1
log.state = log.FAILURE

View file

@ -43,9 +43,9 @@ ORCHESTRATION_BACKEND_CLEANUP_DAYS = Setting('ORCHESTRATION_BACKEND_CLEANUP_DAYS
ORCHESTRATION_SSH_METHOD_BACKEND = Setting('ORCHESTRATION_SSH_METHOD_BACKEND',
'orchestra.contrib.orchestration.methods.OpenSSH',
help_text=_("Two methods provided:<br>"
"<tt>orchestra.contrib.orchestration.methods.OpenSSH</tt> with ControlPersist.<br>"
"<tt>orchestra.contrib.orchestration.methods.Paramiko</tt> with connection pool.<br>"
"Both perform similarly, but OpenSSH has the advantage that the connections are shared between workers,<br>"
help_text=_("Two methods are provided:<br>"
"1) <tt>orchestra.contrib.orchestration.methods.OpenSSH</tt> with ControlPersist.<br>"
"2) <tt>orchestra.contrib.orchestration.methods.Paramiko</tt> with connection pool.<br>"
"Both perform similarly, but OpenSSH has the advantage that the connections are shared between workers. "
"Paramiko, in contrast, has a per worker connection pool.")
)

View file

@ -155,6 +155,29 @@ match_price.help_text = _("Only <b>the rate</b> with a) inmediate inferior metri
def best_price(rates, metric):
pass
candidates = []
selected = False
prev = None
rates = _prepend_missing(rates.distinct())
for rate in rates:
if prev:
if prev.plan != rate.plan:
if not selected and prev.quantity <= metric:
candidates.append(prev)
selected = False
if not selected and rate.quantity > metric:
if prev.quantity <= metric:
candidates.append(prev)
selected = True
prev = rate
if not selected and prev.quantity <= metric:
candidates.append(prev)
candidates.sort(key=lambda r: r.price)
if candidates:
return [AttrDict(**{
'quantity': metric,
'price': candidates[0].price,
})]
return None
best_price.verbose_name = _("Best price")
best_price.help_text = _("Produces the best possible price given all active rating lines.")

View file

@ -7,54 +7,67 @@ 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.middlewares import OperationsMiddleware
from orchestra.contrib.orchestration import Operation, helpers
from .forms import GrantPermissionForm
from .forms import PermissionForm
def grant_permission(modeladmin, request, queryset):
def get_verbose_choice(choices, value):
for choice, verbose in choices:
if choice == value:
return verbose
def set_permission(modeladmin, request, queryset):
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]
form = PermissionForm(user)
if request.method == 'POST':
form = GrantPermissionForm(user, request.POST)
form = PermissionForm(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']
operations = []
for user in queryset:
user.grant_to = to
user.grant_ro = ro
# DOn't collect, execute right away for path validation
OperationsMiddleware.collect('grant_permission', instance=user)
user.set_perm_action = cleaned_data['set_action']
user.set_perm_base_home = cleaned_data['base_home']
user.set_perm_home_extension = cleaned_data['home_extension']
user.set_perm_perms = cleaned_data['permissions']
operations.extend(Operation.create_for_action(user, 'set_permission'))
verbose_action = get_verbose_choice(form.fields['set_action'].choices,
user.set_perm_action)
verbose_permissions = get_verbose_choice(form.fields['permissions'].choices,
user.set_perm_perms)
context = {
'type': _("read-only") if ro else _("read-write"),
'to': to,
'action': verbose_action,
'perms': verbose_permissions,
'to': os.path.join(user.set_perm_base_home, user.set_perm_home_extension),
}
msg = _("Granted %(type)s permissions on %(to)s") % context
msg = _("%(action)s %(perms)s permission to %(to)s") % context
modeladmin.log_change(request, user, msg)
# TODO feedback message
logs = Operation.execute(operations)
helpers.message_user(request, logs)
return
opts = modeladmin.model._meta
app_label = opts.app_label
context = {
'title': _("Grant permission"),
'action_name': _("Grant permission"),
'action_value': 'grant_permission',
'title': _("Set permission"),
'action_name': _("Set permission"),
'action_value': 'set_permission',
'queryset': queryset,
'opts': opts,
'obj': user,
'app_label': app_label,
'action_checkbox_name': admin.helpers.ACTION_CHECKBOX_NAME,
'form': GrantPermissionForm(user),
'form': form,
}
return TemplateResponse(request, 'admin/systemusers/systemuser/grant_permission.html',
return TemplateResponse(request, 'admin/systemusers/systemuser/set_permission.html',
context, current_app=modeladmin.admin_site.name)
grant_permission.url_name = 'grant-permission'
grant_permission.verbose_name = _("Grant permission")
set_permission.url_name = 'set-permission'
set_permission.verbose_name = _("Set permission")
def delete_selected(modeladmin, request, queryset):

View file

@ -6,7 +6,7 @@ from orchestra.admin.actions import disable
from orchestra.contrib.accounts.admin import SelectAccountAdminMixin
from orchestra.contrib.accounts.filters import IsActiveListFilter
from .actions import grant_permission, delete_selected
from .actions import set_permission, delete_selected
from .filters import IsMainListFilter
from .forms import SystemUserCreationForm, SystemUserChangeForm
from .models import SystemUser
@ -41,7 +41,7 @@ class SystemUserAdmin(ChangePasswordAdminMixin, SelectAccountAdminMixin, Extende
add_form = SystemUserCreationForm
form = SystemUserChangeForm
ordering = ('-id',)
actions = (delete_selected, grant_permission, disable)
actions = (delete_selected, set_permission, disable)
change_view_actions = actions
def display_main(self, user):

View file

@ -15,7 +15,7 @@ class UNIXUserBackend(ServiceController):
"""
verbose_name = _("UNIX user")
model = 'systemusers.SystemUser'
actions = ('save', 'delete', 'grant_permission')
actions = ('save', 'delete', 'set_permission', 'validate_path')
doc_settings = (settings,
('SYSTEMUSERS_DEFAULT_GROUP_MEMBERS', 'SYSTEMUSERS_MOVE_ON_DELETE_PATH')
)
@ -65,17 +65,69 @@ class UNIXUserBackend(ServiceController):
else:
self.append("rm -fr %(base_home)s" % context)
def grant_permission(self, user):
# TODO
def set_permission(self, user):
context = self.get_context(user)
context.update({
'to': user.grant_to,
'ro': user.grant_ro,
'perm_action': user.set_perm_action,
'perm_home': user.set_perm_base_home,
'perm_to': os.path.join(user.set_perm_base_home, user.set_perm_home_extension),
'exclude': '',
})
if user.grant_ro:
self.append('echo "acl add read permissions for %(user)s to %(to)s"' % context)
exclude_acl = []
for exclude in settings.SYSTEMUSERS_EXLUDE_ACL_PATHS:
context['exclude'] = exclude
exclude_acl.append('-not -path "%(perm_home)s/%(exclude)s"' % context)
if exclude_acl:
context['exclude'] = ' \\\n -a '.join(exclude_acl)
if user.set_perm_perms == 'read-write':
context['perm_perms'] = 'rwx' if user.set_perm_action == 'grant' else '---'
elif user.set_perm_perms == 'read-only':
context['perm_perms'] = 'r-x' if user.set_perm_action == 'grant' else '-wx'
elif user.set_perm_perms == 'write-only':
context['perm_perms'] = '-wx' if user.set_perm_action == 'grant' else 'r-x'
if user.set_perm_action == 'grant':
self.append(textwrap.dedent("""\
# Home access
setfacl -m u:%(user)s:--x '%(perm_home)s'
# Grant perms to existing and future files
find '%(perm_to)s' %(exclude)s \\
-exec setfacl -m u:%(user)s:%(perm_perms)s {} \\;
find '%(perm_to)s' -type d %(exclude)s \\
-exec setfacl -m d:u:%(user)s:%(perm_perms)s {} \\;
# Account group as the owner of new files
chmod g+s '%(perm_to)s'
""") % context
)
if not user.is_main:
self.append(textwrap.dedent("""\
# Grant access to main user
find '%(perm_to)s' -type d %(exclude)s \\
-exec setfacl -m d:u:%(mainuser)s:rwx {} \\;
""") % context
)
elif user.set_perm_action == 'revoke':
self.append(textwrap.dedent("""\
# Revoke permissions
find '%(perm_to)s' %(exclude)s \\
-exec setfacl -m u:%(user)s:%(perm_perms)s {} \\;
""") % context
)
else:
self.append('echo "acl add read-write permissions for %(user)s to %(to)s"' % context)
raise NotImplementedError()
def validate_path(self, user):
context = {
'perm_to': os.path.join(user.set_perm_base_home, user.set_perm_home_extension)
}
self.append(textwrap.dedent("""\
if [[ ! -e '%(perm_to)s' ]]; then
echo "%(perm_to)s path does not exists." >&2
exit 1
fi
""") % context
)
def get_groups(self, user):
if user.is_main:

View file

@ -1,8 +1,10 @@
import textwrap
from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ngettext, ugettext_lazy as _
from orchestra.contrib.orchestration import Operation
from orchestra.forms import UserCreationForm, UserChangeForm
from . import settings
@ -79,21 +81,44 @@ 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='',
class PermissionForm(forms.Form):
set_action = forms.ChoiceField(label=_("Action"), initial='grant',
choices=(
('grant', _("Grant")),
('revoke', _("Revoke"))
))
base_home = forms.ChoiceField(label=_("Set permissions to"), choices=(),
help_text=_("User will be granted/revoked access to this directory."))
home_extension = forms.CharField(label=_("Home 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."))
permissions = forms.ChoiceField(label=_("Permissions"), initial='read-write',
choices=(
('read-write', _("Read and write")),
('read-only', _("Read only")),
('write-only', _("Write only"))
))
def __init__(self, *args, **kwargs):
instance = args[0]
self.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 = (
super(PermissionForm, self).__init__(*super_args, **kwargs)
related_users = type(self.instance).objects.filter(account=self.instance.account_id)
self.fields['base_home'].choices = (
(user.get_base_home(), user.get_base_home()) for user in related_users
)
def clean(self):
cleaned_data = super(PermissionForm, self).clean()
user = self.instance
user.set_perm_action = cleaned_data['set_action']
user.set_perm_base_home = cleaned_data['base_home']
user.set_perm_home_extension = cleaned_data['home_extension']
user.set_perm_perms = cleaned_data['permissions']
log = Operation.execute_action(user, 'validate_path')[0]
if 'path does not exists' in log.stderr:
raise ValidationError({
'home_extension': log.stderr,
})
return cleaned_data

View file

@ -58,3 +58,10 @@ SYSTEMUSERS_MOVE_ON_DELETE_PATH = Setting('SYSTEMUSERS_MOVE_ON_DELETE_PATH',
help_text="Available fromat names: <tt>%s</tt>" % ', '.join(_backend_names),
validators=[Setting.string_format_validator(_backend_names)],
)
SYSTEMUSERS_EXLUDE_ACL_PATHS = Setting('SYSTEMUSERS_EXLUDE_ACL_PATHS',
(),
help_text=("Relative to user's home.<br>"
"e.g. ('logs', 'logs/apache*', 'webapps')"),
)

View file

@ -1,65 +0,0 @@
{% 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 }}{% for x in ""|ljust:"50" %}&nbsp;{% endfor %}
<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 %}

View file

@ -31,7 +31,7 @@ class WebAppServiceMixin(object):
if [[ $CREATED == 1 && ! $(ls -A %(app_path)s) ]]; then
{
# Wait for other backends to do their thing or cp under construction
sleep 1
sleep 10
if [[ ! $(ls -A %(app_path)s) ]]; then
cp -r %(under_construction_path)s %(app_path)s
chown -R %(user)s:%(group)s %(app_path)s

View file

@ -179,7 +179,6 @@ WEBAPPS_UNDER_CONSTRUCTION_PATH = Setting('WEBAPPS_UNDER_CONSTRUCTION_PATH', '',
# WEBAPPS_TYPES[webapp_type] = value
WEBAPPS_PHP_DISABLED_FUNCTIONS = Setting('WEBAPPS_PHP_DISABLED_FUNCTION', (
'exec',
'passthru',
@ -200,7 +199,14 @@ WEBAPPS_PHP_DISABLED_FUNCTIONS = Setting('WEBAPPS_PHP_DISABLED_FUNCTION', (
'openlog',
'escapeshellcmd',
'escapeshellarg',
'dl'
'dl',
'fsockopen',
'pfsockopen',
'stream_socket_client',
# Used for spamming
'getmxrr',
# Used in some php shells
'str_rot13',
))

View file

@ -34,6 +34,7 @@
<form action="" method="post">{% csrf_token %}
{% if form %}
<fieldset class="module aligned">
{{ form.non_field_errors }}
{% for field in form %}
<div class="form-row ">
<div >

View file

@ -148,8 +148,9 @@ def sshrun(addr, command, *args, executable='bash', persist=False, **kwargs):
'ControlPersist=yes',
'ControlPath=~/.ssh/orchestra-%r-%h-%p',
))
cmd = 'ssh -o {options} -C root@{addr} {executable}'.format(options=' -o '.join(options),
addr=addr, executable=executable)
options = ' -o '.join(options)
cmd = 'ssh -o {options} -C root@{addr} {executable}'.format(options=options, addr=addr,
executable=executable)
return run(cmd, *args, stdin=command.encode('utf8'), **kwargs)
@ -181,7 +182,7 @@ class OperationLocked(Exception):
class LockFile(object):
""" File-based lock mechanism used for preventing concurrency problems """
def __init__(self, lockfile, expire=5*60, unlocked=False):
""" /dev/shm/ can be a good place for storing locks ;) """
# /dev/shm/ can be a good place for storing locks
self.lockfile = lockfile
self.expire = expire
self.unlocked = unlocked

View file

@ -11,7 +11,6 @@ kombu==3.0.23
billiard==3.3.0.18
Markdown==2.4
djangorestframework==3.1.1
paramiko==1.15.2
ecdsa==0.11
Pygments==1.6
django-filter==0.7