admission 0.1.9 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +4 -1
- data/Gemfile +5 -1
- data/bin/server.rb +26 -0
- data/lib/admission/arbitration.rb +2 -0
- data/lib/admission/privilege.rb +6 -0
- data/lib/admission/resource_arbitration.rb +1 -0
- data/lib/admission/status.rb +23 -4
- data/lib/admission/version.rb +1 -1
- data/lib/admission/visualisation.rb +81 -0
- data/spec/integration/resource_arbitrating_spec.rb +0 -1
- data/spec/test_context/privileges_and_rules.rb +1 -1
- data/spec/unit/privilege_spec.rb +27 -0
- data/spec/unit/status_spec.rb +21 -3
- data/visualisation/.babelrc +7 -0
- data/visualisation/actions/index.js +0 -0
- data/visualisation/components/app_container.jsx +73 -0
- data/visualisation/components/input_with_select.jsx +164 -0
- data/visualisation/components/privilege_select.jsx +51 -0
- data/visualisation/components/privileges_panel.jsx +14 -0
- data/visualisation/dist/.gitkeep +0 -0
- data/visualisation/index.jsx +132 -0
- data/visualisation/package.json +24 -0
- data/visualisation/reducers/index.js +30 -0
- data/visualisation/style.scss +143 -0
- data/visualisation/webpack.config.js +32 -0
- data/visualisation/yarn.lock +3326 -0
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4eb5d14f7cda027b322111bf9f1e065063510222
|
4
|
+
data.tar.gz: 80dabfdd53d16c0300a53d2aea8aee8e93e5e2c7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 35eed6cd8fcc1219ef8e9073f1b790ecdea387550c8abfb158777e9c8afea4fde0340e08729b1460c366fea9c2651287b6b98f2555b2c884dcec63dd73ac3aab
|
7
|
+
data.tar.gz: bd0badfc8814b1dadaa883188ddfc2bfb110a9727b552140c3a54803577f6639cd08f58bb78b0713eda310617c5328075e34ecfd072bdad78391baafc4bc76b3
|
data/.gitignore
CHANGED
data/Gemfile
CHANGED
data/bin/server.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
# Set up gems listed in the Gemfile.
|
5
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
|
6
|
+
require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
|
7
|
+
|
8
|
+
require 'rack'
|
9
|
+
require 'byebug'
|
10
|
+
|
11
|
+
require_relative '../lib/admission'
|
12
|
+
require_relative '../lib/admission/visualisation'
|
13
|
+
require_relative '../spec/test_context/index'
|
14
|
+
|
15
|
+
Admission::Visualisation.set :js_entry,
|
16
|
+
Admission::Visualisation::ASSETS_PATH.join('build', 'admission_visualisation.js')
|
17
|
+
|
18
|
+
Admission::Visualisation.set :admission_data,
|
19
|
+
{
|
20
|
+
order: PRIVILEGES_ORDER,
|
21
|
+
rules: ACTIONS_RULES,
|
22
|
+
arbitrator: Admission::Arbitration
|
23
|
+
}
|
24
|
+
|
25
|
+
|
26
|
+
Rack::Handler::WEBrick.run Admission::Visualisation
|
data/lib/admission/privilege.rb
CHANGED
@@ -30,6 +30,12 @@ class Admission::Privilege
|
|
30
30
|
hash == other.hash
|
31
31
|
end
|
32
32
|
|
33
|
+
def eql_or_inherits? sought
|
34
|
+
return true if eql? sought
|
35
|
+
return false unless inherited
|
36
|
+
inherited.any?{|pi| pi.eql_or_inherits? sought}
|
37
|
+
end
|
38
|
+
|
33
39
|
def text_key
|
34
40
|
level == BASE_LEVEL_NAME ? name.to_s : "#{name}-#{level}"
|
35
41
|
end
|
data/lib/admission/status.rb
CHANGED
@@ -4,13 +4,25 @@ class Admission::Status
|
|
4
4
|
|
5
5
|
def initialize person, privileges, rules, arbiter
|
6
6
|
@person = person
|
7
|
-
@privileges = (privileges.nil? || privileges.empty?) ? nil : privileges
|
8
7
|
@rules = rules
|
9
8
|
@arbiter = arbiter
|
9
|
+
|
10
|
+
@privileges = if privileges.nil? || privileges.empty?
|
11
|
+
nil
|
12
|
+
|
13
|
+
else
|
14
|
+
grouped = privileges.inject Hash.new do |h, p|
|
15
|
+
hash = p.context.hash rescue nil.hash
|
16
|
+
(h[hash] ||= []) << p
|
17
|
+
h
|
18
|
+
end
|
19
|
+
|
20
|
+
grouped.values.flatten.freeze
|
21
|
+
end
|
10
22
|
end
|
11
23
|
|
12
24
|
def can? *args
|
13
|
-
return false unless
|
25
|
+
return false unless privileges
|
14
26
|
process_request @arbiter.new(person, rules, *args)
|
15
27
|
end
|
16
28
|
|
@@ -26,11 +38,18 @@ class Admission::Status
|
|
26
38
|
end
|
27
39
|
end
|
28
40
|
|
41
|
+
def has? sought
|
42
|
+
return false unless privileges
|
43
|
+
|
44
|
+
list = privilege.context ? privileges.select{|p| p.context == privilege.context} : privileges
|
45
|
+
list.any?{|p| p.eql_or_inherits? sought}
|
46
|
+
end
|
47
|
+
|
29
48
|
def allowed_in_contexts *args
|
30
|
-
return [] unless
|
49
|
+
return [] unless privileges
|
31
50
|
arbitration = @arbiter.new person, rules, *args
|
32
51
|
|
33
|
-
|
52
|
+
privileges.reduce [] do |list, privilege|
|
34
53
|
context = privilege.context
|
35
54
|
|
36
55
|
unless list.include? context
|
data/lib/admission/version.rb
CHANGED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'sinatra/json'
|
3
|
+
require 'haml'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
class Admission::Visualisation < Sinatra::Base
|
7
|
+
ASSETS_PATH = Pathname.new(__FILE__).join('..', '..', '..', 'visualisation')
|
8
|
+
|
9
|
+
enable :inline_templates
|
10
|
+
|
11
|
+
set :js_entry, ASSETS_PATH.join('dist', 'admission_visualisation.js')
|
12
|
+
|
13
|
+
get '/' do
|
14
|
+
haml :index
|
15
|
+
end
|
16
|
+
|
17
|
+
get '/admission_visualisation.js' do
|
18
|
+
send_file settings.js_entry
|
19
|
+
end
|
20
|
+
|
21
|
+
get '/admission_data' do
|
22
|
+
json Admission::Visualisation.admission_data_to_js(
|
23
|
+
**settings.admission_data)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.admission_data_to_js order:, rules:, arbitrator:, **_
|
27
|
+
js_data = {}
|
28
|
+
|
29
|
+
top_levels = []
|
30
|
+
privileges = order.values.inject Array.new do |arr, levels|
|
31
|
+
tops, others = levels.to_a.partition{|key, _| key == :'^'}
|
32
|
+
tops.first[1].tap{|privilege| top_levels << privilege.text_key}
|
33
|
+
|
34
|
+
others.each do |_, privilege|
|
35
|
+
arr << {name: privilege.name, level: privilege.level,
|
36
|
+
inherits: privilege.inherited && privilege.inherited.map(&:text_key)}
|
37
|
+
end
|
38
|
+
|
39
|
+
arr
|
40
|
+
end
|
41
|
+
js_data[:top_levels] = top_levels
|
42
|
+
js_data[:privileges] = privileges
|
43
|
+
|
44
|
+
rules = if arbitrator == Admission::Arbitration
|
45
|
+
rules.to_a.map do |scope, index|
|
46
|
+
|
47
|
+
index = index.to_a.map do |privilege, rule|
|
48
|
+
if rule.is_a? Proc
|
49
|
+
rule = 'proc'
|
50
|
+
end
|
51
|
+
|
52
|
+
[privilege.text_key, rule]
|
53
|
+
end
|
54
|
+
|
55
|
+
[scope, Hash[index]]
|
56
|
+
end
|
57
|
+
|
58
|
+
else
|
59
|
+
raise "not implemented for #{arbitrator.name}"
|
60
|
+
|
61
|
+
end
|
62
|
+
js_data[:rules] = Hash[rules]
|
63
|
+
|
64
|
+
js_data
|
65
|
+
end
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
__END__
|
70
|
+
|
71
|
+
@@ layout
|
72
|
+
!!! 5
|
73
|
+
%html
|
74
|
+
%head
|
75
|
+
%title= 'Admission'
|
76
|
+
%body.flex-column
|
77
|
+
= yield
|
78
|
+
|
79
|
+
@@ index
|
80
|
+
#admission-visualisation(data-url="/admission_data")
|
81
|
+
%script(src="/admission_visualisation.js")
|
data/spec/unit/privilege_spec.rb
CHANGED
@@ -68,6 +68,33 @@ RSpec.describe Admission::Privilege do
|
|
68
68
|
end
|
69
69
|
|
70
70
|
|
71
|
+
describe '#eql_or_inherits?' do
|
72
|
+
|
73
|
+
it 'returns true since it equals sought privilege' do
|
74
|
+
expect(privilege.eql_or_inherits? privilege).to eq(true)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'return false when does not inherit any' do
|
78
|
+
expect(privilege_superman.eql_or_inherits? privilege_uberman).to eq(false)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'finds nested inherited privilege and therefore evaluates to true' do
|
82
|
+
top_privilege = new_privilege 'man', 'top'
|
83
|
+
top_privilege.inherits_from new_privilege('man', 'branch'),
|
84
|
+
new_privilege('man', 'middle').tap{|p| p.inherits_from new_privilege('man')}
|
85
|
+
|
86
|
+
sought = new_privilege 'man'
|
87
|
+
expect(top_privilege.eql_or_inherits? sought).to eq(true)
|
88
|
+
expect(top_privilege.eql_or_inherits? new_privilege('man', 'nope')).to eq(false)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'ignores context' do
|
92
|
+
expect(privilege.eql_or_inherits? privilege.dup_with_context(:czech)).to eq(true)
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
|
71
98
|
describe '#to_s' do
|
72
99
|
|
73
100
|
it 'prints name and level' do
|
data/spec/unit/status_spec.rb
CHANGED
@@ -27,14 +27,32 @@ RSpec.describe Admission::Status do
|
|
27
27
|
)
|
28
28
|
end
|
29
29
|
|
30
|
-
it 'sets privileges' do
|
31
|
-
instance = Admission::Status.new :person, [
|
30
|
+
it 'sets privileges and freezes them' do
|
31
|
+
instance = Admission::Status.new :person, [:czech], :rules, :arbiter
|
32
32
|
expect(instance).to have_inst_vars(
|
33
33
|
person: :person,
|
34
|
-
privileges: [
|
34
|
+
privileges: [:czech],
|
35
35
|
rules: :rules,
|
36
36
|
arbiter: :arbiter
|
37
37
|
)
|
38
|
+
expect(instance.privileges).to be_frozen
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'sorts privileges by context' do
|
42
|
+
instance = Admission::Status.new :person, [
|
43
|
+
privilege(nil),
|
44
|
+
privilege(:czech),
|
45
|
+
privilege(15),
|
46
|
+
privilege(:czech),
|
47
|
+
privilege({a: 15}),
|
48
|
+
privilege({a: {f: 1}}),
|
49
|
+
privilege(nil),
|
50
|
+
privilege({a: 15}),
|
51
|
+
], :rules, :arbiter
|
52
|
+
expect(instance.privileges.map(&:context)).to eq([
|
53
|
+
nil, nil, :czech, :czech, 15, {:a=>15}, {:a=>15}, {:a=>{:f=>1}}
|
54
|
+
])
|
55
|
+
expect(instance.privileges).to be_frozen
|
38
56
|
end
|
39
57
|
|
40
58
|
end
|
File without changes
|
@@ -0,0 +1,73 @@
|
|
1
|
+
import preact from 'preact';
|
2
|
+
import PrivilegesPanel from './privileges_panel';
|
3
|
+
import classnames from 'classnames';
|
4
|
+
|
5
|
+
export default class AppContainer extends preact.Component {
|
6
|
+
|
7
|
+
constructor (props) {
|
8
|
+
super(props);
|
9
|
+
|
10
|
+
this.switchToPrivileges = this.changePanel.bind(this, 'privileges');
|
11
|
+
this.switchToRules = this.changePanel.bind(this, 'rules');
|
12
|
+
}
|
13
|
+
|
14
|
+
render ({app}, {loaded, load_fail, panel}) {
|
15
|
+
if (!loaded) return <div className="splash-message">
|
16
|
+
<code>... loading admission data ...</code>
|
17
|
+
</div>;
|
18
|
+
|
19
|
+
if (load_fail) return <div className="splash-message">
|
20
|
+
<h4>failed to load admission data</h4>
|
21
|
+
<code>{load_fail}</code>
|
22
|
+
</div>;
|
23
|
+
|
24
|
+
return <div>
|
25
|
+
<ul className="panels-list">
|
26
|
+
|
27
|
+
<li
|
28
|
+
onClick={this.switchToPrivileges}
|
29
|
+
className={classnames({'active': panel === 'privileges'})}>
|
30
|
+
Privileges Order
|
31
|
+
</li>
|
32
|
+
|
33
|
+
<li
|
34
|
+
onClick={this.switchToRules}
|
35
|
+
className={classnames({'active': panel === 'rules'})}>
|
36
|
+
Rules Listing
|
37
|
+
</li>
|
38
|
+
|
39
|
+
</ul>
|
40
|
+
|
41
|
+
{this.renderPanel()}
|
42
|
+
</div>;
|
43
|
+
}
|
44
|
+
|
45
|
+
componentDidMount () {
|
46
|
+
const store = this.props.app.store;
|
47
|
+
|
48
|
+
this.store_unsibscribe = store.subscribe(() => {
|
49
|
+
const state = store.getState();
|
50
|
+
this.setState({loaded: state.loaded, panel: state.panel});
|
51
|
+
});
|
52
|
+
|
53
|
+
setTimeout(this.props.onMounted, 0);
|
54
|
+
}
|
55
|
+
|
56
|
+
componentWillUnmount () {
|
57
|
+
this.store_unsibscribe();
|
58
|
+
}
|
59
|
+
|
60
|
+
changePanel(panel) {
|
61
|
+
this.props.app.store.dispatch({type: 'PANEL_CHANGE', panel: panel});
|
62
|
+
}
|
63
|
+
|
64
|
+
renderPanel () {
|
65
|
+
const app = this.props.app;
|
66
|
+
switch (this.state.panel) {
|
67
|
+
case 'privileges':
|
68
|
+
return <PrivilegesPanel app={app} />;
|
69
|
+
break;
|
70
|
+
|
71
|
+
}
|
72
|
+
}
|
73
|
+
}
|
@@ -0,0 +1,164 @@
|
|
1
|
+
import preact from 'preact';
|
2
|
+
import classnames from 'classnames';
|
3
|
+
|
4
|
+
export default class InputWithSelect extends preact.Component {
|
5
|
+
|
6
|
+
constructor (props) {
|
7
|
+
super(props);
|
8
|
+
|
9
|
+
this.state = {
|
10
|
+
text: props.defaultText || '',
|
11
|
+
matching: null,
|
12
|
+
};
|
13
|
+
|
14
|
+
this.setParentRef = ref => this.element = ref;
|
15
|
+
this.setListRef = ref => this.list = ref;
|
16
|
+
this.onKeyDown = props.app.debounce(this.onKeyDown.bind(this), 200);
|
17
|
+
this.toggleList = props.app.debounce(this.toggleList.bind(this), 400, true);
|
18
|
+
this.closeList = this.closeList.bind(this);
|
19
|
+
this.onSelected = this.onSelected.bind(this);
|
20
|
+
}
|
21
|
+
|
22
|
+
render ({app, placeholder}, {text, matching}) {
|
23
|
+
if (!matching) this.list = null;
|
24
|
+
|
25
|
+
return <div
|
26
|
+
ref={this.setParentRef}
|
27
|
+
className="controls-select">
|
28
|
+
|
29
|
+
<div className="_inputs">
|
30
|
+
<input
|
31
|
+
type="text"
|
32
|
+
className="input_text"
|
33
|
+
placeholder={placeholder}
|
34
|
+
onKeyDown={this.onKeyDown}
|
35
|
+
value={text}/>
|
36
|
+
|
37
|
+
<button
|
38
|
+
tabIndex="-1"
|
39
|
+
className="button"
|
40
|
+
onClick={this.toggleList}>
|
41
|
+
⌄
|
42
|
+
</button>
|
43
|
+
</div>
|
44
|
+
|
45
|
+
{matching && <DropdownList
|
46
|
+
ref={this.setListRef}
|
47
|
+
app={app}
|
48
|
+
items={matching}
|
49
|
+
toSelect={this.onSelected}
|
50
|
+
toClose={this.closeList}
|
51
|
+
/>}
|
52
|
+
|
53
|
+
</div>;
|
54
|
+
}
|
55
|
+
|
56
|
+
componentDidMount () {
|
57
|
+
this._outside_click_listener = e => {
|
58
|
+
if (!this.element.contains(e.target) && this.list) {
|
59
|
+
this.closeList();
|
60
|
+
}
|
61
|
+
};
|
62
|
+
document.addEventListener('click', this._outside_click_listener);
|
63
|
+
}
|
64
|
+
|
65
|
+
componentWillUnmount () {
|
66
|
+
document.removeEventListener('click', this._outside_click_listener);
|
67
|
+
}
|
68
|
+
|
69
|
+
componentWillReceiveProps (new_props) {
|
70
|
+
if (new_props.defaultText !== this.props.defaultText) this.setState({text: new_props.defaultText});
|
71
|
+
}
|
72
|
+
|
73
|
+
onKeyDown (e) {
|
74
|
+
if (this.list && this.list.onKeyDown(e)) return;
|
75
|
+
|
76
|
+
const text = e.target.value.trim();
|
77
|
+
let matching = null;
|
78
|
+
if ((text && text !==this.state.text) || e.keyCode === 40) {
|
79
|
+
matching = filter_items(this.props.all_items, text);
|
80
|
+
}
|
81
|
+
this.setState({matching, text});
|
82
|
+
}
|
83
|
+
|
84
|
+
toggleList () {
|
85
|
+
if (this.list) {
|
86
|
+
this.closeList();
|
87
|
+
|
88
|
+
} else {
|
89
|
+
this.setState({matching: this.props.all_items});
|
90
|
+
}
|
91
|
+
}
|
92
|
+
|
93
|
+
closeList () {
|
94
|
+
this.setState({matching: null});
|
95
|
+
}
|
96
|
+
|
97
|
+
onSelected (value) {
|
98
|
+
this.setState({text: value, matching: null});
|
99
|
+
this.props.onSelect(value);
|
100
|
+
}
|
101
|
+
|
102
|
+
}
|
103
|
+
|
104
|
+
class DropdownList extends preact.Component {
|
105
|
+
|
106
|
+
constructor (props) {
|
107
|
+
super(props);
|
108
|
+
this.state = {selected: -1};
|
109
|
+
}
|
110
|
+
|
111
|
+
render ({app, items, toSelect}, {selected}) {
|
112
|
+
return <div
|
113
|
+
className="_dropdown">
|
114
|
+
<ul>
|
115
|
+
{items.map((name, i) => <li
|
116
|
+
className={classnames({'selected': selected === i})}
|
117
|
+
onClick={() => toSelect(name)}>
|
118
|
+
{name}
|
119
|
+
</li>)}
|
120
|
+
</ul>
|
121
|
+
</div>;
|
122
|
+
}
|
123
|
+
|
124
|
+
onKeyDown (e) {
|
125
|
+
switch (e.keyCode) {
|
126
|
+
case 40: // down
|
127
|
+
this.changeSelection(1);
|
128
|
+
return true;
|
129
|
+
break;
|
130
|
+
|
131
|
+
case 38: // up
|
132
|
+
this.changeSelection(-1);
|
133
|
+
return true;
|
134
|
+
break;
|
135
|
+
|
136
|
+
case 13: // enter
|
137
|
+
const selected = this.state.selected;
|
138
|
+
if (selected !== -1) this.props.toSelect(this.props.items[selected]);
|
139
|
+
return true;
|
140
|
+
break;
|
141
|
+
|
142
|
+
case 27: // escape
|
143
|
+
this.props.toClose();
|
144
|
+
return true;
|
145
|
+
break;
|
146
|
+
|
147
|
+
}
|
148
|
+
return false;
|
149
|
+
}
|
150
|
+
|
151
|
+
changeSelection (value) {
|
152
|
+
let selected = this.state.selected;
|
153
|
+
selected += value;
|
154
|
+
if (selected < 0) selected = 0;
|
155
|
+
if (selected >= this.props.items.length) selected = this.props.items.length -1;
|
156
|
+
this.setState({selected});
|
157
|
+
}
|
158
|
+
}
|
159
|
+
|
160
|
+
function filter_items (all, input_text) {
|
161
|
+
let items = all.filter(value => value.startsWith(input_text));
|
162
|
+
if (items.length === 0) items = null;
|
163
|
+
return items;
|
164
|
+
}
|