From 11fbd213b10509610e335e03dfb096b72c8ac071 Mon Sep 17 00:00:00 2001 From: epriestley Date: Wed, 9 Oct 2013 14:05:10 -0700 Subject: [PATCH] Custom Policy Editor Summary: Ref T603. This isn't remotely usable yet, but I wanted to get any feedback before I build it out anymore. I think this is a reasonable interface for defining custom policies? It's basically similar to Herald, although it's a bit simpler. I imagine users will rarely interact with this, but this will service the high end of policy complexity (and allow the definition of things like "is member of LDAP group" or whatever). Test Plan: See screenshots. Reviewers: btrahan, chad Reviewed By: btrahan CC: aran, asherkin Maniphest Tasks: T603 Differential Revision: https://secure.phabricator.com/D7217 --- src/__celerity_resource_map__.php | 29 ++- src/__phutil_library_map__.php | 11 ++ .../PhabricatorApplicationPolicy.php | 1 + .../PhabricatorPolicyEditController.php | 153 +++++++++++++++ .../policy/rule/PhabricatorPolicyRule.php | 37 ++++ .../PhabricatorPolicyRuleAdministrators.php | 18 ++ .../rule/PhabricatorPolicyRuleLunarPhase.php | 51 +++++ .../rule/PhabricatorPolicyRuleProjects.php | 67 +++++++ .../rule/PhabricatorPolicyRuleUsers.php | 44 +++++ .../css/application/policy/policy-edit.css | 32 ++++ .../policy/behavior-policy-rule-editor.js | 179 ++++++++++++++++++ 11 files changed, 621 insertions(+), 1 deletion(-) create mode 100644 src/applications/policy/controller/PhabricatorPolicyEditController.php create mode 100644 src/applications/policy/rule/PhabricatorPolicyRule.php create mode 100644 src/applications/policy/rule/PhabricatorPolicyRuleAdministrators.php create mode 100644 src/applications/policy/rule/PhabricatorPolicyRuleLunarPhase.php create mode 100644 src/applications/policy/rule/PhabricatorPolicyRuleProjects.php create mode 100644 src/applications/policy/rule/PhabricatorPolicyRuleUsers.php create mode 100644 webroot/rsrc/css/application/policy/policy-edit.css create mode 100644 webroot/rsrc/js/application/policy/behavior-policy-rule-editor.js diff --git a/src/__celerity_resource_map__.php b/src/__celerity_resource_map__.php index 896b9fbf61..ded8d2387b 100644 --- a/src/__celerity_resource_map__.php +++ b/src/__celerity_resource_map__.php @@ -2265,6 +2265,24 @@ celerity_register_resource_map(array( ), 'disk' => '/rsrc/js/application/pholio/behavior-pholio-mock-view.js', ), + 'javelin-behavior-policy-rule-editor' => + array( + 'uri' => '/res/4ae4249d/rsrc/js/application/policy/behavior-policy-rule-editor.js', + 'type' => 'js', + 'requires' => + array( + 0 => 'javelin-behavior', + 1 => 'multirow-row-manager', + 2 => 'javelin-dom', + 3 => 'javelin-util', + 4 => 'phabricator-prefab', + 5 => 'javelin-tokenizer', + 6 => 'javelin-typeahead', + 7 => 'javelin-typeahead-preloaded-source', + 8 => 'javelin-json', + ), + 'disk' => '/rsrc/js/application/policy/behavior-policy-rule-editor.js', + ), 'javelin-behavior-ponder-votebox' => array( 'uri' => '/res/c28daa12/rsrc/js/application/ponder/behavior-votebox.js', @@ -3886,13 +3904,22 @@ celerity_register_resource_map(array( ), 'policy-css' => array( - 'uri' => '/res/ebb12aa0/rsrc/css/application/policy/policy.css', + 'uri' => '/res/51325bff/rsrc/css/application/policy/policy.css', 'type' => 'css', 'requires' => array( ), 'disk' => '/rsrc/css/application/policy/policy.css', ), + 'policy-edit-css' => + array( + 'uri' => '/res/1e2a2b5e/rsrc/css/application/policy/policy-edit.css', + 'type' => 'css', + 'requires' => + array( + ), + 'disk' => '/rsrc/css/application/policy/policy-edit.css', + ), 'ponder-comment-table-css' => array( 'uri' => '/res/4aa4b865/rsrc/css/application/ponder/comments.css', diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index f4c439d217..5ca6db4847 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1483,6 +1483,7 @@ phutil_register_library_map(array( 'PhabricatorPolicyConstants' => 'applications/policy/constants/PhabricatorPolicyConstants.php', 'PhabricatorPolicyController' => 'applications/policy/controller/PhabricatorPolicyController.php', 'PhabricatorPolicyDataTestCase' => 'applications/policy/__tests__/PhabricatorPolicyDataTestCase.php', + 'PhabricatorPolicyEditController' => 'applications/policy/controller/PhabricatorPolicyEditController.php', 'PhabricatorPolicyException' => 'applications/policy/exception/PhabricatorPolicyException.php', 'PhabricatorPolicyExplainController' => 'applications/policy/controller/PhabricatorPolicyExplainController.php', 'PhabricatorPolicyFilter' => 'applications/policy/filter/PhabricatorPolicyFilter.php', @@ -1491,6 +1492,11 @@ phutil_register_library_map(array( 'PhabricatorPolicyManagementUnlockWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementUnlockWorkflow.php', 'PhabricatorPolicyManagementWorkflow' => 'applications/policy/management/PhabricatorPolicyManagementWorkflow.php', 'PhabricatorPolicyQuery' => 'applications/policy/query/PhabricatorPolicyQuery.php', + 'PhabricatorPolicyRule' => 'applications/policy/rule/PhabricatorPolicyRule.php', + 'PhabricatorPolicyRuleAdministrators' => 'applications/policy/rule/PhabricatorPolicyRuleAdministrators.php', + 'PhabricatorPolicyRuleLunarPhase' => 'applications/policy/rule/PhabricatorPolicyRuleLunarPhase.php', + 'PhabricatorPolicyRuleProjects' => 'applications/policy/rule/PhabricatorPolicyRuleProjects.php', + 'PhabricatorPolicyRuleUsers' => 'applications/policy/rule/PhabricatorPolicyRuleUsers.php', 'PhabricatorPolicyTestCase' => 'applications/policy/__tests__/PhabricatorPolicyTestCase.php', 'PhabricatorPolicyTestObject' => 'applications/policy/__tests__/PhabricatorPolicyTestObject.php', 'PhabricatorPolicyType' => 'applications/policy/constants/PhabricatorPolicyType.php', @@ -3672,12 +3678,17 @@ phutil_register_library_map(array( 'PhabricatorPolicyConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorPolicyController' => 'PhabricatorController', 'PhabricatorPolicyDataTestCase' => 'PhabricatorTestCase', + 'PhabricatorPolicyEditController' => 'PhabricatorPolicyController', 'PhabricatorPolicyException' => 'Exception', 'PhabricatorPolicyExplainController' => 'PhabricatorPolicyController', 'PhabricatorPolicyManagementShowWorkflow' => 'PhabricatorPolicyManagementWorkflow', 'PhabricatorPolicyManagementUnlockWorkflow' => 'PhabricatorPolicyManagementWorkflow', 'PhabricatorPolicyManagementWorkflow' => 'PhutilArgumentWorkflow', 'PhabricatorPolicyQuery' => 'PhabricatorQuery', + 'PhabricatorPolicyRuleAdministrators' => 'PhabricatorPolicyRule', + 'PhabricatorPolicyRuleLunarPhase' => 'PhabricatorPolicyRule', + 'PhabricatorPolicyRuleProjects' => 'PhabricatorPolicyRule', + 'PhabricatorPolicyRuleUsers' => 'PhabricatorPolicyRule', 'PhabricatorPolicyTestCase' => 'PhabricatorTestCase', 'PhabricatorPolicyTestObject' => 'PhabricatorPolicyInterface', 'PhabricatorPolicyType' => 'PhabricatorPolicyConstants', diff --git a/src/applications/policy/application/PhabricatorApplicationPolicy.php b/src/applications/policy/application/PhabricatorApplicationPolicy.php index c9eec4bcef..54c55aefaa 100644 --- a/src/applications/policy/application/PhabricatorApplicationPolicy.php +++ b/src/applications/policy/application/PhabricatorApplicationPolicy.php @@ -15,6 +15,7 @@ final class PhabricatorApplicationPolicy extends PhabricatorApplication { '/policy/' => array( 'explain/(?P[^/]+)/(?P[^/]+)/' => 'PhabricatorPolicyExplainController', + 'edit/' => 'PhabricatorPolicyEditController', ), ); } diff --git a/src/applications/policy/controller/PhabricatorPolicyEditController.php b/src/applications/policy/controller/PhabricatorPolicyEditController.php new file mode 100644 index 0000000000..a5d7d462d2 --- /dev/null +++ b/src/applications/policy/controller/PhabricatorPolicyEditController.php @@ -0,0 +1,153 @@ +getRequest(); + $viewer = $request->getUser(); + + $root_id = celerity_generate_unique_node_id(); + + $action_options = array( + 'allow' => pht('Allow'), + 'deny' => pht('Deny'), + ); + + $rules = id(new PhutilSymbolLoader()) + ->setAncestorClass('PhabricatorPolicyRule') + ->loadObjects(); + + $rules = msort($rules, 'getRuleOrder'); + + $default_value = 'deny'; + $default_rule = array( + 'action' => head_key($action_options), + 'rule' => head_key($rules), + 'value' => null, + ); + + if ($request->isFormPost()) { + $data = $request->getStr('rules'); + $data = @json_decode($data, true); + if (!is_array($data)) { + throw new Exception("Failed to JSON decode rule data!"); + } + + $rule_data = array(); + foreach ($data as $rule) { + $action = idx($rule, 'action'); + switch ($action) { + case 'allow': + case 'deny': + break; + default: + throw new Exception("Invalid action '{$action}'!"); + } + + $rule_class = idx($rule, 'rule'); + if (empty($rules[$rule_class])) { + throw new Exception("Invalid rule class '{$rule_class}'!"); + } + + $rule_obj = $rules[$rule_class]; + + $value = $rule_obj->getValueForStorage(idx($rule, 'value')); + $value = $rule_obj->getValueForDisplay($viewer, $value); + + $rule_data[] = array( + 'action' => $action, + 'rule' => $rule_class, + 'value' => $value, + ); + } + + $default_value = $request->getStr('default'); + } else { + $rule_data = array( + $default_rule, + ); + } + + $default_select = AphrontFormSelectControl::renderSelectTag( + $default_value, + $action_options, + array( + 'name' => 'default', + )); + + + $form = id(new PHUIFormLayoutView()) + ->appendChild( + javelin_tag( + 'input', + array( + 'type' => 'hidden', + 'name' => 'rules', + 'sigil' => 'rules', + ))) + ->appendChild( + id(new AphrontFormInsetView()) + ->setTitle(pht('Rules')) + ->setRightButton( + javelin_tag( + 'a', + array( + 'href' => '#', + 'class' => 'button green', + 'sigil' => 'create-rule', + 'mustcapture' => true + ), + pht('New Rule'))) + ->setDescription( + pht('These rules are processed in order.')) + ->setContent(javelin_tag( + 'table', + array( + 'sigil' => 'rules', + 'class' => 'policy-rules-table' + ), + ''))) + ->appendChild( + id(new AphrontFormMarkupControl()) + ->setLabel(pht('If No Rules Match')) + ->setValue(pht( + "%s all other users.", + $default_select))); + + $form = phutil_tag( + 'div', + array( + 'id' => $root_id, + ), + $form); + + $rule_options = mpull($rules, 'getRuleDescription'); + $type_map = mpull($rules, 'getValueControlType'); + $templates = mpull($rules, 'getValueControlTemplate'); + + require_celerity_resource('policy-edit-css'); + Javelin::initBehavior( + 'policy-rule-editor', + array( + 'rootID' => $root_id, + 'actions' => $action_options, + 'rules' => $rule_options, + 'types' => $type_map, + 'templates' => $templates, + 'data' => $rule_data, + 'defaultRule' => $default_rule, + )); + + $dialog = id(new AphrontDialogView()) + ->setWidth(AphrontDialogView::WIDTH_FULL) + ->setUser($viewer) + ->setTitle(pht('Edit Policy')) + ->appendChild($form) + ->addSubmitButton(pht('Save Policy')) + ->addCancelButton('#'); + + return id(new AphrontDialogResponse())->setDialog($dialog); + } + +} diff --git a/src/applications/policy/rule/PhabricatorPolicyRule.php b/src/applications/policy/rule/PhabricatorPolicyRule.php new file mode 100644 index 0000000000..39fc614af2 --- /dev/null +++ b/src/applications/policy/rule/PhabricatorPolicyRule.php @@ -0,0 +1,37 @@ +getIsAdmin(); + } + + public function getValueControlType() { + return self::CONTROL_TYPE_NONE; + } + +} diff --git a/src/applications/policy/rule/PhabricatorPolicyRuleLunarPhase.php b/src/applications/policy/rule/PhabricatorPolicyRuleLunarPhase.php new file mode 100644 index 0000000000..df2fa79674 --- /dev/null +++ b/src/applications/policy/rule/PhabricatorPolicyRuleLunarPhase.php @@ -0,0 +1,51 @@ +isFull(); + case 'new': + return $moon->isNew(); + case 'waxing': + return $moon->isWaxing(); + case 'waning': + return $moon->isWaning(); + } + + return false; + } + + public function getValueControlType() { + return self::CONTROL_TYPE_SELECT; + } + + public function getValueControlTemplate() { + return array( + 'options' => array( + self::PHASE_FULL => pht('is full'), + self::PHASE_NEW => pht('is new'), + self::PHASE_WAXING => pht('is waxing'), + self::PHASE_WANING => pht('is waning'), + ), + ); + } + + public function getRuleOrder() { + return 1000; + } + +} diff --git a/src/applications/policy/rule/PhabricatorPolicyRuleProjects.php b/src/applications/policy/rule/PhabricatorPolicyRuleProjects.php new file mode 100644 index 0000000000..11a08cf878 --- /dev/null +++ b/src/applications/policy/rule/PhabricatorPolicyRuleProjects.php @@ -0,0 +1,67 @@ +setViewer(PhabricatorUser::getOmnipotentUser()) + ->withMemberPHIDs(array($viewer->getPHID())) + ->withPHIDs($values) + ->execute(); + foreach ($projects as $project) { + $this->memberships[$viewer->getPHID()][$project->getPHID()] = true; + } + } + + public function applyRule(PhabricatorUser $viewer, $value) { + foreach ($value as $project_phid) { + if (isset($this->memberships[$viewer->getPHID()][$project_phid])) { + return true; + } + } + return false; + } + + public function getValueControlType() { + return self::CONTROL_TYPE_TOKENIZER; + } + + public function getValueControlTemplate() { + return array( + 'markup' => new AphrontTokenizerTemplateView(), + 'uri' => '/typeahead/common/projects/', + 'placeholder' => pht('Type a project name...'), + ); + } + + public function getRuleOrder() { + return 200; + } + + public function getValueForStorage($value) { + PhutilTypeSpec::newFromString('list')->check($value); + return array_values($value); + } + + public function getValueForDisplay(PhabricatorUser $viewer, $value) { + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($value) + ->execute(); + + return mpull($handles, 'getFullName', 'getPHID'); + } + +} diff --git a/src/applications/policy/rule/PhabricatorPolicyRuleUsers.php b/src/applications/policy/rule/PhabricatorPolicyRuleUsers.php new file mode 100644 index 0000000000..a4f28aa5dc --- /dev/null +++ b/src/applications/policy/rule/PhabricatorPolicyRuleUsers.php @@ -0,0 +1,44 @@ +getPHID()]); + } + + public function getValueControlType() { + return self::CONTROL_TYPE_TOKENIZER; + } + + public function getValueControlTemplate() { + return array( + 'markup' => new AphrontTokenizerTemplateView(), + 'uri' => '/typeahead/common/accounts/', + 'placeholder' => pht('Type a user name...'), + ); + } + + public function getRuleOrder() { + return 100; + } + + public function getValueForStorage($value) { + PhutilTypeSpec::newFromString('list')->check($value); + return array_values($value); + } + + public function getValueForDisplay(PhabricatorUser $viewer, $value) { + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($value) + ->execute(); + + return mpull($handles, 'getFullName', 'getPHID'); + } + +} diff --git a/webroot/rsrc/css/application/policy/policy-edit.css b/webroot/rsrc/css/application/policy/policy-edit.css new file mode 100644 index 0000000000..08446d96ae --- /dev/null +++ b/webroot/rsrc/css/application/policy/policy-edit.css @@ -0,0 +1,32 @@ +/** + * @provides policy-edit-css + */ + +.policy-rules-table { + width: 100%; +} + +.policy-rules-table td { + padding: 4px; + width: 32px; + vertical-align: middle; +} + +.policy-rules-table td.action-cell { + width: 120px; +} + +.policy-rules-table td.rule-cell { + width: 180px; +} + +.policy-rules-table td.value-cell { + width: auto; + padding-right: 12px; +} + +.policy-rules-table td.action-cell select, +.policy-rules-table td.rule-cell select, +.policy-rules-table td input { + width: 100%; +} diff --git a/webroot/rsrc/js/application/policy/behavior-policy-rule-editor.js b/webroot/rsrc/js/application/policy/behavior-policy-rule-editor.js new file mode 100644 index 0000000000..a7b77376e8 --- /dev/null +++ b/webroot/rsrc/js/application/policy/behavior-policy-rule-editor.js @@ -0,0 +1,179 @@ +/** + * @provides javelin-behavior-policy-rule-editor + * @requires javelin-behavior + * multirow-row-manager + * javelin-dom + * javelin-util + * phabricator-prefab + * javelin-tokenizer + * javelin-typeahead + * javelin-typeahead-preloaded-source + * javelin-json + */ +JX.behavior('policy-rule-editor', function(config) { + var root = JX.$(config.rootID); + var rows = []; + var data = {}; + + JX.DOM.listen( + root, + 'click', + 'create-rule', + function(e) { + e.kill(); + new_rule(config.defaultRule); + }); + + JX.DOM.listen( + root, + 'change', + 'rule-select', + function(e) { + e.kill(); + + var row = e.getNode(JX.MultirowRowManager.getRowSigil()); + var row_id = rules_manager.getRowID(row); + + data[row_id].rule = data[row_id].ruleNode.value; + data[row_id].value = null; + + redraw(row_id); + }); + + JX.DOM.listen( + JX.DOM.findAbove(root, 'form'), + 'submit', + null, + function(e) { + var rules = JX.DOM.find(e.getNode('tag:form'), 'input', 'rules'); + + var value = []; + for (var ii = 0; ii < rows.length; ii++) { + var row_data = data[rows[ii]]; + + var row_dict = { + action: row_data.actionNode.value, + rule: row_data.rule, + value: row_data.getValue() + }; + + value.push(row_dict); + } + + rules.value = JX.JSON.stringify(value); + }); + + + var rules_table = JX.DOM.find(root, 'table', 'rules'); + var rules_manager = new JX.MultirowRowManager(rules_table); + rules_manager.listen( + 'row-removed', + function(row_id) { + delete data[row_id]; + for (var ii = 0; ii < rows.length; ii++) { + if (rows[ii] == row_id) { + rows.splice(ii, 1); + break; + } + } + }); + + + function new_rule(spec) { + var row = rules_manager.addRow([]); + var row_id = rules_manager.getRowID(row); + + rows.push(row_id); + data[row_id] = JX.copy({}, spec); + + redraw(row_id); + } + + function redraw(row_id) { + var action_content = JX.Prefab.renderSelect( + config.actions, + data[row_id].action); + data[row_id].actionNode = action_content; + var action_cell = JX.$N('td', {className: "action-cell"}, action_content); + + var rule_content = JX.Prefab.renderSelect( + config.rules, + data[row_id].rule, + {sigil: 'rule-select'}); + data[row_id].ruleNode = rule_content; + var rule_cell = JX.$N('td', {className: "rule-cell"}, rule_content); + + var input = render_input(data[row_id].rule, null); + + var value_content = input.node; + data[row_id].getValue = input.get; + input.set(data[row_id].value); + + var value_cell = JX.$N('td', {className: "value-cell"}, value_content); + + rules_manager.updateRow(row_id, [action_cell, rule_cell, value_cell]); + } + + function render_input(rule, value) { + var node, get_fn, set_fn; + var type = config.types[rule]; + var template = config.templates[rule]; + + switch (type) { + case 'tokenizer': + node = JX.$H(template.markup).getNode(); + node.id = ''; + + var datasource = new JX.TypeaheadPreloadedSource(template.uri); + + var typeahead = new JX.Typeahead(node); + typeahead.setDatasource(datasource); + + var tokenizer = new JX.Tokenizer(node); + tokenizer.setLimit(template.limit); + tokenizer.setTypeahead(typeahead); + tokenizer.setPlaceholder(template.placeholder); + tokenizer.start(); + + get_fn = function() { return JX.keys(tokenizer.getTokens()); }; + set_fn = function(map) { + if (!map) { + return; + } + for (var k in map) { + tokenizer.addToken(k, map[k]); + } + }; + break; + case 'none': + node = null; + get_fn = JX.bag; + set_fn = JX.bag; + break; + case 'select': + node = JX.Prefab.renderSelect( + config.templates[rule].options, + value); + get_fn = function() { return node.value; }; + set_fn = function(v) { node.value = v; }; + break; + default: + case 'text': + node = JX.$N('input', {type: 'text'}); + get_fn = function() { return node.value; }; + set_fn = function(v) { node.value = v; }; + break; + } + + return { + node: node, + get: get_fn, + set: set_fn + }; + } + + for (var ii = 0; ii < config.data.length; ii++) { + new_rule(config.data[ii]); + } + +});