mirror of
https://github.com/valitydev/redash.git
synced 2024-11-07 09:28:51 +00:00
Merge pull request #2350 from getredash/auth
Authentication related settings improvements
This commit is contained in:
commit
bb28b2f0fb
@ -72,6 +72,7 @@ case "$1" in
|
|||||||
scheduler
|
scheduler
|
||||||
;;
|
;;
|
||||||
dev_server)
|
dev_server)
|
||||||
|
export FLASK_DEBUG=1
|
||||||
exec /app/manage.py runserver --debugger --reload -h 0.0.0.0
|
exec /app/manage.py runserver --debugger --reload -h 0.0.0.0
|
||||||
;;
|
;;
|
||||||
shell)
|
shell)
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
.contacts {
|
|
||||||
&:not(.c-profile) {
|
|
||||||
padding: 0 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& > [class*="col-"] {
|
|
||||||
padding: 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c-item {
|
|
||||||
border: 1px solid #e2e2e2;
|
|
||||||
border-radius: 2px;
|
|
||||||
margin-bottom: 24px;
|
|
||||||
|
|
||||||
.ci-avatar {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
img {
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 2px 2px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ci-avatar {
|
|
||||||
margin: -1px -1px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c-info {
|
|
||||||
text-align: center;
|
|
||||||
margin-top: 15px;
|
|
||||||
padding: 0 5px;
|
|
||||||
|
|
||||||
strong {
|
|
||||||
color: #000;
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
small {
|
|
||||||
color: #999;
|
|
||||||
margin-top: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong,
|
|
||||||
small {
|
|
||||||
.text-overflow();
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.c-footer {
|
|
||||||
border-top: 1px solid #e2e2e2;
|
|
||||||
margin-top: 18px;
|
|
||||||
|
|
||||||
& > button {
|
|
||||||
padding: 4px 10px 3px;
|
|
||||||
color: #333;
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
text-align: center;
|
|
||||||
color: #333;
|
|
||||||
font-weight: 500;
|
|
||||||
border-radius: 2px;
|
|
||||||
background: #fff;
|
|
||||||
border: 0;
|
|
||||||
|
|
||||||
& > i {
|
|
||||||
font-size: 16px;
|
|
||||||
vertical-align: middle;
|
|
||||||
margin-top: -3px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
.todo-lists {
|
|
||||||
padding: 15px 0 0 0;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.list-group-item {
|
|
||||||
margin: 0;
|
|
||||||
min-height: 36px;
|
|
||||||
|
|
||||||
|
|
||||||
&:not(:last-child) {
|
|
||||||
border-bottom: 1px solid #E2EBFF;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
background: #E2EBFF;
|
|
||||||
left: 50px;
|
|
||||||
width: 1px;
|
|
||||||
height: 100%;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.input-helper:before {
|
|
||||||
background: lighten(@red, 10%) !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tl-item {
|
|
||||||
padding-left: 47px !important;
|
|
||||||
|
|
||||||
input:checked + .input-helper + span {
|
|
||||||
text-decoration: line-through;
|
|
||||||
color: #b5b5b5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.todo-footer {
|
|
||||||
padding: 10px;
|
|
||||||
margin-top: 20px;
|
|
||||||
border-top: 1px solid #E2EBFF;
|
|
||||||
text-align: center;
|
|
||||||
position: relative;
|
|
||||||
z-index: 2;
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
@ -35,6 +35,20 @@ body {
|
|||||||
clear: both;
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.callout {
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #eee;
|
||||||
|
border-left-width: 5px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-warning {
|
||||||
|
border-left-color: #aa6708;
|
||||||
|
}
|
||||||
|
|
||||||
|
.callout-info {
|
||||||
|
border-left-color: #1b809e;
|
||||||
|
}
|
||||||
|
|
||||||
// Fixed width layout for specific pages
|
// Fixed width layout for specific pages
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
|
@ -127,3 +127,11 @@ export function prettySize(bytes) {
|
|||||||
|
|
||||||
return bytes.toFixed(3) + ' ' + units[unit];
|
return bytes.toFixed(3) + ' ' + units[unit];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function join(arr) {
|
||||||
|
if (arr === undefined || arr === null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return arr.join(' / ');
|
||||||
|
}
|
||||||
|
@ -16,38 +16,64 @@
|
|||||||
<h3>Authentication</h3>
|
<h3>Authentication</h3>
|
||||||
<p>
|
<p>
|
||||||
<label>
|
<label>
|
||||||
<input name="input" type="checkbox" ng-model="$ctrl.settings.auth_password_login_enabled"
|
<input name="input" type="checkbox" ng-model="$ctrl.settings.auth_password_login_enabled" ng-change="$ctrl.update('auth_password_login_enabled')"
|
||||||
ng-change="$ctrl.update('auth_password_login_enabled')" accesskey="tab">
|
accesskey="tab" ng-disabled="$ctrl.disablePasswordLoginToggle()"> Password Login Enabled
|
||||||
Password Login Enabled
|
<span uib-popover="Password login can be disabled only if another login method is enabled." popover-trigger="'mouseenter'"
|
||||||
|
ng-if="$ctrl.disablePasswordLoginToggle()">
|
||||||
|
<i class="fa fa-question-circle"></i>
|
||||||
|
</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
<div class="callout callout-warning" ng-if="!$ctrl.settings.auth_password_login_enabled">
|
||||||
|
Password based login is currently disabled and users will be able to login only with the enabled SSO options.
|
||||||
|
</div>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<div ng-if="$ctrl.googleLoginEnabled">
|
||||||
|
<h4>Google Login</h4>
|
||||||
|
<label>
|
||||||
|
Allowed Google Apps Domains
|
||||||
|
</label>
|
||||||
|
<ui-select multiple tagging tagging-label="false" ng-model="$ctrl.settings.auth_google_apps_domains" tagging-tokens="SPACE|,"
|
||||||
|
title="Google Apps Domain(s)"
|
||||||
|
ng-change="$ctrl.update('auth_google_apps_domains')">
|
||||||
|
<ui-select-match placeholder="Google Apps Domain(s)">{{$item}}</ui-select-match>
|
||||||
|
<!-- the ui-select-choices is here just to make ui-select work -->
|
||||||
|
<ui-select-choices repeat="domain in $ctrl.domains">
|
||||||
|
{{domain}}
|
||||||
|
</ui-select-choices>
|
||||||
|
</ui-select>
|
||||||
|
|
||||||
|
<div class="callout callout-info m-t-5" ng-if="$ctrl.settings.auth_google_apps_domains | notEmpty">
|
||||||
|
Any user registered with a <strong>{{$ctrl.settings.auth_google_apps_domains | join}}</strong> Google Apps account will be able to login. If they don't have an existing user, a new user will be created and join the <strong>Default</strong> group.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h4>SAML</h4>
|
<h4>SAML</h4>
|
||||||
<p>
|
<p>
|
||||||
<label>
|
<label>
|
||||||
<input name="input" type="checkbox" ng-model="$ctrl.settings.auth_saml_enabled"
|
<input name="input" type="checkbox" ng-model="$ctrl.settings.auth_saml_enabled" ng-change="$ctrl.update('auth_saml_enabled')"
|
||||||
ng-change="$ctrl.update('auth_saml_enabled')" accesskey="tab">
|
accesskey="tab"> SAML Enabled
|
||||||
SAML Enabled
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div ng-show="$ctrl.settings.auth_saml_enabled">
|
<div ng-show="$ctrl.settings.auth_saml_enabled">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>SAML Metadata URL</label>
|
<label>SAML Metadata URL</label>
|
||||||
<input name="input" type="string" class="form-control" ng-model="$ctrl.settings.auth_saml_metadata_url" accesskey="tab"
|
<input name="input" type="string" class="form-control" ng-model="$ctrl.settings.auth_saml_metadata_url" accesskey="tab" ng-change="$ctrl.update('auth_saml_metadata_url')"
|
||||||
ng-change="$ctrl.update('auth_saml_metadata_url')" ng-model-options="{ debounce: 200 }">
|
ng-model-options="{ debounce: 200 }">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>SAML Entity ID</label>
|
<label>SAML Entity ID</label>
|
||||||
<input name="input" type="string" class="form-control" ng-model="$ctrl.settings.auth_saml_entity_id" accesskey="tab"
|
<input name="input" type="string" class="form-control" ng-model="$ctrl.settings.auth_saml_entity_id" accesskey="tab" ng-change="$ctrl.update('auth_saml_entity_id')"
|
||||||
ng-change="$ctrl.update('auth_saml_entity_id')" ng-model-options="{ debounce: 200 }">
|
ng-model-options="{ debounce: 200 }">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>SAML NameID Format</label>
|
<label>SAML NameID Format</label>
|
||||||
<input name="input" type="string" class="form-control" ng-model="$ctrl.settings.auth_saml_nameid_format" accesskey="tab"
|
<input name="input" type="string" class="form-control" ng-model="$ctrl.settings.auth_saml_nameid_format" accesskey="tab"
|
||||||
ng-change="$ctrl.update('auth_saml_nameid_format')" ng-model-options="{ debounce: 200 }">
|
ng-change="$ctrl.update('auth_saml_nameid_format')" ng-model-options="{ debounce: 200 }">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</settings-screen>
|
</settings-screen>
|
@ -1,7 +1,7 @@
|
|||||||
import settingsMenu from '@/lib/settings-menu';
|
import settingsMenu from '@/lib/settings-menu';
|
||||||
import template from './organization.html';
|
import template from './organization.html';
|
||||||
|
|
||||||
function OrganizationSettingsCtrl($http, toastr, Events) {
|
function OrganizationSettingsCtrl($http, toastr, clientConfig, Events) {
|
||||||
Events.record('view', 'page', 'org_settings');
|
Events.record('view', 'page', 'org_settings');
|
||||||
|
|
||||||
this.settings = {};
|
this.settings = {};
|
||||||
@ -13,10 +13,20 @@ function OrganizationSettingsCtrl($http, toastr, Events) {
|
|||||||
$http.post('api/settings/organization', { [key]: this.settings[key] }).then((response) => {
|
$http.post('api/settings/organization', { [key]: this.settings[key] }).then((response) => {
|
||||||
this.settings = response.data.settings;
|
this.settings = response.data.settings;
|
||||||
toastr.success('Settings changes saved.');
|
toastr.success('Settings changes saved.');
|
||||||
|
|
||||||
|
if (this.disablePasswordLoginToggle() && this.settings.auth_password_login_enabled === false) {
|
||||||
|
this.settings.auth_password_login_enabled = true;
|
||||||
|
this.update('auth_password_login_enabled');
|
||||||
|
}
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
toastr.error('Failed saving changes.');
|
toastr.error('Failed saving changes.');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this.googleLoginEnabled = clientConfig.googleLoginEnabled;
|
||||||
|
|
||||||
|
this.disablePasswordLoginToggle = () =>
|
||||||
|
(clientConfig.googleLoginEnabled || this.settings.auth_saml_enabled) === false;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function init(ngModule) {
|
export default function init(ngModule) {
|
||||||
|
@ -64,6 +64,7 @@ def render_token_login_page(template, org_slug, token):
|
|||||||
show_saml_login=current_org.get_setting('auth_saml_enabled'),
|
show_saml_login=current_org.get_setting('auth_saml_enabled'),
|
||||||
show_remote_user_login=settings.REMOTE_USER_LOGIN_ENABLED,
|
show_remote_user_login=settings.REMOTE_USER_LOGIN_ENABLED,
|
||||||
show_ldap_login=settings.LDAP_LOGIN_ENABLED,
|
show_ldap_login=settings.LDAP_LOGIN_ENABLED,
|
||||||
|
org_slug=org_slug,
|
||||||
user=user), status_code
|
user=user), status_code
|
||||||
|
|
||||||
|
|
||||||
@ -172,7 +173,8 @@ def client_config():
|
|||||||
'dateFormat': date_format,
|
'dateFormat': date_format,
|
||||||
'dateTimeFormat': "{0} HH:mm".format(date_format),
|
'dateTimeFormat': "{0} HH:mm".format(date_format),
|
||||||
'mailSettingsMissing': settings.MAIL_DEFAULT_SENDER is None,
|
'mailSettingsMissing': settings.MAIL_DEFAULT_SENDER is None,
|
||||||
'dashboardRefreshIntervals': settings.DASHBOARD_REFRESH_INTERVALS
|
'dashboardRefreshIntervals': settings.DASHBOARD_REFRESH_INTERVALS,
|
||||||
|
'googleLoginEnabled': settings.GOOGLE_OAUTH_ENABLED
|
||||||
}
|
}
|
||||||
|
|
||||||
client_config.update(defaults)
|
client_config.update(defaults)
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
from flask import request
|
from flask import request
|
||||||
|
|
||||||
from redash.models import db
|
from redash.models import db, Organization
|
||||||
from redash.handlers.base import BaseResource
|
from redash.handlers.base import BaseResource, record_event
|
||||||
from redash.permissions import require_admin
|
from redash.permissions import require_admin
|
||||||
from redash.settings.organization import settings as org_settings
|
from redash.settings.organization import settings as org_settings
|
||||||
|
|
||||||
|
|
||||||
def get_settings_with_defaults(defaults, values):
|
def get_settings_with_defaults(defaults, org):
|
||||||
|
values = org.settings.get('settings', {})
|
||||||
settings = {}
|
settings = {}
|
||||||
|
|
||||||
for setting, default_value in defaults.iteritems():
|
for setting, default_value in defaults.iteritems():
|
||||||
@ -19,14 +20,15 @@ def get_settings_with_defaults(defaults, values):
|
|||||||
else:
|
else:
|
||||||
settings[setting] = current_value
|
settings[setting] = current_value
|
||||||
|
|
||||||
|
settings['auth_google_apps_domains'] = org.google_apps_domains
|
||||||
|
|
||||||
return settings
|
return settings
|
||||||
|
|
||||||
|
|
||||||
class OrganizationSettings(BaseResource):
|
class OrganizationSettings(BaseResource):
|
||||||
@require_admin
|
@require_admin
|
||||||
def get(self):
|
def get(self):
|
||||||
current_values = self.current_org.settings.get('settings', {})
|
settings = get_settings_with_defaults(org_settings, self.current_org)
|
||||||
settings = get_settings_with_defaults(org_settings, current_values)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"settings": settings
|
"settings": settings
|
||||||
@ -39,13 +41,27 @@ class OrganizationSettings(BaseResource):
|
|||||||
if self.current_org.settings.get('settings') is None:
|
if self.current_org.settings.get('settings') is None:
|
||||||
self.current_org.settings['settings'] = {}
|
self.current_org.settings['settings'] = {}
|
||||||
|
|
||||||
|
previous_values = {}
|
||||||
for k, v in new_values.iteritems():
|
for k, v in new_values.iteritems():
|
||||||
self.current_org.set_setting(k, v)
|
if k == 'auth_google_apps_domains':
|
||||||
|
previous_values[k] = self.current_org.google_apps_domains
|
||||||
|
self.current_org.settings[Organization.SETTING_GOOGLE_APPS_DOMAINS] = v
|
||||||
|
else:
|
||||||
|
previous_values[k] = self.current_org.get_setting(k, raise_on_missing=False)
|
||||||
|
self.current_org.set_setting(k, v)
|
||||||
|
|
||||||
db.session.add(self.current_org)
|
db.session.add(self.current_org)
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
|
|
||||||
settings = get_settings_with_defaults(org_settings, self.current_org.settings['settings'])
|
self.record_event({
|
||||||
|
'action': 'edit',
|
||||||
|
'object_id': self.current_org.id,
|
||||||
|
'object_type': 'settings',
|
||||||
|
'new_values': new_values,
|
||||||
|
'previous_values': previous_values
|
||||||
|
})
|
||||||
|
|
||||||
|
settings = get_settings_with_defaults(org_settings, self.current_org)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"settings": settings
|
"settings": settings
|
||||||
|
@ -341,14 +341,17 @@ class Organization(TimestampMixin, db.Model):
|
|||||||
self.settings['settings'][key] = value
|
self.settings['settings'][key] = value
|
||||||
flag_modified(self, 'settings')
|
flag_modified(self, 'settings')
|
||||||
|
|
||||||
def get_setting(self, key):
|
def get_setting(self, key, raise_on_missing=True):
|
||||||
if key in self.settings.get('settings', {}):
|
if key in self.settings.get('settings', {}):
|
||||||
return self.settings['settings'][key]
|
return self.settings['settings'][key]
|
||||||
|
|
||||||
if key in org_settings:
|
if key in org_settings:
|
||||||
return org_settings[key]
|
return org_settings[key]
|
||||||
|
|
||||||
raise KeyError(key)
|
if raise_on_missing:
|
||||||
|
raise KeyError(key)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def admin_group(self):
|
def admin_group(self):
|
||||||
|
@ -29,15 +29,15 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if show_saml_login %}
|
{% if show_saml_login %}
|
||||||
<a href="/saml/login" class="login-button btn btn-default btn-block">SAML Login</a>
|
<a href="{{ url_for('saml_auth.sp_initiated', org_slug=org_slug) }}" class="login-button btn btn-default btn-block">SAML Login</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if show_remote_user_login %}
|
{% if show_remote_user_login %}
|
||||||
<a href="/remote_user/login" class="login-button btn btn-default btn-block">Remote User Login</a>
|
<a href="{{ url_for('remote_user_auth.login') }}" class="login-button btn btn-default btn-block">Remote User Login</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if show_ldap_login %}
|
{% if show_ldap_login %}
|
||||||
<a href="/ldap_auth/login" class="login-button btn btn-default btn-block">LDAP/SSO Login</a>
|
<a href="{{ url_for('ldap_auth.login') }}" class="login-button btn btn-default btn-block">LDAP/SSO Login</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if show_google_openid or show_saml_login or show_remote_user_login or show_ldap_login %}
|
{% if show_google_openid or show_saml_login or show_remote_user_login or show_ldap_login %}
|
||||||
|
@ -12,4 +12,20 @@ class TestOrganizationSettings(BaseTestCase):
|
|||||||
rv = self.make_request('post', '/api/settings/organization', data={'auth_password_login_enabled': True}, user=admin)
|
rv = self.make_request('post', '/api/settings/organization', data={'auth_password_login_enabled': True}, user=admin)
|
||||||
updated_org = Organization.get_by_slug(self.factory.org.slug)
|
updated_org = Organization.get_by_slug(self.factory.org.slug)
|
||||||
self.assertEqual(rv.json['settings']['auth_password_login_enabled'], True)
|
self.assertEqual(rv.json['settings']['auth_password_login_enabled'], True)
|
||||||
self.assertEqual(updated_org.settings['settings']['auth_password_login_enabled'], True)
|
self.assertEqual(updated_org.settings['settings']['auth_password_login_enabled'], True)
|
||||||
|
|
||||||
|
def test_updates_google_apps_domains(self):
|
||||||
|
admin = self.factory.create_admin()
|
||||||
|
domains = ['example.com']
|
||||||
|
rv = self.make_request('post', '/api/settings/organization', data={'auth_google_apps_domains': domains}, user=admin)
|
||||||
|
updated_org = Organization.get_by_slug(self.factory.org.slug)
|
||||||
|
self.assertEqual(updated_org.google_apps_domains, domains)
|
||||||
|
|
||||||
|
def test_get_returns_google_appas_domains(self):
|
||||||
|
admin = self.factory.create_admin()
|
||||||
|
domains = ['example.com']
|
||||||
|
admin.org.settings[Organization.SETTING_GOOGLE_APPS_DOMAINS] = domains
|
||||||
|
|
||||||
|
rv = self.make_request('get', '/api/settings/organization', user=admin)
|
||||||
|
self.assertEqual(rv.json['settings']['auth_google_apps_domains'], domains)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user