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

Reply via email to