##// END OF EJS Templates
auth-plugins: updated UI for authentication plugins, and allow unsorted (in order of registration) display of plugins.
marcink -
r3233:4c12f36b default
parent child Browse files
Show More
@@ -1,150 +1,155 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2018 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 import collections
22 23
23 24 from pyramid.exceptions import ConfigurationError
24 25
25 26 from rhodecode.lib.utils2 import safe_str
26 27 from rhodecode.model.settings import SettingsModel
27 28 from rhodecode.translation import _
28 29
29 30
30 31 log = logging.getLogger(__name__)
31 32
32 33
33 34 class AuthnResourceBase(object):
34 35 __name__ = None
35 36 __parent__ = None
36 37
37 38 def get_root(self):
38 39 current = self
39 40 while current.__parent__ is not None:
40 41 current = current.__parent__
41 42 return current
42 43
43 44
44 45 class AuthnPluginResourceBase(AuthnResourceBase):
45 46
46 47 def __init__(self, plugin):
47 48 self.plugin = plugin
48 49 self.__name__ = plugin.get_url_slug()
49 50 self.display_name = plugin.get_display_name()
50 51
51 52
52 53 class AuthnRootResource(AuthnResourceBase):
53 54 """
54 55 This is the root traversal resource object for the authentication settings.
55 56 """
56 57
57 58 def __init__(self):
58 self._store = {}
59 self._store = collections.OrderedDict()
59 60 self._resource_name_map = {}
60 61 self.display_name = _('Global')
61 62
62 63 def __getitem__(self, key):
63 64 """
64 65 Customized get item function to return only items (plugins) that are
65 66 activated.
66 67 """
67 68 if self._is_item_active(key):
68 69 return self._store[key]
69 70 else:
70 71 raise KeyError('Authentication plugin "{}" is not active.'.format(
71 72 key))
72 73
73 74 def __iter__(self):
74 75 for key in self._store.keys():
75 76 if self._is_item_active(key):
76 77 yield self._store[key]
77 78
78 79 def _is_item_active(self, key):
79 80 activated_plugins = SettingsModel().get_auth_plugins()
80 81 plugin_id = self.get_plugin_id(key)
81 82 return plugin_id in activated_plugins
82 83
83 84 def get_plugin_id(self, resource_name):
84 85 """
85 86 Return the plugin id for the given traversal resource name.
86 87 """
87 88 # TODO: Store this info in the resource element.
88 89 return self._resource_name_map[resource_name]
89 90
90 91 def get_sorted_list(self):
91 92 """
92 93 Returns a sorted list of sub resources for displaying purposes.
93 94 """
94 95 def sort_key(resource):
95 96 return str.lower(safe_str(resource.display_name))
96 97
97 98 active = [item for item in self]
98 99 return sorted(active, key=sort_key)
99 100
100 def get_nav_list(self):
101 def get_nav_list(self, sort=True):
101 102 """
102 103 Returns a sorted list of resources for displaying the navigation.
103 104 """
104 list = self.get_sorted_list()
105 list.insert(0, self)
106 return list
105 if sort:
106 nav_list = self.get_sorted_list()
107 else:
108 nav_list = [item for item in self]
109
110 nav_list.insert(0, self)
111 return nav_list
107 112
108 113 def add_authn_resource(self, config, plugin_id, resource):
109 114 """
110 115 Register a traversal resource as a sub element to the authentication
111 116 settings. This method is registered as a directive on the pyramid
112 117 configurator object and called by plugins.
113 118 """
114 119
115 120 def _ensure_unique_name(name, limit=100):
116 121 counter = 1
117 122 current = name
118 123 while current in self._store.keys():
119 124 current = '{}{}'.format(name, counter)
120 125 counter += 1
121 126 if counter > limit:
122 127 raise ConfigurationError(
123 128 'Cannot build unique name for traversal resource "%s" '
124 129 'registered by plugin "%s"', name, plugin_id)
125 130 return current
126 131
127 132 # Allow plugin resources with identical names by rename duplicates.
128 133 unique_name = _ensure_unique_name(resource.__name__)
129 134 if unique_name != resource.__name__:
130 135 log.warn('Name collision for traversal resource "%s" registered '
131 136 'by authentication plugin "%s"', resource.__name__,
132 137 plugin_id)
133 138 resource.__name__ = unique_name
134 139
135 140 log.debug('Register traversal resource "%s" for plugin "%s"',
136 141 unique_name, plugin_id)
137 142 self._resource_name_map[unique_name] = plugin_id
138 143 resource.__parent__ = self
139 144 self._store[unique_name] = resource
140 145
141 146
142 147 root = AuthnRootResource()
143 148
144 149
145 150 def root_factory(request=None):
146 151 """
147 152 Returns the root traversal resource instance used for the authentication
148 153 settings route.
149 154 """
150 155 return root
@@ -1,118 +1,124 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="/base/base.mako"/>
3 3
4 4 <%def name="title()">
5 5 ${_('Authentication Settings')}
6 6 %if c.rhodecode_name:
7 7 &middot; ${h.branding(c.rhodecode_name)}}
8 8 %endif
9 9 </%def>
10 10
11 11 <%def name="breadcrumbs_links()">
12 12 ${h.link_to(_('Admin'),h.route_path('admin_home'))}
13 13 &raquo;
14 14 ${_('Authentication Plugins')}
15 15 </%def>
16 16
17 17 <%def name="menu_bar_nav()">
18 18 ${self.menu_items(active='admin')}
19 19 </%def>
20 20
21 21 <%def name="main()">
22 22
23 23 <div class="box">
24 24 <div class="title">
25 25 ${self.breadcrumbs()}
26 26 </div>
27 27
28 28 <div class='sidebar-col-wrapper'>
29 29
30 30 <div class="sidebar">
31 31 <ul class="nav nav-pills nav-stacked">
32 % for item in resource.get_root().get_nav_list():
32 % for item in resource.get_root().get_nav_list(sort=False):
33 33 <li ${'class=active' if item == resource else ''}>
34 34 <a href="${request.resource_path(item, route_name='auth_home')}">${item.display_name}</a>
35 35 </li>
36 36 % endfor
37 37 </ul>
38 38 </div>
39 39
40 40 <div class="main-content-full-width">
41 41 ${h.secure_form(request.resource_path(resource, route_name='auth_home'), request=request)}
42 <div class="form">
43
44 42 <div class="panel panel-default">
45 43
46 44 <div class="panel-heading">
47 45 <h3 class="panel-title">${_("Enabled and Available Plugins")}</h3>
48 46 </div>
49 47
50 <div class="fields panel-body">
48 <div class="panel-body">
51 49
52 <div class="field">
53 <div class="label">${_("Enabled Plugins")}</div>
50
51 <div class="label">${_("Ordered Enabled Plugins")}</div>
54 52 <div class="textarea text-area editor">
55 ${h.textarea('auth_plugins',cols=23,rows=5,class_="medium")}
53 ${h.textarea('auth_plugins',cols=120,rows=20,class_="medium")}
56 54 </div>
57 <p class="help-block pre-formatting">${_('List of plugins, separated by commas.'
55 <div class="field">
56 <p class="help-block pre-formatting">${_('List of plugins, separated by commas.'
58 57 '\nThe order of the plugins is also the order in which '
59 'RhodeCode Enterprise will try to authenticate a user.')}</p>
60 </div>
58 'RhodeCode Enterprise will try to authenticate a user.')}
59 </p>
60 </div>
61 61
62 <div class="field">
63 <div class="label">${_('Available Built-in Plugins')}</div>
64 <ul class="auth_plugins">
65 %for plugin in available_plugins:
66 <li>
67 <div class="auth_buttons">
68 <span plugin_id="${plugin.get_id()}" class="toggle-plugin btn ${'btn-success' if plugin.get_id() in enabled_plugins else ''}">
69 ${_('enabled') if plugin.get_id() in enabled_plugins else _('disabled')}
70 </span>
71 ${plugin.get_display_name()} (${plugin.get_id()})
72 </div>
73 </li>
74 %endfor
75 </ul>
76 </div>
62 <table class="rctable">
63 <th>${_('Activate')}</th>
64 <th>${_('Plugin Name')}</th>
65 <th>${_('Documentation')}</th>
66 <th>${_('Plugin ID')}</th>
67 %for plugin in available_plugins:
68 <tr>
69 <td>
70 <span plugin_id="${plugin.get_id()}" class="toggle-plugin btn ${'btn-success' if plugin.get_id() in enabled_plugins else ''}">
71 ${_('enabled') if plugin.get_id() in enabled_plugins else _('disabled')}
72 </span>
73 </td>
74 <td>${plugin.get_display_name()}</td>
75 <td>
76 % if plugin.docs():
77 <a href="${plugin.docs()}">docs</a>
78 % endif
79 </td>
80 <td>${plugin.get_id()}</td>
81 </tr>
82 %endfor
83 </table>
77 84
78 85 <div class="buttons">
79 86 ${h.submit('save',_('Save'),class_="btn")}
80 87 </div>
81 88 </div>
82 89 </div>
83 </div>
84 90 ${h.end_form()}
85 91 </div>
86 92 </div>
87 93 </div>
88 94
89 95 <script>
90 96 $('.toggle-plugin').click(function(e){
91 97 var auth_plugins_input = $('#auth_plugins');
92 98 var elems = [];
93 99
94 100 $.each(auth_plugins_input.val().split(',') , function (index, element) {
95 101 if (element !== "") {
96 102 elems.push(element.strip())
97 103 }
98 104 });
99 105
100 106 var cur_button = e.currentTarget;
101 107 var plugin_id = $(cur_button).attr('plugin_id');
102 108 if($(cur_button).hasClass('btn-success')){
103 109 elems.splice(elems.indexOf(plugin_id), 1);
104 110 auth_plugins_input.val(elems.join(',\n'));
105 111 $(cur_button).removeClass('btn-success');
106 112 cur_button.innerHTML = _gettext('disabled');
107 113 }
108 114 else{
109 115 if(elems.indexOf(plugin_id) == -1){
110 116 elems.push(plugin_id);
111 117 }
112 118 auth_plugins_input.val(elems.join(',\n'));
113 119 $(cur_button).addClass('btn-success');
114 120 cur_button.innerHTML = _gettext('enabled');
115 121 }
116 122 });
117 123 </script>
118 124 </%def>
@@ -1,323 +1,323 b''
1 1 ${h.secure_form(h.route_path('admin_settings_global_update'), request=request)}
2 2
3 3 <div class="panel panel-default">
4 4 <div class="panel-heading" id="branding-options">
5 5 <h3 class="panel-title">${_('Branding')} <a class="permalink" href="#branding-options"> ΒΆ</a></h3>
6 6 </div>
7 7 <div class="panel-body">
8 8 <div class="label">
9 9 <label for="rhodecode_title">${_('Title')}</label>
10 10 </div>
11 11 <div class="field input">
12 12 ${h.text('rhodecode_title',size=60)}
13 13 </div>
14 14 <div class="field">
15 15 <span class="help-block">
16 16 ${_('Set a custom title for your RhodeCode instance (limited to 40 characters).')}
17 17 </span>
18 18 </div>
19 19 <div class="label">
20 20 <label for="rhodecode_realm">${_('HTTP[S] authentication realm')}</label>
21 21 </div>
22 22 <div class="field input">
23 23 ${h.text('rhodecode_realm',size=60)}
24 24 </div>
25 25 <div class="field">
26 26 <span class="help-block">
27 27 ${_('Set a custom text that is shown as authentication message to clients trying to connect.')}
28 28 </span>
29 29 </div>
30 30 </div>
31 31 </div>
32 32
33 33
34 34 <div class="panel panel-default">
35 35 <div class="panel-heading" id="personal-group-options">
36 36 <h3 class="panel-title">${_('Personal Repository Group')} <a class="permalink" href="#personal-group-options"> ΒΆ</a></h3>
37 37 </div>
38 38 <div class="panel-body">
39 39 <div class="checkbox">
40 40 ${h.checkbox('rhodecode_create_personal_repo_group','True')}
41 41 <label for="rhodecode_create_personal_repo_group">${_('Create Personal Repository Group')}</label>
42 42 </div>
43 43 <span class="help-block">
44 44 ${_('Always create Personal Repository Groups for new users.')} <br/>
45 45 ${_('When creating new users from add user form or API you can still turn this off via a checkbox or flag')}
46 46 </span>
47 47
48 48 <div class="label">
49 49 <label for="rhodecode_personal_repo_group_pattern">${_('Personal Repo Group Pattern')}</label>
50 50 </div>
51 51 <div class="field input">
52 52 ${h.text('rhodecode_personal_repo_group_pattern',size=60, placeholder=c.personal_repo_group_default_pattern)}
53 53 </div>
54 54 <span class="help-block">
55 55 ${_('Pattern used to create Personal Repository Groups. Prefix can be other existing repository group path[s], eg. /u/${username}')} <br/>
56 56 ${_('Available variables are currently ${username} and ${user_id}')}
57 57 </span>
58 58 </div>
59 59 </div>
60 60
61 61
62 62 <div class="panel panel-default">
63 63 <div class="panel-heading" id="captcha-options">
64 64 <h3 class="panel-title">${_('Registration Captcha')} <a class="permalink" href="#captcha-options"> ΒΆ</a></h3>
65 65 </div>
66 66 <div class="panel-body">
67 67 <div class="label">
68 68 <label for="rhodecode_captcha_public_key">${_('Google reCaptcha v2 site key.')}</label>
69 69 </div>
70 70 <div class="field input">
71 71 ${h.text('rhodecode_captcha_public_key',size=60)}
72 72 </div>
73 73 <div class="field">
74 74 <span class="help-block">
75 75 ${_('Site key for reCaptcha v2 system.')}
76 76 </span>
77 77 </div>
78 78
79 79 <div class="label">
80 80 <label for="rhodecode_captcha_private_key">${_('Google reCaptcha v2 secret key.')}</label>
81 81 </div>
82 82 <div class="field input">
83 83 ${h.text('rhodecode_captcha_private_key',size=60)}
84 84 </div>
85 85 <div class="field">
86 86 <span class="help-block">
87 87 ${_('Secret key for reCaptcha v2 system. Setting this value will enable captcha on registration and password reset forms.')}
88 88 </span>
89 89 </div>
90 90 </div>
91 91 </div>
92 92
93 93 <div class="panel panel-default">
94 94 <div class="panel-heading" id="header-code-options">
95 95 <h3 class="panel-title">${_('Custom Header Code')} <a class="permalink" href="#header-code-options"> ΒΆ</a></h3>
96 96 </div>
97 97 <div class="panel-body">
98 98 <div class="select">
99 99 <select id="pre_template" >
100 100 <option value="#">${_('Templates...')}</option>
101 101 <option value="ga">Google Analytics</option>
102 102 <option value="clicky">Clicky</option>
103 103 <option value="server_announce">${_('Server Announcement')}</option>
104 104 <option value="flash_filtering">${_('Flash message filtering')}</option>
105 105 <option value="custom_logo">${_('Custom logos')}</option>
106 106 </select>
107 107 </div>
108 108 <div style="padding: 10px 0px"></div>
109 109 <div class="textarea text-area">
110 110 ${h.textarea('rhodecode_pre_code',cols=23,rows=5,class_="medium")}
111 111 <span class="help-block">${_('Custom js/css code added at the end of the <header/> tag.')}
112 112 ${_('Use <script/> or <css/> tags to define custom styling or scripting')}</span>
113 113 </div>
114 114 </div>
115 115 </div>
116 116
117 117 <div class="panel panel-default">
118 118 <div class="panel-heading" id="footer-code-options">
119 119 <h3 class="panel-title">${_('Custom Footer Code')} <a class="permalink" href="#footer-code-options"> ΒΆ</a></h3>
120 120 </div>
121 121 <div class="panel-body">
122 122 <div class="select">
123 123 <select id="post_template" >
124 124 <option value="#">${_('Templates...')}</option>
125 125 <option value="ga">Google Analytics</option>
126 126 <option value="clicky">Clicky</option>
127 127 <option value="server_announce">${_('Server Announcement')}</option>
128 128 </select>
129 129 </div>
130 130 <div style="padding: 10px 0px"></div>
131 131 <div class="textarea text-area">
132 132 ${h.textarea('rhodecode_post_code',cols=23,rows=5, class_="medium")}
133 133 <span class="help-block">${_('Custom js/css code added at the end of the <body> tag.')}
134 134 ${_('Use <script> or <css> tags to define custom styling or scripting')}</span>
135 135 </div>
136 136 </div>
137 137 </div>
138 138
139 139 <div class="buttons">
140 140 ${h.submit('save',_('Save settings'),class_="btn")}
141 141 ${h.reset('reset',_('Reset'),class_="btn")}
142 142 </div>
143 143 ${h.end_form()}
144 144
145 145
146 146
147 147 ## TEMPLATES ##
148 148 ###############
149 149
150 150 <script id="ga_tmpl" type="text/x-template">
151 151 <%text filter="h">
152 152 <script>
153 153 // Google Analytics
154 154 // Put your Google Analytics code instead of _GACODE_
155 155 var _gaq_code = '_GACODE_';
156 156 var _gaq = _gaq || [];
157 157 _gaq.push(['_setAccount', _gaq_code]);
158 158 _gaq.push(['_trackPageview']);
159 159
160 160 (function() {
161 161 var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
162 162 ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
163 163 var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
164 164 })();
165 165
166 166 rhodecode_statechange_callback = function(url, data){
167 167 // ANALYTICS callback on html5 history state changed
168 168 // triggered by file browser, url is the new url,
169 169 // data is extra info passed from the State object
170 170 if (typeof window._gaq !== 'undefined') {
171 171 _gaq.push(['_trackPageview', url]);
172 172 }
173 173 };
174 174 </script>
175 175 </%text>
176 176 </script>
177 177
178 178
179 179
180 180 <script id="clicky_tmpl" type="text/x-template">
181 181 <%text filter="h">
182 182 <script src="//static.getclicky.com/js" type="text/javascript"></script>
183 183 <script type="text/javascript">
184 184 // Clicky Analytics - should be used in the footer code section.
185 185 // Put your Clicky code instead of _CLICKYCODE_ here,
186 186 // and below in the <img> tag.
187 187 var _cl_code = _CLICKYCODE_;
188 188 try{clicky.init(_cl_code);}catch(e){}
189 189
190 190 rhodecode_statechange_callback = function(url, data){
191 191 // ANALYTICS callback on html5 history state changed
192 192 // triggered by file browser, url is the new url,
193 193 // data is extra info passed from the State object
194 194 if (typeof window.clicky !== 'undefined') {
195 195 clicky.log(url);
196 196 }
197 197 }
198 198 </script>
199 199 <noscript>
200 200 // Put your clicky code in the src file.
201 201 <p><img alt="Clicky" width="1" height="1"
202 202 src="//in.getclicky.com/_CLICKYCODE_ns.gif" /></p>
203 203 </noscript>
204 204 </%text>
205 205 </script>
206 206
207 207
208 208
209 209 <script id="server_announce_tmpl" type='text/x-template'>
210 210 <%text filter="h">
211 211 <script>
212 212 // Server announcement displayed on the top of the page.
213 213 // This can be used to send a global maintenance messages or other
214 214 // important messages to all users of the RhodeCode Enterprise system.
215 215
216 216 $(document).ready(function(e) {
217 217
218 218 // EDIT - put your message below
219 219 var message = "TYPE YOUR MESSAGE HERE";
220 220
221 221 // EDIT - choose "info"/"warning"/"error"/"success"/"neutral" as appropriate
222 222 var alert_level = "info";
223 223
224 224 $("#body").prepend(
225 225 ("<div id='server-announcement' class='"+alert_level+"'>_MSG_"+"</div>").replace("_MSG_", message)
226 226 )
227 227 })
228 228 </script>
229 229 </%text>
230 230 </script>
231 231
232 232 <script id="flash_filtering_tmpl" type='text/x-template'>
233 233 <%text filter="h">
234 234 <script>
235 235 // This filters out some flash messages before they are presented to user
236 236 // based on their contents. Could be used to filter out warnings/errors
237 237 // of license messages
238 238
239 239 var filteredMessages = [];
240 240 for(var i =0; i< alertMessagePayloads.length; i++){
241 241 if (typeof alertMessagePayloads[i].message.subdata.subtype !== 'undefined' &&
242 242 alertMessagePayloads[i].message.subdata.subtype.indexOf('rc_license') !== -1){
243 243 continue
244 244 }
245 245 filteredMessages.push(alertMessagePayloads[i]);
246 246 }
247 247 alertMessagePayloads = filteredMessages;
248 248 </script>
249 249 </%text>
250 250 </script>
251 251
252 252
253 253 <script id="custom_logo_tmpl" type='text/x-template'>
254 254 <%text filter="h">
255 255 <script>
256 256
257 257 $(document).ready(function(e) {
258 258 // 1) Set custom logo on login/register pages.
259 259
260 260 // external URL, custom company logo
261 261 //$('.sign-in-image').attr("src", "http://server.com/logo_path/custom_logo.png");
262 262
263 263 // Alternative logo from static folder
264 264 $('.sign-in-image').attr("src", "/_static/rhodecode/images/RhodeCode_Logo_Black.png");
265 265
266 // set width/height
266 // option to set width/height, adjust if required to make your image look good.
267 267 $('.sign-in-image').css({"width": "300px", "height": "345px"});
268 268
269 269 // 2) Header logo on top bar
270 270 $('.logo-wrapper').find('img').attr('src', 'http://server.com/logo_path/custom_header_logo.png')
271 271
272 272 });
273 273 </script>
274 274 </%text>
275 275 </script>
276 276
277 277
278 278 <script>
279 279 var pre_cm = initCodeMirror('rhodecode_pre_code', '', false);
280 280 var pre_old = pre_cm.getValue();
281 281
282 282 var post_cm = initCodeMirror('rhodecode_post_code', '', false);
283 283 var post_old = post_cm.getValue();
284 284
285 285 var get_data = function(type, old) {
286 286 var get_tmpl = function(tmpl_name){
287 287 // unescape some stuff
288 288 return htmlEnDeCode.htmlDecode($('#'+tmpl_name+'_tmpl').html());
289 289 };
290 290 return {
291 291 '#': old,
292 292 'ga': get_tmpl('ga'),
293 293 'clicky': get_tmpl('clicky'),
294 294 'server_announce': get_tmpl('server_announce'),
295 295 'flash_filtering': get_tmpl('flash_filtering'),
296 296 'custom_logo': get_tmpl('custom_logo')
297 297 }[type]
298 298 };
299 299
300 300 $('#pre_template').select2({
301 301 containerCssClass: 'drop-menu',
302 302 dropdownCssClass: 'drop-menu-dropdown',
303 303 dropdownAutoWidth: true,
304 304 minimumResultsForSearch: -1
305 305 });
306 306
307 307 $('#post_template').select2({
308 308 containerCssClass: 'drop-menu',
309 309 dropdownCssClass: 'drop-menu-dropdown',
310 310 dropdownAutoWidth: true,
311 311 minimumResultsForSearch: -1
312 312 });
313 313
314 314 $('#post_template').on('change', function(e){
315 315 var sel = this.value;
316 316 post_cm.setValue(get_data(sel, post_old))
317 317 });
318 318
319 319 $('#pre_template').on('change', function(e){
320 320 var sel = this.value;
321 321 pre_cm.setValue(get_data(sel, pre_old))
322 322 })
323 323 </script>
General Comments 0
You need to be logged in to leave comments. Login now