Andrew Bogott has submitted this change and it was merged. ( https://gerrit.wikimedia.org/r/353156 )
Change subject: Horizon: Add sudo policy panel ...................................................................... Horizon: Add sudo policy panel This works but the error messages are cryptic... we should at least detect naming conflicts and report those failures correctly. Bug: T162097 Change-Id: I383456e8d94bbd9488b09550ef4dc9155e0bc69f --- A modules/openstack/files/mitaka/horizon/sudo/__init__.py A modules/openstack/files/mitaka/horizon/sudo/panel.py A modules/openstack/files/mitaka/horizon/sudo/sudorules.py A modules/openstack/files/mitaka/horizon/sudo/templates/sudo/_common_horizontal_form.html A modules/openstack/files/mitaka/horizon/sudo/templates/sudo/index.html A modules/openstack/files/mitaka/horizon/sudo/urls.py A modules/openstack/files/mitaka/horizon/sudo/views.py A modules/openstack/files/mitaka/horizon/sudo/workflows.py A modules/openstack/files/mitaka/horizon/sudo_enable.py A modules/openstack/files/mitaka/horizon/sudo_group_add.py M modules/openstack/manifests/horizon/service.pp 11 files changed, 733 insertions(+), 0 deletions(-) Approvals: Andrew Bogott: Looks good to me, approved jenkins-bot: Verified diff --git a/modules/openstack/files/mitaka/horizon/sudo/__init__.py b/modules/openstack/files/mitaka/horizon/sudo/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/sudo/__init__.py diff --git a/modules/openstack/files/mitaka/horizon/sudo/panel.py b/modules/openstack/files/mitaka/horizon/sudo/panel.py new file mode 100644 index 0000000..e1ff773 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/sudo/panel.py @@ -0,0 +1,27 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.utils.translation import ugettext_lazy as _ +import horizon + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +class ProjectSudoPanel(horizon.Panel): + name = _("Project Sudo") + slug = "sudo" diff --git a/modules/openstack/files/mitaka/horizon/sudo/sudorules.py b/modules/openstack/files/mitaka/horizon/sudo/sudorules.py new file mode 100644 index 0000000..1172a11 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/sudo/sudorules.py @@ -0,0 +1,217 @@ +# Copyright (c) 2017 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ldap +import ldap.modlist +import logging + +from django.conf import settings + +from horizon import exceptions + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +# A single sudoer rule, with some human readable labels +class SudoRule: + def _get_formatted_user_list(self, userlist): + projmembers = '%%project-%s' % self.project + listcopy = list(userlist) + if projmembers in listcopy: + listcopy.remove(projmembers) + listcopy.insert(0, "[Any project member]") + + if 'ALL' in listcopy: + listcopy.remove('ALL') + listcopy.insert(0, "[Anyone]") + + return ', '.join(listcopy) + + def __init__(self, + project, + name, + users, + runas, + commands, + options): + self.id = name + self.project = project + self.name = name + self.users = users + self.runas = runas + self.commands = commands + self.options = options + + self.users_hr = self._get_formatted_user_list(users) + self.runas_hr = self._get_formatted_user_list(runas) + + self.commands_hr = ', '.join(commands) + + if '!authenticate' in self.options: + self.authrequired = False + else: + self.authrequired = True + + if '!authenticate' in options: + options.remove('!authenticate') + if 'authenticate' in options: + options.remove('authenticate') + + self.options_hr = ', '.join(options) + + +def _getLdapInfo(attr, conffile="/etc/ldap.conf"): + try: + f = open(conffile) + except IOError: + if conffile == "/etc/ldap.conf": + # fallback to /etc/ldap/ldap.conf, which will likely + # have less information + f = open("/etc/ldap/ldap.conf") + for line in f: + if line.strip() == "": + continue + if line.split()[0].lower() == attr.lower(): + return line.split(None, 1)[1].strip() + break + + +def _open_ldap(): + ldapHost = _getLdapInfo("uri") + sslType = _getLdapInfo("ssl") + + binddn = getattr(settings, "LDAP_USER", '') + bindpw = getattr(settings, "LDAP_USER_PASSWORD", '') + + ds = ldap.initialize(ldapHost) + ds.protocol_version = ldap.VERSION3 + if sslType == "start_tls": + ds.start_tls_s() + + try: + ds.simple_bind_s(binddn, bindpw) + return ds + except ldap.CONSTRAINT_VIOLATION: + LOG.error("LDAP bind failure: Too many failed attempts.\n") + except ldap.INVALID_DN_SYNTAX: + LOG.error("LDAP bind failure: The bind DN is incorrect... \n") + except ldap.NO_SUCH_OBJECT: + LOG.error("LDAP bind failure: " + "Unable to locate the bind DN account.\n") + except ldap.UNWILLING_TO_PERFORM as msg: + LOG.error("LDAP bind failure: " + "The LDAP server was unwilling to perform the action" + " requested.\nError was: %s\n" % msg[0]["info"]) + except ldap.INVALID_CREDENTIALS: + LOG.error("LDAP bind failure: Password incorrect.\n") + + LOG.error("Failed to connect to ldap.") + raise exceptions.ConfigurationError() + + +def rules_for_project(project, rulename=None): + LOG.debug("getting rules for %s" % project) + projects_basedn = getattr(settings, "LDAP_PROJECTS_BASE", '') + sudoer_base = "ou=sudoers,cn=%s,%s" % (project, projects_basedn) + rules = [] + + ds = _open_ldap() + + if rulename: + filter = "(&(objectClass=sudorole)(cn=%s))" % rulename + else: + filter = '(objectClass=sudorole)' + + sudorecords = ds.search_s(sudoer_base, + ldap.SCOPE_ONELEVEL, + filterstr=filter) + + for record in sudorecords: + content = record[1] + + name = content.get('cn', [''])[0] + users = content.get("sudoUser", []) + runas = content.get("sudoRunAsUser", []) + command = content.get("sudoCommand", []) + options = content.get("sudoOption", []) + + rule = SudoRule(project, + name, + users, + runas, + command, + options) + rules.append(rule) + + return rules + + +def _dn_for_rule(rule): + projects_basedn = getattr(settings, "LDAP_PROJECTS_BASE", '') + sudoer_base = "ou=sudoers,cn=%s,%s" % (rule.project, projects_basedn) + return "cn=%s,%s" % (rule.name, sudoer_base) + + +def _modentry_for_rule(rule): + ruleEntry = {} + ruleEntry['cn'] = rule.name.encode('utf8') + ruleEntry['objectClass'] = 'sudoRole' + ruleEntry['sudoHost'] = 'ALL' + ruleEntry['sudoOption'] = [opt.encode('utf8') for opt in rule.options] + ruleEntry['sudoCommand'] = [cmd.encode('utf8') for cmd in rule.commands] + ruleEntry['sudoUser'] = [usr.encode('utf8') for usr in rule.users] + ruleEntry['sudoRunAsUser'] = [usr.encode('utf8') for usr in rule.runas] + + if not rule.authrequired: + ruleEntry['sudoOption'].append("!authenticate") + + return ruleEntry + + +def add_rule(rule): + ds = _open_ldap() + + dn = _dn_for_rule(rule) + modentry = _modentry_for_rule(rule) + modlist = ldap.modlist.addModlist(modentry) + ds.add_s(dn, modlist) + return True + + +def update_rule(rule): + ds = _open_ldap() + + dn = _dn_for_rule(rule) + newentry = _modentry_for_rule(rule) + + # get the old rule so we can make a proper modlist. This is potentially + # racy but less racy than caching it elsewhere. + oldrecords = ds.search_s(dn, ldap.SCOPE_BASE) + + modlist = ldap.modlist.modifyModlist(oldrecords[0][1], newentry) + ds.modify_s(dn, modlist) + return True + + +def delete_rule(project, rulename): + ds = _open_ldap() + + projects_basedn = getattr(settings, "LDAP_PROJECTS_BASE", '') + sudoer_base = "ou=sudoers,cn=%s,%s" % (project, projects_basedn) + + dn = "cn=%s,%s" % (rulename, sudoer_base) + ds.delete_s(dn) + return True diff --git a/modules/openstack/files/mitaka/horizon/sudo/templates/sudo/_common_horizontal_form.html b/modules/openstack/files/mitaka/horizon/sudo/templates/sudo/_common_horizontal_form.html new file mode 100644 index 0000000..97f0ef9 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/sudo/templates/sudo/_common_horizontal_form.html @@ -0,0 +1,3 @@ +<div class="form-horizontal"> + {% include "horizon/common/_horizontal_fields.html" %} +</div> diff --git a/modules/openstack/files/mitaka/horizon/sudo/templates/sudo/index.html b/modules/openstack/files/mitaka/horizon/sudo/templates/sudo/index.html new file mode 100644 index 0000000..6f667f2 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/sudo/templates/sudo/index.html @@ -0,0 +1,13 @@ +{% extends 'base.html' %} +{% load i18n %} +{% block title %}{% trans "Sudoer Policies" %}{% endblock %} + +{% block page_header %} + {% include "horizon/common/_page_header.html" with title=_("Sudoer Policy") %} +{% endblock page_header %} + +{% block main %} + {{ table.render }} +{% endblock %} + + diff --git a/modules/openstack/files/mitaka/horizon/sudo/urls.py b/modules/openstack/files/mitaka/horizon/sudo/urls.py new file mode 100644 index 0000000..955fa7e --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/sudo/urls.py @@ -0,0 +1,26 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +from django.conf.urls import url, patterns + +from wikimediasudodashboard import views + +urlpatterns = patterns( + '', + url(r'^$', views.IndexView.as_view(), name='index'), + url(r'^create/$', views.CreateView.as_view(), name='create'), + url(r'^(?P<rule_name>[^/]+)/modify/$', + views.ModifyView.as_view(), name='modify'), + +) diff --git a/modules/openstack/files/mitaka/horizon/sudo/views.py b/modules/openstack/files/mitaka/horizon/sudo/views.py new file mode 100644 index 0000000..d7b5f7f --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/sudo/views.py @@ -0,0 +1,141 @@ +# Copyright (c) 2016 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.utils.translation import ungettext_lazy +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import tables +from horizon import workflows + +import sudorules +import workflows as sudo_workflows + +logging.basicConfig() +LOG = logging.getLogger(__name__) + + +class AddRule(tables.LinkAction): + name = "adda" + verbose_name = _("Add Rule") + url = "horizon:project:sudo:create" + classes = ("ajax-modal",) + icon = "plus" + # todo: Make real nova policy rules for this + policy_rules = (("dns", "create_record"),) + + +class ModifyRule(tables.LinkAction): + name = "modify" + verbose_name = _("Modify Rule") + url = "horizon:project:sudo:modify" + classes = ("ajax-modal",) + + # todo: Make real nova policy rules for this + policy_rules = (("dns", "create_record"),) + + +class DeleteRule(tables.DeleteAction): + + @staticmethod + def action_present(count): + return ungettext_lazy(u"Delete Rule", u"Delete Rules", count) + + @staticmethod + def action_past(count): + return ungettext_lazy(u"Deleted Rule", u"Deleted Rules", count) + + # todo: Make real nova policy rules for this + policy_rules = (("dns", "create_record"),) + + def delete(self, request, obj_id): + project_id = request.user.tenant_id + sudorules.delete_rule(project_id, obj_id) + + +class SudoTable(tables.DataTable): + name = tables.Column("name", verbose_name=_("Sudo policy name"),) + users = tables.Column("users_hr", verbose_name=_("Users"),) + runas = tables.Column("runas_hr", verbose_name=_("Allow running as"),) + commands = tables.Column("commands_hr", verbose_name=_("Commands"),) + options = tables.Column("options_hr", verbose_name=_("Options"),) + authenticate = tables.Column("authrequired", + verbose_name=_("Require Password"),) + + class Meta(object): + name = "proxies" + verbose_name = _("Sudo Policies") + table_actions = (AddRule, DeleteRule, ) + row_actions = (ModifyRule, DeleteRule, ) + + +def get_sudo_rule_list(request): + project = request.user.tenant_id + rules = [] + try: + rules = sudorules.rules_for_project(project) + except Exception: + exceptions.handle(request, _("Unable to retrieve sudo rules.")) + return rules + + +class IndexView(tables.DataTableView): + table_class = SudoTable + template_name = 'project/sudo/index.html' + page_title = _("Sudo Policies") + + def get_data(self): + return get_sudo_rule_list(self.request) + + +class CreateView(workflows.WorkflowView): + workflow_class = sudo_workflows.CreateRule + + def get_initial(self): + initial = super(CreateView, self).get_initial() + initial['project_id'] = self.request.user.tenant_id + initial['rulename'] = 'newrule' + initial['commands'] = 'ALL' + initial[sudo_workflows.SUDO_USER_ROLE_NAME] = [sudo_workflows.allUsersTuple( + self.request.user.tenant_id)[0]] + initial[sudo_workflows.SUDO_RUNAS_ROLE_NAME] = [] + + return initial + + +class ModifyView(workflows.WorkflowView): + workflow_class = sudo_workflows.ModifyRule + + def get_initial(self): + initial = super(ModifyView, self).get_initial() + + project = self.request.user.tenant_id + rulename = self.kwargs['rule_name'] + + rule = sudorules.rules_for_project(project, rulename)[0] + + initial['project_id'] = project + initial['rulename'] = rulename + + initial['commands'] = "\n".join(rule.commands) + initial[sudo_workflows.SUDO_USER_ROLE_NAME] = rule.users + initial[sudo_workflows.SUDO_RUNAS_ROLE_NAME] = rule.runas + initial['options'] = "\n".join(rule.options) + + initial['authrequired'] = rule.authrequired + + return initial diff --git a/modules/openstack/files/mitaka/horizon/sudo/workflows.py b/modules/openstack/files/mitaka/horizon/sudo/workflows.py new file mode 100644 index 0000000..c68f070 --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/sudo/workflows.py @@ -0,0 +1,267 @@ +# Copyright (c) 2017 Andrew Bogott for Wikimedia Foundation +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging + +from django.utils.translation import ugettext_lazy as _ + +from horizon import exceptions +from horizon import forms +from horizon import workflows + +from openstack_dashboard.api import keystone + +import sudorules + +LOG = logging.getLogger(__name__) + +SUDO_USER_MEMBER_SLUG = 'sudo_users' +SUDO_RUNAS_SLUG = 'sudo_runas' +COMMON_HORIZONTAL_TEMPLATE = "project/sudo/_common_horizontal_form.html" + +SUDO_USER_ROLE_NAME = 'user' +SUDO_RUNAS_ROLE_NAME = 'runas' + +NO_SUDO_FOR = ['novaadmin', 'novaobserver'] + + +def allUsersTuple(project_id): + return ("%%project-%s" % project_id, "[Any project member]") + + +def anyUserTuple(): + return ("ALL", "[Anyone]") + + +class UpdateRuleUsersAction(workflows.MembershipAction): + role_name = SUDO_USER_ROLE_NAME + + def __init__(self, request, *args, **kwargs): + super(UpdateRuleUsersAction, self).__init__(request, + *args, + **kwargs) + err_msg = _('Unable to retrieve user list. Please try again later.') + + project_id = self.initial['project_id'] + + # The user-selection widget we're using thinks in terms of roles. We only want + # one, simple list so we will collect them in the stand-in 'user' role. + default_role_name = self.get_default_role_field_name() + self.fields[default_role_name] = forms.CharField(required=False) + self.fields[default_role_name].initial = self.role_name + + # Get list of available users + all_users = [] + try: + # We can't use the default user_list function because it requires + # us to be an admin user. + users = keystone.keystoneclient(request).users.list(default_project=project_id) + all_users = [keystone.VERSIONS.upgrade_v2_user(user) for user in users] + all_users_dict = {user.id: user for user in all_users} + except Exception: + exceptions.handle(request, err_msg) + + # The v3 user list doesn't actually filter by project (code comments + # to the contrary) so we have to dig through the role list to find + # out who's actually in our project. + # Anyone who is in all_users_dict and also has a role in the + # project is a potential sudoer. + project_users = set() + manager = keystone.keystoneclient(request).role_assignments + project_role_assignments = manager.list(project=project_id) + for role_assignment in project_role_assignments: + if not hasattr(role_assignment, 'user'): + continue + user_id = role_assignment.user['id'] + if user_id in NO_SUDO_FOR: + continue + if user_id in all_users_dict: + project_users.add(all_users_dict[user_id]) + + users_list = [(user.id, user.name) for user in project_users] + users_list.insert(0, anyUserTuple()) + users_list.insert(0, allUsersTuple(project_id)) + + # Add a field to collect the list of users with role 'user' + field_name = self.get_member_field_name(self.role_name) + label = self.role_name + self.fields[field_name] = forms.MultipleChoiceField(required=False, + label=label) + self.fields[field_name].choices = users_list + self.fields[field_name].initial = self.initial[self.role_name] + + class Meta(object): + name = _("Users") + slug = SUDO_USER_MEMBER_SLUG + + +class UpdateRuleUsers(workflows.UpdateMembersStep): + action_class = UpdateRuleUsersAction + available_list_title = _("") + members_list_title = _("Rule Users") + no_available_text = _("No users found.") + no_members_text = _("No users.") + show_roles = False + role_name = SUDO_USER_ROLE_NAME + contributes = (SUDO_USER_ROLE_NAME,) + + def contribute(self, data, context): + if data: + post = self.workflow.request.POST + + field_name = self.get_member_field_name(self.role_name) + context[self.role_name] = post.getlist(field_name) + return context + + +class UpdateRuleRunAsUsersAction(UpdateRuleUsersAction): + role_name = SUDO_RUNAS_ROLE_NAME + + class Meta(object): + name = _("Run as") + slug = SUDO_RUNAS_SLUG + + +class UpdateRuleRunAsUsers(UpdateRuleUsers): + action_class = UpdateRuleRunAsUsersAction + available_list_title = _("") + members_list_title = _("Allow running as") + role_name = SUDO_RUNAS_ROLE_NAME + contributes = (SUDO_RUNAS_ROLE_NAME,) + + +LDAP_TEXT_VALIDATOR = "^[A-Za-z][\w_\-\.]*$" +LDAP_TEXT_VALIDATOR_MESSAGES = {'invalid': + _("This must start with a letter, " + "followed by only letters, numbers, ., -, or _.")} + + +class CreateRuleInfoAction(workflows.Action): + # Hide the domain_id and domain_name by default + project_id = forms.CharField(label=_("Project ID"), + required=False, + widget=forms.HiddenInput()) + rulename = forms.RegexField(label=_("Rule Name"), + max_length=64, + help_text=_("Name of this sudo rule. " + "Must be a unique name within this project."), + regex=LDAP_TEXT_VALIDATOR, + error_messages=LDAP_TEXT_VALIDATOR_MESSAGES, + required=True) + commands = forms.CharField(widget=forms.widgets.Textarea( + attrs={'rows': 4}), + label=_("Commands"), + help_text=_("List of permitted commands, one per line, " + "or ALL to permit all actions."), + required=True) + options = forms.CharField(widget=forms.widgets.Textarea( + attrs={'rows': 2}), + label=_("Options"), + required=False) + authrequired = forms.BooleanField(label=_("Passphrase required"), + required=False, + initial=False) + + def __init__(self, request, *args, **kwargs): + super(CreateRuleInfoAction, self).__init__(request, + *args, + **kwargs) + + class Meta(object): + name = _("Rule") + help_text = _("Create a rule to permit certain sudo commands.") + slug = "rule_info" + + +class CreateRuleInfo(workflows.Step): + action_class = CreateRuleInfoAction + template_name = COMMON_HORIZONTAL_TEMPLATE + contributes = ("rulename", + "commands", + "project_id", + "options", + "authrequired") + + def contribute(self, data, context): + if data: + post = self.workflow.request.POST + + context['commands'] = post.getlist('commands')[0].splitlines() + context['options'] = post.getlist('options')[0].splitlines() + if not post.getlist('authrequired'): + context['options'].append("!authenticate") + context['rulename'] = post.getlist('rulename')[0] + + return context + + +class CreateRule(workflows.Workflow): + slug = "create_sudo_rule" + name = _("Create Rule") + finalize_button_name = _("Create Rule") + success_message = _('Created sudo rule "%s".') + failure_message = _('Unable to create sudo rule "%s".') + success_url = "horizon:project:sudo:index" + default_steps = (CreateRuleInfo, + UpdateRuleUsers, + UpdateRuleRunAsUsers) + + def __init__(self, request=None, context_seed=None, entry_point=None, + *args, **kwargs): + super(CreateRule, self).__init__(request=request, + context_seed=context_seed, + entry_point=entry_point, + *args, + **kwargs) + + def handle(self, request, data): + rule = sudorules.SudoRule(project=data['project_id'], + name=data['rulename'], + users=data[SUDO_USER_ROLE_NAME], + runas=data[SUDO_RUNAS_ROLE_NAME], + commands=data['commands'], + options=data['options']) + + return sudorules.add_rule(rule) + + +class ModifyRule(workflows.Workflow): + slug = "modify_sudo_rule" + name = _("Modify Rule") + finalize_button_name = _("Update Rule") + success_message = _('Changed sudo rule "%s".') + failure_message = _('Unable to change sudo rule "%s".') + success_url = "horizon:project:sudo:index" + default_steps = (CreateRuleInfo, + UpdateRuleUsers, + UpdateRuleRunAsUsers) + + def __init__(self, request=None, context_seed=None, entry_point=None, + *args, **kwargs): + super(ModifyRule, self).__init__(request=request, + context_seed=context_seed, + entry_point=entry_point, + *args, + **kwargs) + + def handle(self, request, data): + rule = sudorules.SudoRule(project=data['project_id'], + name=data['rulename'], + users=data[SUDO_USER_ROLE_NAME], + runas=data[SUDO_RUNAS_ROLE_NAME], + commands=data['commands'], + options=data['options']) + + return sudorules.update_rule(rule) diff --git a/modules/openstack/files/mitaka/horizon/sudo_enable.py b/modules/openstack/files/mitaka/horizon/sudo_enable.py new file mode 100644 index 0000000..4b6061a --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/sudo_enable.py @@ -0,0 +1,6 @@ +PANEL = 'projectsudopanel' +PANEL_GROUP = 'sudoers' +PANEL_DASHBOARD = 'project' +ADD_PANEL = ('wikimediasudodashboard.panel.ProjectSudoPanel') +ADD_INSTALLED_APPS = ['wikimediasudodashboard'] +AUTO_DISCOVER_STATIC_FILES = True diff --git a/modules/openstack/files/mitaka/horizon/sudo_group_add.py b/modules/openstack/files/mitaka/horizon/sudo_group_add.py new file mode 100644 index 0000000..63e8a5c --- /dev/null +++ b/modules/openstack/files/mitaka/horizon/sudo_group_add.py @@ -0,0 +1,6 @@ +# The name of the panel group to be added to HORIZON_CONFIG. Required. +PANEL_GROUP = 'sudoers' +# The display name of the PANEL_GROUP. Required. +PANEL_GROUP_NAME = 'Sudoer Policies' +# The name of the dashboard the PANEL_GROUP associated with. Required. +PANEL_GROUP_DASHBOARD = 'project' diff --git a/modules/openstack/manifests/horizon/service.pp b/modules/openstack/manifests/horizon/service.pp index 02cdf5d..728b5f0 100644 --- a/modules/openstack/manifests/horizon/service.pp +++ b/modules/openstack/manifests/horizon/service.pp @@ -214,6 +214,33 @@ require => Package['python-designate-dashboard', 'openstack-dashboard'], } + # sudo dashboard + file { '/usr/lib/python2.7/dist-packages/wikimediasudodashboard': + source => "puppet:///modules/openstack/${openstack_version}/horizon/sudo", + owner => 'root', + group => 'root', + mode => '0644', + require => Package['python-designate-dashboard', 'openstack-dashboard'], + notify => Exec['djangorefresh'], + recurse => true, + } + file { '/usr/share/openstack-dashboard/openstack_dashboard/local/enabled/_1926_project_sudo_panel.py': + source => "puppet:///modules/openstack/${openstack_version}/horizon/sudo_enable.py", + owner => 'root', + group => 'root', + mode => '0644', + notify => Exec['djangorefresh'], + require => Package['python-designate-dashboard', 'openstack-dashboard'], + } + file { '/usr/share/openstack-dashboard/openstack_dashboard/local/enabled/_72_sudoers_add_group.py': + source => "puppet:///modules/openstack/${openstack_version}/horizon/sudo_group_add.py", + owner => 'root', + group => 'root', + mode => '0644', + notify => Exec['djangorefresh'], + require => Package['python-designate-dashboard', 'openstack-dashboard'], + } + if $openstack_version != 'liberty' { # Override some .js files to provide a simplified user experience. Alas # we can't do this via the overrides.py monkeypatch below -- To view, visit https://gerrit.wikimedia.org/r/353156 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: merged Gerrit-Change-Id: I383456e8d94bbd9488b09550ef4dc9155e0bc69f Gerrit-PatchSet: 11 Gerrit-Project: operations/puppet Gerrit-Branch: production Gerrit-Owner: Andrew Bogott <[email protected]> Gerrit-Reviewer: Andrew Bogott <[email protected]> Gerrit-Reviewer: BryanDavis <[email protected]> Gerrit-Reviewer: jenkins-bot <> _______________________________________________ MediaWiki-commits mailing list [email protected] https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits
