admission 0.1.9 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
}
|