mirror of
https://github.com/valitydev/redash.git
synced 2024-11-06 09:05:17 +00:00
Change multi-org implementation:
To avoid complications with how Google Auth works, when enabling organization multi-tenancy on a single instance, each organization becomes a "sub folder" instead of a sub-domain.
This commit is contained in:
parent
f7b57fa580
commit
7c6b95e71d
@ -8,7 +8,7 @@ if __name__ == '__main__':
|
||||
with db.database.transaction():
|
||||
Organization.create_table()
|
||||
|
||||
default_org = Organization.create(name="Default", settings={
|
||||
default_org = Organization.create(name="Default", slug='default', settings={
|
||||
Organization.SETTING_GOOGLE_APPS_DOMAINS: settings.GOOGLE_APPS_DOMAIN
|
||||
})
|
||||
|
||||
|
@ -4,6 +4,7 @@
|
||||
<!--[if IE 8]> <html class="no-js lt-ie9" ng-app="redash" ng-controller='MainCtrl'> <![endif]-->
|
||||
<!--[if gt IE 8]><!--> <html class="no-js" ng-app="redash" ng-controller='MainCtrl'> <!--<![endif]-->
|
||||
<head>
|
||||
<base href="{{base_href}}">
|
||||
<title ng-bind="'{{name}} | ' + pageTitle"></title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
@ -52,13 +53,13 @@
|
||||
<a href="#" ng-bind="name"></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li ng-repeat="dashboard in group" role="presentation">
|
||||
<a role="menu-item" ng-href="/dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
|
||||
<a role="menu-item" ng-href="dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</span>
|
||||
<li ng-repeat="dashboard in otherDashboards">
|
||||
<a role="menu-item" ng-href="/dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
|
||||
<a role="menu-item" ng-href="dashboard/{{dashboard.slug}}" ng-bind="dashboard.name"></a>
|
||||
</li>
|
||||
<li class="divider" ng-show="currentUser.hasPermission('create_dashboard') && (groupedDashboards.length > 0 || otherDashboards.length > 0)"></li>
|
||||
<li><a data-toggle="modal" href="#new_dashboard_dialog" ng-show="currentUser.hasPermission('create_dashboard')">New Dashboard</a></li>
|
||||
@ -67,12 +68,12 @@
|
||||
<li class="dropdown" ng-show="currentUser.hasPermission('view_query')" dropdown>
|
||||
<a href="#" class="dropdown-toggle" dropdown-toggle>Queries <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu" dropdown-menu>
|
||||
<li ng-show="currentUser.hasPermission('create_query')"><a href="/queries/new">New Query</a></li>
|
||||
<li><a href="/queries">Queries</a></li>
|
||||
<li ng-show="currentUser.hasPermission('create_query')"><a href="queries/new">New Query</a></li>
|
||||
<li><a href="queries">Queries</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/alerts">Alerts</a>
|
||||
<a href="alerts">Alerts</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="navbar-form navbar-left" role="search" ng-submit="searchQueries()">
|
||||
@ -83,16 +84,16 @@
|
||||
</form>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li ng-show="currentUser.hasPermission('admin')">
|
||||
<a href="/data_sources" title="Data Sources"><i class="fa fa-database"></i></a>
|
||||
<a href="data_sources" title="Data Sources"><i class="fa fa-database"></i></a>
|
||||
</li>
|
||||
<li ng-show="currentUser.hasPermission('list_users')">
|
||||
<a href="/users" title="Users"><i class="fa fa-users"></i></a>
|
||||
<a href="users" title="Users"><i class="fa fa-users"></i></a>
|
||||
</li>
|
||||
<li class="dropdown" dropdown>
|
||||
<a href="#" class="dropdown-toggle" dropdown-toggle><span ng-bind="currentUser.name"></span> <span class="caret"></span></a>
|
||||
<ul class="dropdown-menu" dropdown-menu>
|
||||
<li style="width:300px">
|
||||
<a ng-href="/users/{{currentUser.id}}">
|
||||
<a ng-href="users/{{currentUser.id}}">
|
||||
<div class="row">
|
||||
<div class="col-sm-2">
|
||||
<img ng-src="{{currentUser.gravatar_url}}" size="40px" class="img-circle"/>
|
||||
@ -106,7 +107,7 @@
|
||||
<li class="divider">
|
||||
</li>
|
||||
<li>
|
||||
<a href="/logout" target="_self">Log out</a>
|
||||
<a href="logout" target="_self">Log out</a>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
@ -229,6 +230,7 @@
|
||||
// TODO: move currentUser & features to be an Angular service
|
||||
var clientConfig = {{ client_config|safe }};
|
||||
var currentUser = {{ user|safe }};
|
||||
var currentOrgSlug = "{{ org_slug }}";
|
||||
|
||||
currentUser.canEdit = function(object) {
|
||||
var user_id = object.user_id || (object.user && object.user.id);
|
||||
@ -241,6 +243,7 @@
|
||||
|
||||
currentUser.isAdmin = currentUser.hasPermission('admin');
|
||||
|
||||
|
||||
{{ analytics|safe }}
|
||||
</script>
|
||||
|
||||
|
@ -49,7 +49,7 @@
|
||||
{% if show_google_openid %}
|
||||
|
||||
<div class="row">
|
||||
<a href="/oauth/google?next={{next}}"><img src="/google_login.png" class="login-button"/></a>
|
||||
<a href="{{ google_auth_url }}"><img src="/google_login.png" class="login-button"/></a>
|
||||
</div>
|
||||
|
||||
<div class="login-or">
|
||||
|
@ -29,7 +29,7 @@
|
||||
{
|
||||
"label": "Name",
|
||||
"map": "name",
|
||||
"cellTemplate": '<a href="/alerts/{{dataRow.id}}">{{dataRow.name}}</a> (<a href="/queries/{{dataRow.query.id}}">query</a>)'
|
||||
"cellTemplate": '<a href="alerts/{{dataRow.id}}">{{dataRow.name}}</a> (<a href="queries/{{dataRow.query.id}}">query</a>)'
|
||||
},
|
||||
{
|
||||
'label': 'Created By',
|
||||
|
@ -13,7 +13,7 @@
|
||||
{
|
||||
"label": "Name",
|
||||
"map": "name",
|
||||
"cellTemplate": '<a href="/groups/{{dataRow.id}}">{{dataRow.name}}</a>'
|
||||
"cellTemplate": '<a href="groups/{{dataRow.id}}">{{dataRow.name}}</a>'
|
||||
}
|
||||
];
|
||||
|
||||
@ -113,7 +113,7 @@
|
||||
// Clear selection, to clear up the input control.
|
||||
$scope.newDataSource.selected = undefined;
|
||||
|
||||
$http.post('/api/groups/' + $routeParams.groupId + '/data_sources', {'data_source_id': dataSource.id}).success(function(user) {
|
||||
$http.post('api/groups/' + $routeParams.groupId + '/data_sources', {'data_source_id': dataSource.id}).success(function(user) {
|
||||
dataSource.view_only = false;
|
||||
$scope.dataSources.unshift(dataSource);
|
||||
|
||||
@ -124,13 +124,13 @@
|
||||
};
|
||||
|
||||
$scope.changePermission = function(dataSource, viewOnly) {
|
||||
$http.post('/api/groups/' + $routeParams.groupId + '/data_sources/' + dataSource.id, {view_only: viewOnly}).success(function() {
|
||||
$http.post('api/groups/' + $routeParams.groupId + '/data_sources/' + dataSource.id, {view_only: viewOnly}).success(function() {
|
||||
dataSource.view_only = viewOnly;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeDataSource = function(dataSource) {
|
||||
$http.delete('/api/groups/' + $routeParams.groupId + '/data_sources/' + dataSource.id).success(function() {
|
||||
$http.delete('api/groups/' + $routeParams.groupId + '/data_sources/' + dataSource.id).success(function() {
|
||||
$scope.dataSources = _.filter($scope.dataSources, function(ds) { return dataSource != ds; });
|
||||
});
|
||||
};
|
||||
@ -160,14 +160,14 @@
|
||||
// Clear selection, to clear up the input control.
|
||||
$scope.newMember.selected = undefined;
|
||||
|
||||
$http.post('/api/groups/' + $routeParams.groupId + '/members', {'user_id': user.id}).success(function() {
|
||||
$http.post('api/groups/' + $routeParams.groupId + '/members', {'user_id': user.id}).success(function() {
|
||||
$scope.members.unshift(user);
|
||||
user.alreadyMember = true;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.removeMember = function(member) {
|
||||
$http.delete('/api/groups/' + $routeParams.groupId + '/members/' + member.id).success(function() {
|
||||
$http.delete('api/groups/' + $routeParams.groupId + '/members/' + member.id).success(function() {
|
||||
$scope.members = _.filter($scope.members, function(m) { return m != member });
|
||||
|
||||
if ($scope.foundUsers) {
|
||||
@ -191,7 +191,7 @@
|
||||
{
|
||||
"label": "Name",
|
||||
"map": "name",
|
||||
"cellTemplate": '<img src="{{dataRow.gravatar_url}}" height="40px"/> <a href="/users/{{dataRow.id}}">{{dataRow.name}}</a>'
|
||||
"cellTemplate": '<img src="{{dataRow.gravatar_url}}" height="40px"/> <a href="users/{{dataRow.id}}">{{dataRow.name}}</a>'
|
||||
},
|
||||
{
|
||||
'label': 'Joined',
|
||||
|
@ -81,7 +81,7 @@
|
||||
$scope.dashboard.layout = layout;
|
||||
|
||||
layout = JSON.stringify(layout);
|
||||
$http.post('/api/dashboards/' + $scope.dashboard.id, {
|
||||
$http.post('api/dashboards/' + $scope.dashboard.id, {
|
||||
'name': $scope.dashboard.name,
|
||||
'layout': layout
|
||||
}).success(function(response) {
|
||||
@ -94,7 +94,7 @@
|
||||
Events.record(currentUser, 'edit', 'dashboard', $scope.dashboard.id);
|
||||
} else {
|
||||
|
||||
$http.post('/api/dashboards', {
|
||||
$http.post('api/dashboards', {
|
||||
'name': $scope.dashboard.name
|
||||
}).success(function(response) {
|
||||
$(element).modal('hide');
|
||||
|
@ -34,7 +34,7 @@
|
||||
});
|
||||
});
|
||||
|
||||
$http.get('/api/data_sources/types').success(function (types) {
|
||||
$http.get('api/data_sources/types').success(function (types) {
|
||||
setType(types);
|
||||
|
||||
$scope.dataSourceTypes = types;
|
||||
|
@ -40,7 +40,7 @@
|
||||
}
|
||||
}]);
|
||||
|
||||
directives.directive('rdTab', function () {
|
||||
directives.directive('rdTab', ['$location', function ($location) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
scope: {
|
||||
@ -48,9 +48,10 @@
|
||||
'name': '@'
|
||||
},
|
||||
transclude: true,
|
||||
template: '<li class="rd-tab" ng-class="{active: tabId==selectedTab}"><a href="#{{tabId}}">{{name}}<span ng-transclude></span></a></li>',
|
||||
template: '<li class="rd-tab" ng-class="{active: tabId==selectedTab}"><a href="{{basePath}}#{{tabId}}">{{name}}<span ng-transclude></span></a></li>',
|
||||
replace: true,
|
||||
link: function (scope) {
|
||||
scope.basePath = $location.path().substring(1);
|
||||
scope.$watch(function () {
|
||||
return scope.$parent.selectedTab
|
||||
}, function (tab) {
|
||||
@ -58,7 +59,7 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}]);
|
||||
|
||||
directives.directive('rdTabs', ['$location', function ($location) {
|
||||
return {
|
||||
@ -67,9 +68,11 @@
|
||||
tabsCollection: '=',
|
||||
selectedTab: '='
|
||||
},
|
||||
template: '<ul class="nav nav-tabs"><li ng-class="{active: tab==selectedTab}" ng-repeat="tab in tabsCollection"><a href="#{{tab.key}}">{{tab.name}}</a></li></ul>',
|
||||
template: '<ul class="nav nav-tabs"><li ng-class="{active: tab==selectedTab}" ng-repeat="tab in tabsCollection"><a href="{{basePath}}#{{tab.key}}">{{tab.name}}</a></li></ul>',
|
||||
replace: true,
|
||||
link: function ($scope, element, attrs) {
|
||||
$scope.basePath = $location.path().substring(1);
|
||||
console.log($location.path, $location);
|
||||
$scope.selectTab = function (tabKey) {
|
||||
$scope.selectedTab = _.find($scope.tabsCollection, function (tab) {
|
||||
return tab.key == tabKey;
|
||||
|
@ -10,7 +10,7 @@
|
||||
},
|
||||
template: '<small><span class="glyphicon glyphicon-link"></span></small> <a ng-href="{{link}}" class="query-link">{{query.name}}</a>',
|
||||
link: function(scope, element) {
|
||||
scope.link = '/queries/' + scope.query.id;
|
||||
scope.link = 'queries/' + scope.query.id;
|
||||
if (scope.visualization) {
|
||||
if (scope.visualization.type === 'TABLE') {
|
||||
// link to hard-coded table tab instead of the (hidden) visualization tab
|
||||
@ -29,10 +29,10 @@
|
||||
restrict: 'E',
|
||||
template: '<span ng-show="query.id && canViewSource">\
|
||||
<a ng-show="!sourceMode"\
|
||||
ng-href="/queries/{{query.id}}/source#{{selectedTab}}">Show Source\
|
||||
ng-href="queries/{{query.id}}/source#{{selectedTab}}">Show Source\
|
||||
</a>\
|
||||
<a ng-show="sourceMode"\
|
||||
ng-href="/queries/{{query.id}}#{{selectedTab}}">Hide Source\
|
||||
ng-href="queries/{{query.id}}#{{selectedTab}}">Hide Source\
|
||||
</a>\
|
||||
</span>'
|
||||
}
|
||||
@ -50,7 +50,7 @@
|
||||
if (scope.queryResult.getId() == null) {
|
||||
element.attr('href', '');
|
||||
} else {
|
||||
element.attr('href', '/api/queries/' + scope.query.id + '/results/' + scope.queryResult.getId() + '.csv');
|
||||
element.attr('href', 'api/queries/' + scope.query.id + '/results/' + scope.queryResult.getId() + '.csv');
|
||||
element.attr('download', scope.query.name.replace(" ", "_") + moment(scope.queryResult.getUpdatedAt()).format("_YYYY_MM_DD") + ".csv");
|
||||
}
|
||||
});
|
||||
|
@ -1,10 +1,10 @@
|
||||
(function () {
|
||||
var Dashboard = function($resource) {
|
||||
var resource = $resource('/api/dashboards/:slug', {slug: '@slug'}, {
|
||||
var resource = $resource('api/dashboards/:slug', {slug: '@slug'}, {
|
||||
recent: {
|
||||
method: 'get',
|
||||
isArray: true,
|
||||
url: "/api/dashboards/recent"
|
||||
url: "api/dashboards/recent"
|
||||
}});
|
||||
|
||||
resource.prototype.canEdit = function() {
|
||||
|
@ -24,8 +24,8 @@
|
||||
};
|
||||
|
||||
var QueryResult = function ($resource, $timeout, $q) {
|
||||
var QueryResultResource = $resource('/api/query_results/:id', {id: '@id'}, {'post': {'method': 'POST'}});
|
||||
var Job = $resource('/api/jobs/:id', {id: '@id'});
|
||||
var QueryResultResource = $resource('api/query_results/:id', {id: '@id'}, {'post': {'method': 'POST'}});
|
||||
var Job = $resource('api/jobs/:id', {id: '@id'});
|
||||
|
||||
var updateFunction = function (props) {
|
||||
angular.extend(this, props);
|
||||
@ -414,17 +414,17 @@
|
||||
};
|
||||
|
||||
var Query = function ($resource, QueryResult, DataSource) {
|
||||
var Query = $resource('/api/queries/:id', {id: '@id'},
|
||||
var Query = $resource('api/queries/:id', {id: '@id'},
|
||||
{
|
||||
search: {
|
||||
method: 'get',
|
||||
isArray: true,
|
||||
url: "/api/queries/search"
|
||||
url: "api/queries/search"
|
||||
},
|
||||
recent: {
|
||||
method: 'get',
|
||||
isArray: true,
|
||||
url: "/api/queries/recent"
|
||||
url: "api/queries/recent"
|
||||
}});
|
||||
|
||||
Query.newQuery = function () {
|
||||
@ -546,10 +546,10 @@
|
||||
var actions = {
|
||||
'get': {'method': 'GET', 'cache': false, 'isArray': false},
|
||||
'query': {'method': 'GET', 'cache': false, 'isArray': true},
|
||||
'getSchema': {'method': 'GET', 'cache': true, 'isArray': true, 'url': '/api/data_sources/:id/schema'}
|
||||
'getSchema': {'method': 'GET', 'cache': true, 'isArray': true, 'url': 'api/data_sources/:id/schema'}
|
||||
};
|
||||
|
||||
var DataSourceResource = $resource('/api/data_sources/:id', {id: '@id'}, actions);
|
||||
var DataSourceResource = $resource('api/data_sources/:id', {id: '@id'}, actions);
|
||||
|
||||
return DataSourceResource;
|
||||
};
|
||||
@ -577,7 +577,7 @@
|
||||
'delete': {method: 'DELETE', transformResponse: transform}
|
||||
};
|
||||
|
||||
var UserResource = $resource('/api/users/:id', {id: '@id'}, actions);
|
||||
var UserResource = $resource('api/users/:id', {id: '@id'}, actions);
|
||||
|
||||
return UserResource;
|
||||
};
|
||||
@ -586,15 +586,15 @@
|
||||
var actions = {
|
||||
'get': {'method': 'GET', 'cache': false, 'isArray': false},
|
||||
'query': {'method': 'GET', 'cache': false, 'isArray': true},
|
||||
'members': {'method': 'GET', 'cache': true, 'isArray': true, 'url': '/api/groups/:id/members'},
|
||||
'dataSources': {'method': 'GET', 'cache': true, 'isArray': true, 'url': '/api/groups/:id/data_sources'}
|
||||
'members': {'method': 'GET', 'cache': true, 'isArray': true, 'url': 'api/groups/:id/members'},
|
||||
'dataSources': {'method': 'GET', 'cache': true, 'isArray': true, 'url': 'api/groups/:id/data_sources'}
|
||||
};
|
||||
var resource = $resource('/api/groups/:id', {id: '@id'}, actions);
|
||||
var resource = $resource('api/groups/:id', {id: '@id'}, actions);
|
||||
return resource;
|
||||
};
|
||||
|
||||
var AlertSubscription = function ($resource) {
|
||||
var resource = $resource('/api/alerts/:alertId/subscriptions/:userId', {alertId: '@alert_id', userId: '@user.id'});
|
||||
var resource = $resource('api/alerts/:alertId/subscriptions/:userId', {alertId: '@alert_id', userId: '@user.id'});
|
||||
return resource;
|
||||
};
|
||||
|
||||
@ -613,13 +613,13 @@
|
||||
}].concat($http.defaults.transformRequest)
|
||||
}
|
||||
};
|
||||
var resource = $resource('/api/alerts/:id', {id: '@id'}, actions);
|
||||
var resource = $resource('api/alerts/:id', {id: '@id'}, actions);
|
||||
|
||||
return resource;
|
||||
};
|
||||
|
||||
var Widget = function ($resource, Query) {
|
||||
var WidgetResource = $resource('/api/widgets/:id', {id: '@id'});
|
||||
var WidgetResource = $resource('api/widgets/:id', {id: '@id'});
|
||||
|
||||
WidgetResource.prototype.getQuery = function () {
|
||||
if (!this.query && this.visualization) {
|
||||
|
@ -26,7 +26,7 @@
|
||||
var events = this.events;
|
||||
this.events = [];
|
||||
|
||||
$http.post('/api/events', events);
|
||||
$http.post('api/events', events);
|
||||
|
||||
}, 1000);
|
||||
|
||||
|
@ -44,7 +44,7 @@
|
||||
}
|
||||
|
||||
this.$get = ['$resource', function ($resource) {
|
||||
var Visualization = $resource('/api/visualizations/:id', {id: '@id'});
|
||||
var Visualization = $resource('api/visualizations/:id', {id: '@id'});
|
||||
Visualization.visualizations = this.visualizations;
|
||||
Visualization.visualizationTypes = this.visualizationTypes;
|
||||
Visualization.renderVisualizationsTemplate = this.getSwitchTemplate('renderTemplate');
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="container">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/alerts">Alerts</a></li>
|
||||
<li><a href="alerts">Alerts</a></li>
|
||||
<li class="active">{{alert.name || getDefaultName() || "New"}}</li>
|
||||
</ol>
|
||||
<div class="row">
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<p>
|
||||
<a href="/alerts/new" class="btn btn-default"><i class="fa fa-plus"></i> New Alert</a>
|
||||
<a href="alerts/new" class="btn btn-default"><i class="fa fa-plus"></i> New Alert</a>
|
||||
</p>
|
||||
|
||||
<smart-table rows="alerts" columns="gridColumns"
|
||||
|
@ -46,7 +46,7 @@
|
||||
tooltip-placement="bottom">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
|
||||
|
||||
<span class="pull-right">
|
||||
<a class="btn btn-default btn-xs" ng-href="/queries/{{query.id}}#{{widget.visualization.id}}" ng-show="currentUser.hasPermission('view_query')"><span class="glyphicon glyphicon-link"></span></a>
|
||||
<a class="btn btn-default btn-xs" ng-href="queries/{{query.id}}#{{widget.visualization.id}}" ng-show="currentUser.hasPermission('view_query')"><span class="glyphicon glyphicon-link"></span></a>
|
||||
<button type="button" class="btn btn-default btn-xs" ng-show="dashboard.canEdit()" ng-click="deleteWidget()" title="Remove Widget"><span class="glyphicon glyphicon-trash"></span></button>
|
||||
</span>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
<div class="container">
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="/data_sources">Data Sources</a></li>
|
||||
<li><a href="data_sources">Data Sources</a></li>
|
||||
<li class="active">{{dataSource.name || "New"}}</li>
|
||||
</ol>
|
||||
<div class="row">
|
||||
|
@ -9,7 +9,7 @@
|
||||
<i class="fa fa-database"></i> {{dataSource.name}}
|
||||
<button class="btn btn-xs btn-danger pull-right" ng-click="deleteDataSource($event, dataSource)">Delete</button>
|
||||
</div>
|
||||
<a ng-href="/data_sources/new" class="list-group-item">
|
||||
<a ng-href="data_sources/new" class="list-group-item">
|
||||
<i class="fa fa-plus"></i> Add Data Source
|
||||
</a>
|
||||
</div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"><a href="/users">Users</a></li>
|
||||
<li role="presentation" class="active"><a href="/groups">Groups</a></li>
|
||||
<li role="presentation"><a href="users">Users</a></li>
|
||||
<li role="presentation" class="active"><a href="groups">Groups</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="row voffset1">
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"><a href="/users">Users</a></li>
|
||||
<li role="presentation" class="active"><a href="/groups">Groups</a></li>
|
||||
<li role="presentation"><a href="users">Users</a></li>
|
||||
<li role="presentation" class="active"><a href="groups">Groups</a></li>
|
||||
</ul>
|
||||
|
||||
<group-name group="group"></group-name>
|
||||
@ -9,8 +9,8 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-4">
|
||||
<ul class="nav nav-pills">
|
||||
<li role="presentation" class="active"><a href="/groups/{{group.id}}">Members</a></li>
|
||||
<li role="presentation" ng-if="currentUser.isAdmin"><a href="/groups/{{group.id}}/data_sources">Data Sources</a></li>
|
||||
<li role="presentation" class="active"><a href="groups/{{group.id}}">Members</a></li>
|
||||
<li role="presentation" ng-if="currentUser.isAdmin"><a href="groups/{{group.id}}/data_sources">Data Sources</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation"><a href="/users">Users</a></li>
|
||||
<li role="presentation" class="active"><a href="/groups">Groups</a></li>
|
||||
<li role="presentation"><a href="users">Users</a></li>
|
||||
<li role="presentation" class="active"><a href="groups">Groups</a></li>
|
||||
</ul>
|
||||
|
||||
<group-name group="group"></group-name>
|
||||
@ -9,8 +9,8 @@
|
||||
<div class="row">
|
||||
<div class="col-lg-4">
|
||||
<ul class="nav nav-pills">
|
||||
<li role="presentation"><a href="/groups/{{group.id}}">Members</a></li>
|
||||
<li role="presentation" class="active"><a href="/groups/{{group.id}}/data_sources">Data Sources</a></li>
|
||||
<li role="presentation"><a href="groups/{{group.id}}">Members</a></li>
|
||||
<li role="presentation" class="active"><a href="groups/{{group.id}}/data_sources">Data Sources</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-lg-8">
|
||||
|
@ -7,14 +7,14 @@
|
||||
</div>
|
||||
<div class="list-group-item" ng-repeat="dashboard in dashboards" >
|
||||
<button type="button" class="close delete-button" aria-hidden="true" ng-show="dashboard.canEdit()" ng-click="archiveDashboard(dashboard)" tooltip="Delete Dashboard">×</button>
|
||||
<a ng-href="/dashboard/{{dashboard.slug}}">{{dashboard.name}}</a>
|
||||
<a ng-href="dashboard/{{dashboard.slug}}">{{dashboard.name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="currentUser.hasPermission('super_admin')">
|
||||
<div class="list-group">
|
||||
<div class="list-group-item active">Admin</div>
|
||||
<a href="/admin/status" class="list-group-item">Status</a>
|
||||
<a href="admin/status" class="list-group-item">Status</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,9 +1,9 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<p>
|
||||
<a href="/queries/new" class="btn btn-default">New Query</a>
|
||||
<a href="queries/new" class="btn btn-default">New Query</a>
|
||||
<button ng-show="currentUser.hasPermission('create_dashboard')" type="button" class="btn btn-default" data-toggle="modal" href="#new_dashboard_dialog">New Dashboard</button>
|
||||
<a href="/alerts/new" class="btn btn-default">New Alert</a>
|
||||
<a href="alerts/new" class="btn btn-default">New Alert</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
<div class="list-group-item active">
|
||||
Recent Dashboards
|
||||
</div>
|
||||
<a ng-href="/dashboard/{{dashboard.slug}}" class="list-group-item" ng-repeat="dashboard in recentDashboards">
|
||||
<a ng-href="dashboard/{{dashboard.slug}}" class="list-group-item" ng-repeat="dashboard in recentDashboards">
|
||||
{{dashboard.name}}
|
||||
</a>
|
||||
</div>
|
||||
@ -21,14 +21,14 @@
|
||||
<div class="list-group-item active">
|
||||
Recent Queries
|
||||
</div>
|
||||
<a ng-href="/queries/{{query.id}}" class="list-group-item" ng-repeat="query in recentQueries">{{query.name}}</a>
|
||||
<a ng-href="queries/{{query.id}}" class="list-group-item" ng-repeat="query in recentQueries">{{query.name}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="currentUser.hasPermission('admin')" class="row">
|
||||
<div class="list-group">
|
||||
<div class="list-group-item active">Admin</div>
|
||||
<a href="/admin/status" class="list-group-item">Status</a>
|
||||
<a href="admin/status" class="list-group-item">Status</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@ -1 +1 @@
|
||||
<a ng-href="/queries/{{dataRow.id}}">{{dataRow.name}}</a>
|
||||
<a ng-href="queries/{{dataRow.id}}">{{dataRow.name}}</a>
|
@ -4,7 +4,7 @@
|
||||
<div style="width: 100%; position:absolute; top:50px; z-index:2000">
|
||||
<div class="well well-lg" style="width: 70%; margin: auto;">
|
||||
You don't have permission to create new queries on any of the data sources available to you.
|
||||
You can either <a href="/queries">browse existing queries</a>, or ask for additional permissions from your re:dash admin.
|
||||
You can either <a href="queries">browse existing queries</a>, or ask for additional permissions from your re:dash admin.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -1,13 +1,13 @@
|
||||
<div class="container">
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation" class="active"><a href="/users">Users</a></li>
|
||||
<li role="presentation"><a href="/groups">Groups</a></li>
|
||||
<li role="presentation" class="active"><a href="users">Users</a></li>
|
||||
<li role="presentation"><a href="groups">Groups</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="row voffset1">
|
||||
<div class="col-md-12">
|
||||
<p ng-if="currentUser.hasPermission('admin')">
|
||||
<a href="/users/new" class="btn btn-default"><i class="fa fa-plus"></i> New User</a>
|
||||
<a href="users/new" class="btn btn-default"><i class="fa fa-plus"></i> New User</a>
|
||||
</p>
|
||||
|
||||
<smart-table rows="users" columns="gridColumns"
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation" class="active"><a href="/users">Users</a></li>
|
||||
<li role="presentation"><a href="/groups">Groups</a></li>
|
||||
<li role="presentation" class="active"><a href="users">Users</a></li>
|
||||
<li role="presentation"><a href="groups">Groups</a></li>
|
||||
</ul>
|
||||
|
||||
<form class="form" name="userForm" ng-submit="saveUser()" novalidate>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<div class="container">
|
||||
<ul class="nav nav-tabs">
|
||||
<li role="presentation" class="active"><a href="/users">Users</a></li>
|
||||
<li role="presentation"><a href="/groups">Groups</a></li>
|
||||
<li role="presentation" class="active"><a href="users">Users</a></li>
|
||||
<li role="presentation"><a href="groups">Groups</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>{{user.name}}</h2>
|
||||
|
@ -19,7 +19,7 @@
|
||||
<span class="label label-default">Updated: <span am-time-ago="queryResult.getUpdatedAt()"></span></span>
|
||||
|
||||
<span class="pull-right">
|
||||
<a class="btn btn-default btn-xs" ng-href="/queries/{{query.id}}#{{widget.visualization.id}}" target="_blank"><span class="glyphicon glyphicon-link"></span></a>
|
||||
<a class="btn btn-default btn-xs" ng-href="queries/{{query.id}}#{{widget.visualization.id}}" target="_blank"><span class="glyphicon glyphicon-link"></span></a>
|
||||
</span>
|
||||
|
||||
<span class="pull-right">
|
||||
|
@ -3,13 +3,14 @@ import hmac
|
||||
import time
|
||||
import logging
|
||||
|
||||
from flask import redirect, request, jsonify
|
||||
from flask.ext.login import LoginManager
|
||||
|
||||
from flask.ext.login import user_logged_in
|
||||
|
||||
from redash import models, settings
|
||||
from redash.authentication import google_oauth, saml_auth
|
||||
from redash.authentication.org_resolving import current_org
|
||||
from redash.authentication.helper import get_login_url
|
||||
from redash.tasks import record_event
|
||||
|
||||
login_manager = LoginManager()
|
||||
@ -28,7 +29,10 @@ def sign(key, path, expires):
|
||||
|
||||
@login_manager.user_loader
|
||||
def load_user(user_id):
|
||||
return models.User.get_by_id_and_org(user_id, current_org.id)
|
||||
try:
|
||||
return models.User.get_by_id_and_org(user_id, current_org.id)
|
||||
except models.User.DoesNotExist:
|
||||
return None
|
||||
|
||||
|
||||
def hmac_load_user_from_request(request):
|
||||
@ -61,11 +65,12 @@ def get_user_from_api_key(api_key, query_id):
|
||||
return None
|
||||
|
||||
user = None
|
||||
|
||||
try:
|
||||
user = models.User.get_by_api_key(api_key)
|
||||
user = models.User.get_by_api_key_and_org(api_key, current_org.id)
|
||||
except models.User.DoesNotExist:
|
||||
if query_id:
|
||||
query = models.Query.get_by_id(query_id)
|
||||
query = models.Query.get_by_id_and_org(query_id, current_org.id)
|
||||
if query and query.api_key == api_key:
|
||||
user = models.ApiUser(api_key, query.org, query.groups.keys())
|
||||
|
||||
@ -101,10 +106,22 @@ def log_user_logged_in(app, user):
|
||||
record_event.delay(event)
|
||||
|
||||
|
||||
@login_manager.unauthorized_handler
|
||||
def redirect_to_login():
|
||||
if request.is_xhr or '/api/' in request.path:
|
||||
response = jsonify({'message': "Couldn't find resource. Please login and try again."})
|
||||
response.status_code = 404
|
||||
return response
|
||||
|
||||
login_url = get_login_url(next=request.url, external=False)
|
||||
|
||||
return redirect(login_url)
|
||||
|
||||
|
||||
def setup_authentication(app):
|
||||
login_manager.init_app(app)
|
||||
login_manager.anonymous_user = models.AnonymousUser
|
||||
login_manager.login_view = 'login'
|
||||
|
||||
app.secret_key = settings.COOKIE_SECRET
|
||||
app.register_blueprint(google_oauth.blueprint)
|
||||
app.register_blueprint(saml_auth.blueprint)
|
||||
|
@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from flask.ext.login import login_user
|
||||
import requests
|
||||
from flask import redirect, url_for, Blueprint, flash, request
|
||||
from flask import redirect, url_for, Blueprint, flash, request, session
|
||||
from flask_oauthlib.client import OAuth
|
||||
from redash import models, settings
|
||||
from redash.authentication.org_resolving import current_org
|
||||
@ -62,10 +62,16 @@ def create_and_login_user(org, name, email):
|
||||
login_user(user_object, remember=True)
|
||||
|
||||
|
||||
@blueprint.route('/<org_slug>/oauth/google', endpoint="authorize_org")
|
||||
def org_login(org_slug):
|
||||
session['org_slug'] = current_org.slug
|
||||
return redirect(url_for(".authorize", next=request.args.get('next', None)))
|
||||
|
||||
|
||||
@blueprint.route('/oauth/google', endpoint="authorize")
|
||||
def login():
|
||||
next = request.args.get('next', '/')
|
||||
callback = url_for('.callback', _external=True)
|
||||
next = request.args.get('next', url_for("index", org_slug=session.get('org_slug')))
|
||||
logger.debug("Callback url: %s", callback)
|
||||
logger.debug("Next is: %s", next)
|
||||
return google_remote_app().authorize(callback=callback, state=next)
|
||||
@ -86,13 +92,18 @@ def authorized():
|
||||
flash("Validation error. Please retry.")
|
||||
return redirect(url_for('login'))
|
||||
|
||||
if not verify_profile(current_org, profile):
|
||||
logger.warning("User tried to login with unauthorized domain name: %s (org: %s)", profile['email'], current_org)
|
||||
if 'org_slug' in session:
|
||||
org = models.Organization.get_by_slug(session.pop('org_slug'))
|
||||
else:
|
||||
org = current_org
|
||||
|
||||
if not verify_profile(org, profile):
|
||||
logger.warning("User tried to login with unauthorized domain name: %s (org: %s)", profile['email'], org)
|
||||
flash("Your Google Apps domain name isn't allowed.")
|
||||
return redirect(url_for('login'))
|
||||
return redirect(url_for('login', org_slug=org.slug))
|
||||
|
||||
create_and_login_user(current_org.id, profile['name'], profile['email'])
|
||||
create_and_login_user(org.id, profile['name'], profile['email'])
|
||||
|
||||
next = request.args.get('state', '/')
|
||||
next = request.args.get('state') or url_for("index", org_slug=org.slug)
|
||||
|
||||
return redirect(next)
|
||||
|
13
redash/authentication/helper.py
Normal file
13
redash/authentication/helper.py
Normal file
@ -0,0 +1,13 @@
|
||||
from redash import settings
|
||||
from redash.authentication.org_resolving import current_org
|
||||
from flask import url_for, request
|
||||
|
||||
|
||||
# TODO: move this back to authentication/__init__.py after resolving circular depdency between redash.wsgi and redash.handler
|
||||
def get_login_url(external=False, next="/"):
|
||||
if settings.MULTI_ORG:
|
||||
login_url = url_for('login', org_slug=current_org.slug, next=next, _external=external)
|
||||
else:
|
||||
login_url = url_for('login', next=next, _external=external)
|
||||
|
||||
return login_url
|
@ -4,26 +4,19 @@ single_org strategy, which assumes you have a single Organization in your instal
|
||||
"""
|
||||
|
||||
import logging
|
||||
from redash import settings
|
||||
from redash.models import Organization
|
||||
from werkzeug.local import LocalProxy
|
||||
from flask import request
|
||||
|
||||
|
||||
def single_org(request):
|
||||
return Organization.select().first()
|
||||
|
||||
|
||||
def multi_org(request):
|
||||
return Organization.get_by_domain(request.host)
|
||||
|
||||
|
||||
def _get_current_org():
|
||||
org = globals()[settings.ORG_RESOLVING](request)
|
||||
logging.debug("Current organization: %s (resolved with: %s)", org, settings.ORG_RESOLVING)
|
||||
slug = request.view_args.get('org_slug', 'default')
|
||||
org = Organization.get_by_slug(slug)
|
||||
logging.debug("Current organization: %s (slug: %s)", org, slug)
|
||||
return org
|
||||
|
||||
|
||||
# TODO: move to authentication
|
||||
current_org = LocalProxy(_get_current_org)
|
||||
|
||||
|
||||
|
@ -1,11 +1,19 @@
|
||||
from flask import jsonify
|
||||
from flask import jsonify, url_for
|
||||
from flask_login import login_required
|
||||
|
||||
from redash import settings
|
||||
from redash.wsgi import app
|
||||
from redash.permissions import require_super_admin
|
||||
from redash.monitor import get_status
|
||||
|
||||
|
||||
def org_scoped_rule(rule):
|
||||
if settings.MULTI_ORG:
|
||||
return "/<org_slug:org_slug>{}".format(rule)
|
||||
|
||||
return rule
|
||||
|
||||
|
||||
@app.route('/ping', methods=['GET'])
|
||||
def ping():
|
||||
return 'PONG.'
|
||||
|
@ -115,7 +115,7 @@ class AlertSubscriptionResource(BaseResource):
|
||||
'object_type': 'alert'
|
||||
})
|
||||
|
||||
api.add_resource(AlertResource, '/api/alerts/<alert_id>', endpoint='alert')
|
||||
api.add_resource(AlertSubscriptionListResource, '/api/alerts/<alert_id>/subscriptions', endpoint='alert_subscriptions')
|
||||
api.add_resource(AlertSubscriptionResource, '/api/alerts/<alert_id>/subscriptions/<subscriber_id>', endpoint='alert_subscription')
|
||||
api.add_resource(AlertListResource, '/api/alerts', endpoint='alerts')
|
||||
api.add_org_resource(AlertResource, '/api/alerts/<alert_id>', endpoint='alert')
|
||||
api.add_org_resource(AlertSubscriptionListResource, '/api/alerts/<alert_id>/subscriptions', endpoint='alert_subscriptions')
|
||||
api.add_org_resource(AlertSubscriptionResource, '/api/alerts/<alert_id>/subscriptions/<subscriber_id>', endpoint='alert_subscription')
|
||||
api.add_org_resource(AlertListResource, '/api/alerts', endpoint='alerts')
|
||||
|
@ -1,16 +1,19 @@
|
||||
from flask import render_template, request, redirect, session, url_for, flash
|
||||
from flask import render_template, request, redirect, url_for, flash
|
||||
from flask_login import current_user, login_user, logout_user
|
||||
|
||||
from redash import models, settings
|
||||
from redash.wsgi import app
|
||||
from redash.handlers import org_scoped_rule
|
||||
from redash.authentication.org_resolving import current_org
|
||||
from redash.authentication.helper import get_login_url
|
||||
|
||||
|
||||
@app.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
next_path = request.args.get('next', '/')
|
||||
@app.route(org_scoped_rule('/login'), methods=['GET', 'POST'])
|
||||
def login(org_slug=None):
|
||||
index_url = url_for("index", org_slug=org_slug)
|
||||
next_path = request.args.get('next', index_url)
|
||||
|
||||
if current_user.is_authenticated():
|
||||
if current_user.is_authenticated:
|
||||
return redirect(next_path)
|
||||
|
||||
if not settings.PASSWORD_LOGIN_ENABLED:
|
||||
@ -31,19 +34,22 @@ def login():
|
||||
except models.User.DoesNotExist:
|
||||
flash("Wrong email or password.")
|
||||
|
||||
if settings.MULTI_ORG:
|
||||
google_auth_url = url_for('google_oauth.authorize_org', next=next_path, org_slug=current_org.slug)
|
||||
else:
|
||||
google_auth_url = url_for('google_oauth.authorize', next=next_path)
|
||||
|
||||
return render_template("login.html",
|
||||
name=settings.NAME,
|
||||
analytics=settings.ANALYTICS,
|
||||
next=next_path,
|
||||
username=request.form.get('username', ''),
|
||||
show_google_openid=settings.GOOGLE_OAUTH_ENABLED,
|
||||
google_auth_url=google_auth_url,
|
||||
show_saml_login=settings.SAML_LOGIN_ENABLED)
|
||||
|
||||
|
||||
@app.route('/logout')
|
||||
def logout():
|
||||
@app.route(org_scoped_rule('/logout'))
|
||||
def logout(org_slug=None):
|
||||
logout_user()
|
||||
# TODO(@arikfr): need to check if this is really needed.
|
||||
session.pop('openid', None)
|
||||
|
||||
return redirect('/login')
|
||||
return redirect(get_login_url())
|
||||
|
@ -12,6 +12,11 @@ class BaseResource(Resource):
|
||||
super(BaseResource, self).__init__(*args, **kwargs)
|
||||
self._user = None
|
||||
|
||||
def dispatch_request(self, *args, **kwargs):
|
||||
kwargs.pop('org_slug', None)
|
||||
|
||||
return super(BaseResource, self).dispatch_request(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def current_user(self):
|
||||
return current_user._get_current_object()
|
||||
|
@ -62,7 +62,7 @@ class DashboardAPI(BaseResource):
|
||||
|
||||
return dashboard.to_dict(with_widgets=True, user=self.current_user)
|
||||
|
||||
api.add_resource(DashboardListAPI, '/api/dashboards', endpoint='dashboards')
|
||||
api.add_resource(DashboardRecentAPI, '/api/dashboards/recent', endpoint='recent_dashboards')
|
||||
api.add_resource(DashboardAPI, '/api/dashboards/<dashboard_slug>', endpoint='dashboard')
|
||||
api.add_org_resource(DashboardListAPI, '/api/dashboards', endpoint='dashboards')
|
||||
api.add_org_resource(DashboardRecentAPI, '/api/dashboards/recent', endpoint='recent_dashboards')
|
||||
api.add_org_resource(DashboardAPI, '/api/dashboards/<dashboard_slug>', endpoint='dashboard')
|
||||
|
||||
|
@ -16,7 +16,7 @@ class DataSourceTypeListAPI(BaseResource):
|
||||
def get(self):
|
||||
return [q.to_dict() for q in query_runners.values()]
|
||||
|
||||
api.add_resource(DataSourceTypeListAPI, '/api/data_sources/types', endpoint='data_source_types')
|
||||
api.add_org_resource(DataSourceTypeListAPI, '/api/data_sources/types', endpoint='data_source_types')
|
||||
|
||||
|
||||
class DataSourceAPI(BaseResource):
|
||||
@ -80,8 +80,8 @@ class DataSourceListAPI(BaseResource):
|
||||
|
||||
return datasource.to_dict(all=True)
|
||||
|
||||
api.add_resource(DataSourceListAPI, '/api/data_sources', endpoint='data_sources')
|
||||
api.add_resource(DataSourceAPI, '/api/data_sources/<data_source_id>', endpoint='data_source')
|
||||
api.add_org_resource(DataSourceListAPI, '/api/data_sources', endpoint='data_sources')
|
||||
api.add_org_resource(DataSourceAPI, '/api/data_sources/<data_source_id>', endpoint='data_source')
|
||||
|
||||
|
||||
class DataSourceSchemaAPI(BaseResource):
|
||||
@ -91,4 +91,4 @@ class DataSourceSchemaAPI(BaseResource):
|
||||
|
||||
return schema
|
||||
|
||||
api.add_resource(DataSourceSchemaAPI, '/api/data_sources/<data_source_id>/schema')
|
||||
api.add_org_resource(DataSourceSchemaAPI, '/api/data_sources/<data_source_id>/schema')
|
||||
|
@ -1,19 +1,20 @@
|
||||
import logging
|
||||
|
||||
from flask import render_template, request, redirect, session, url_for, flash
|
||||
from flask import render_template
|
||||
from flask.ext.restful import abort
|
||||
|
||||
from flask_login import current_user, login_required
|
||||
from flask_login import login_required
|
||||
|
||||
from redash import models, settings
|
||||
from redash.wsgi import app
|
||||
from redash.utils import json_dumps
|
||||
from redash.handlers import org_scoped_rule
|
||||
from redash.authentication.org_resolving import current_org
|
||||
|
||||
@app.route('/embed/query/<query_id>/visualization/<visualization_id>', methods=['GET'])
|
||||
|
||||
@app.route(org_scoped_rule('/embed/query/<query_id>/visualization/<visualization_id>'), methods=['GET'])
|
||||
@login_required
|
||||
def embed(query_id, visualization_id):
|
||||
# TODO: should use some helper here insetad of current_user.org to allow refactoring of org retrieval logic.
|
||||
query = models.Query.get_by_id_and_org(query_id, current_user.org)
|
||||
def embed(query_id, visualization_id, org_slug=None):
|
||||
# TODO: add event for embed access
|
||||
query = models.Query.get_by_id_and_org(query_id, current_org)
|
||||
vis = query.visualizations.where(models.Visualization.id == visualization_id).first()
|
||||
qr = {}
|
||||
|
||||
|
@ -16,7 +16,7 @@ class EventAPI(BaseResource):
|
||||
record_event.delay(event)
|
||||
|
||||
|
||||
api.add_resource(EventAPI, '/api/events', endpoint='events')
|
||||
api.add_org_resource(EventAPI, '/api/events', endpoint='events')
|
||||
|
||||
|
||||
class MetricsAPI(BaseResource):
|
||||
|
@ -186,9 +186,9 @@ class GroupDataSourceResource(BaseResource):
|
||||
})
|
||||
|
||||
|
||||
api.add_resource(GroupListResource, '/api/groups', endpoint='groups')
|
||||
api.add_resource(GroupResource, '/api/groups/<group_id>', endpoint='group')
|
||||
api.add_resource(GroupMemberListResource, '/api/groups/<group_id>/members', endpoint='group_members')
|
||||
api.add_resource(GroupMemberResource, '/api/groups/<group_id>/members/<user_id>', endpoint='group_member')
|
||||
api.add_resource(GroupDataSourceListResource, '/api/groups/<group_id>/data_sources', endpoint='group_data_sources')
|
||||
api.add_resource(GroupDataSourceResource, '/api/groups/<group_id>/data_sources/<data_source_id>', endpoint='group_data_source')
|
||||
api.add_org_resource(GroupListResource, '/api/groups', endpoint='groups')
|
||||
api.add_org_resource(GroupResource, '/api/groups/<group_id>', endpoint='group')
|
||||
api.add_org_resource(GroupMemberListResource, '/api/groups/<group_id>/members', endpoint='group_members')
|
||||
api.add_org_resource(GroupMemberResource, '/api/groups/<group_id>/members/<user_id>', endpoint='group_member')
|
||||
api.add_org_resource(GroupDataSourceListResource, '/api/groups/<group_id>/data_sources', endpoint='group_data_sources')
|
||||
api.add_org_resource(GroupDataSourceResource, '/api/groups/<group_id>/data_sources/<data_source_id>', endpoint='group_data_source')
|
||||
|
@ -105,7 +105,7 @@ class QueryAPI(BaseResource):
|
||||
query.archive()
|
||||
|
||||
|
||||
api.add_resource(QuerySearchAPI, '/api/queries/search', endpoint='queries_search')
|
||||
api.add_resource(QueryRecentAPI, '/api/queries/recent', endpoint='recent_queries')
|
||||
api.add_resource(QueryListAPI, '/api/queries', endpoint='queries')
|
||||
api.add_resource(QueryAPI, '/api/queries/<query_id>', endpoint='query')
|
||||
api.add_org_resource(QuerySearchAPI, '/api/queries/search', endpoint='queries_search')
|
||||
api.add_org_resource(QueryRecentAPI, '/api/queries/recent', endpoint='recent_queries')
|
||||
api.add_org_resource(QueryListAPI, '/api/queries', endpoint='queries')
|
||||
api.add_org_resource(QueryAPI, '/api/queries/<query_id>', endpoint='query')
|
||||
|
@ -138,8 +138,8 @@ class QueryResultAPI(BaseResource):
|
||||
return make_response(s.getvalue(), 200, headers)
|
||||
|
||||
|
||||
api.add_resource(QueryResultListAPI, '/api/query_results', endpoint='query_results')
|
||||
api.add_resource(QueryResultAPI,
|
||||
api.add_org_resource(QueryResultListAPI, '/api/query_results', endpoint='query_results')
|
||||
api.add_org_resource(QueryResultAPI,
|
||||
'/api/query_results/<query_result_id>',
|
||||
'/api/queries/<query_id>/results.<filetype>',
|
||||
'/api/queries/<query_id>/results/<query_result_id>.<filetype>',
|
||||
@ -156,4 +156,4 @@ class JobAPI(BaseResource):
|
||||
job = QueryTask(job_id=job_id)
|
||||
job.cancel()
|
||||
|
||||
api.add_resource(JobAPI, '/api/jobs/<job_id>', endpoint='job')
|
||||
api.add_org_resource(JobAPI, '/api/jobs/<job_id>', endpoint='job')
|
||||
|
@ -1,33 +1,30 @@
|
||||
import hashlib
|
||||
import json
|
||||
|
||||
from flask import render_template, send_from_directory, current_app
|
||||
from flask import render_template, send_from_directory, current_app, url_for, request
|
||||
from flask_login import current_user, login_required
|
||||
|
||||
from redash import settings, __version__, redis_connection
|
||||
from redash import settings, __version__
|
||||
from redash.handlers import org_scoped_rule
|
||||
from redash.wsgi import app
|
||||
from redash.version_check import get_latest_version
|
||||
from redash.authentication.org_resolving import current_org
|
||||
|
||||
|
||||
|
||||
@app.route('/<path:filename>')
|
||||
def send_static(filename):
|
||||
if current_app.debug:
|
||||
cache_timeout = 0
|
||||
else:
|
||||
cache_timeout = None
|
||||
|
||||
return send_from_directory(settings.STATIC_ASSETS_PATH, filename, cache_timeout=cache_timeout)
|
||||
|
||||
|
||||
@app.route('/admin/<anything>/<whatever>')
|
||||
@app.route('/admin/<anything>')
|
||||
@app.route('/dashboard/<anything>')
|
||||
@app.route('/alerts')
|
||||
@app.route('/alerts/<pk>')
|
||||
@app.route('/queries')
|
||||
@app.route('/data_sources')
|
||||
@app.route('/data_sources/<pk>')
|
||||
@app.route('/users')
|
||||
@app.route('/users/<pk>')
|
||||
@app.route('/groups')
|
||||
@app.route('/groups/<pk>')
|
||||
@app.route('/groups/<pk>/data_sources')
|
||||
@app.route('/queries/<query_id>')
|
||||
@app.route('/queries/<query_id>/<anything>')
|
||||
@app.route('/personal')
|
||||
@app.route('/')
|
||||
@login_required
|
||||
def index(**kwargs):
|
||||
|
||||
email_md5 = hashlib.md5(current_user.email.lower()).hexdigest()
|
||||
gravatar_url = "https://www.gravatar.com/avatar/%s?s=40" % email_md5
|
||||
|
||||
@ -51,20 +48,46 @@ def index(**kwargs):
|
||||
'Cache-Control': 'no-cache, no-store, max-age=0, must-revalidate'
|
||||
}
|
||||
|
||||
if settings.MULTI_ORG:
|
||||
base_href = url_for('index', _external=True, org_slug=current_org.slug)
|
||||
else:
|
||||
base_href = url_for('index', _external=True)
|
||||
|
||||
response = render_template("index.html",
|
||||
user=json.dumps(user),
|
||||
base_href=base_href,
|
||||
name=settings.NAME,
|
||||
org_slug=current_org.slug,
|
||||
client_config=json.dumps(client_config),
|
||||
analytics=settings.ANALYTICS)
|
||||
|
||||
return response, 200, headers
|
||||
|
||||
|
||||
@app.route('/<path:filename>')
|
||||
def send_static(filename):
|
||||
if current_app.debug:
|
||||
cache_timeout = 0
|
||||
else:
|
||||
cache_timeout = None
|
||||
def register_static_routes(rules):
|
||||
# Make sure that / is the first route considered as index.
|
||||
app.add_url_rule(org_scoped_rule("/"), "index", index)
|
||||
|
||||
for rule in rules:
|
||||
app.add_url_rule(org_scoped_rule(rule), None, index)
|
||||
|
||||
rules = ['/admin/<anything>/<whatever>',
|
||||
'/admin/<anything>',
|
||||
'/dashboard/<anything>',
|
||||
'/alerts',
|
||||
'/alerts/<pk>',
|
||||
'/queries',
|
||||
'/data_sources',
|
||||
'/data_sources/<pk>',
|
||||
'/users',
|
||||
'/users/<pk>',
|
||||
'/groups',
|
||||
'/groups/<pk>',
|
||||
'/groups/<pk>/data_sources',
|
||||
'/queries/<query_id>',
|
||||
'/queries/<query_id>/<anything>',
|
||||
'/personal']
|
||||
|
||||
register_static_routes(rules)
|
||||
|
||||
|
||||
return send_from_directory(settings.STATIC_ASSETS_PATH, filename, cache_timeout=cache_timeout)
|
||||
|
@ -95,7 +95,7 @@ class UserResource(BaseResource):
|
||||
return user.to_dict(with_api_key=is_admin_or_owner(user_id))
|
||||
|
||||
|
||||
api.add_resource(UserListResource, '/api/users', endpoint='users')
|
||||
api.add_resource(UserResource, '/api/users/<user_id>', endpoint='user')
|
||||
api.add_org_resource(UserListResource, '/api/users', endpoint='users')
|
||||
api.add_org_resource(UserResource, '/api/users/<user_id>', endpoint='user')
|
||||
|
||||
|
||||
|
@ -47,5 +47,5 @@ class VisualizationResource(BaseResource):
|
||||
|
||||
vis.delete_instance()
|
||||
|
||||
api.add_resource(VisualizationListResource, '/api/visualizations', endpoint='visualizations')
|
||||
api.add_resource(VisualizationResource, '/api/visualizations/<visualization_id>', endpoint='visualization')
|
||||
api.add_org_resource(VisualizationListResource, '/api/visualizations', endpoint='visualizations')
|
||||
api.add_org_resource(VisualizationResource, '/api/visualizations/<visualization_id>', endpoint='visualization')
|
||||
|
@ -51,5 +51,5 @@ class WidgetAPI(BaseResource):
|
||||
|
||||
return {'layout': widget.dashboard.layout}
|
||||
|
||||
api.add_resource(WidgetListAPI, '/api/widgets', endpoint='widgets')
|
||||
api.add_resource(WidgetAPI, '/api/widgets/<int:widget_id>', endpoint='widget')
|
||||
api.add_org_resource(WidgetListAPI, '/api/widgets', endpoint='widgets')
|
||||
api.add_org_resource(WidgetAPI, '/api/widgets/<int:widget_id>', endpoint='widget')
|
||||
|
@ -161,7 +161,7 @@ class Organization(ModelTimestampsMixin, BaseModel):
|
||||
|
||||
id = peewee.PrimaryKeyField()
|
||||
name = peewee.CharField()
|
||||
domain = peewee.CharField(null=True, unique=True)
|
||||
slug = peewee.CharField(unique=True)
|
||||
settings = JSONField()
|
||||
|
||||
class Meta:
|
||||
@ -171,8 +171,8 @@ class Organization(ModelTimestampsMixin, BaseModel):
|
||||
return u"<Organization: {}, {}>".format(self.id, self.name)
|
||||
|
||||
@classmethod
|
||||
def get_by_domain(cls, domain):
|
||||
return cls.get(cls.domain == domain)
|
||||
def get_by_slug(cls, slug):
|
||||
return cls.get(cls.slug == slug)
|
||||
|
||||
@property
|
||||
def default_group(self):
|
||||
@ -291,8 +291,8 @@ class User(ModelTimestampsMixin, BaseModel, BelongsToOrgMixin, UserMixin, Permis
|
||||
return cls.get(cls.email == email, cls.org == org)
|
||||
|
||||
@classmethod
|
||||
def get_by_api_key(cls, api_key):
|
||||
return cls.get(cls.api_key == api_key)
|
||||
def get_by_api_key_and_org(cls, api_key, org):
|
||||
return cls.get(cls.api_key == api_key, cls.org == org)
|
||||
|
||||
@classmethod
|
||||
def all(cls, org):
|
||||
@ -1023,7 +1023,7 @@ all_models = (Organization, Group, DataSource, DataSourceGroup, User, QueryResul
|
||||
|
||||
|
||||
def init_db():
|
||||
default_org = Organization.create(name="Default", settings={})
|
||||
default_org = Organization.create(name="Default", slug='default', settings={})
|
||||
admin_group = Group.create(name='admin', permissions=['admin', 'super_admin'], org=default_org, type=Group.BUILTIN_GROUP)
|
||||
default_group = Group.create(name='default', permissions=Group.DEFAULT_PERMISSIONS, org=default_org, type=Group.BUILTIN_GROUP)
|
||||
|
||||
|
@ -25,6 +25,7 @@ PRESTO_TYPES_MAPPING = {
|
||||
"date" : TYPE_DATE,
|
||||
}
|
||||
|
||||
|
||||
class Presto(BaseQueryRunner):
|
||||
@classmethod
|
||||
def configuration_schema(cls):
|
||||
|
@ -74,7 +74,8 @@ QUERY_RESULTS_CLEANUP_MAX_AGE = int(os.environ.get("REDASH_QUERY_RESULTS_CLEANUP
|
||||
|
||||
AUTH_TYPE = os.environ.get("REDASH_AUTH_TYPE", "api_key")
|
||||
PASSWORD_LOGIN_ENABLED = parse_boolean(os.environ.get("REDASH_PASSWORD_LOGIN_ENABLED", "true"))
|
||||
ORG_RESOLVING = os.environ.get("REDASH_ORG_RESOLVING", "single_org")
|
||||
|
||||
MULTI_ORG = parse_boolean(os.environ.get("REDASH_MULTI_ORG", "false"))
|
||||
|
||||
# The following is deprecated and should be defined with the Organization object
|
||||
GOOGLE_APPS_DOMAIN = set_from_string(os.environ.get("REDASH_GOOGLE_APPS_DOMAIN", ""))
|
||||
@ -153,8 +154,6 @@ SENTRY_DSN = os.environ.get("REDASH_SENTRY_DSN", "")
|
||||
# Client side toggles:
|
||||
ALLOW_SCRIPTS_IN_USER_INPUT = parse_boolean(os.environ.get("REDASH_ALLOW_SCRIPTS_IN_USER_INPUT", "false"))
|
||||
CLIENT_SIDE_METRICS = parse_boolean(os.environ.get("REDASH_CLIENT_SIDE_METRICS", "false"))
|
||||
# http://api.highcharts.com/highcharts#plotOptions.series.turboThreshold
|
||||
HIGHCHARTS_TURBO_THRESHOLD = int(os.environ.get("REDASH_HIGHCHARTS_TURBO_THRESHOLD", "1000"))
|
||||
DATE_FORMAT = os.environ.get("REDASH_DATE_FORMAT", "DD/MM/YY")
|
||||
|
||||
# Features:
|
||||
@ -172,7 +171,6 @@ SCHEMA_RUN_TABLE_SIZE_CALCULATIONS = parse_boolean(os.environ.get("REDASH_SCHEMA
|
||||
COMMON_CLIENT_CONFIG = {
|
||||
'clientSideMetrics': CLIENT_SIDE_METRICS,
|
||||
'allowScriptsInUserInput': ALLOW_SCRIPTS_IN_USER_INPUT,
|
||||
'highChartsTurboThreshold': HIGHCHARTS_TURBO_THRESHOLD,
|
||||
'dateFormat': DATE_FORMAT,
|
||||
'dateTimeFormat': "{0} HH:mm".format(DATE_FORMAT),
|
||||
'allowAllToEditQueries': FEATURE_ALLOW_ALL_TO_EDIT_QUERIES,
|
||||
|
@ -63,6 +63,16 @@ def json_dumps(data):
|
||||
return json.dumps(data, cls=JSONEncoder)
|
||||
|
||||
|
||||
def build_url(request, host, path):
|
||||
parts = request.host.split(':')
|
||||
if len(parts) > 1:
|
||||
port = parts[1]
|
||||
if (port, request.scheme) not in (('80', 'http'), ('443', 'https')):
|
||||
host = '{}:{}'.format(host, port)
|
||||
|
||||
return "{}://{}{}".format(request.scheme, host, path)
|
||||
|
||||
|
||||
class UnicodeWriter:
|
||||
"""
|
||||
A CSV writer which will write rows to CSV file "f",
|
||||
|
@ -8,6 +8,19 @@ from redash import settings, utils, mail, __version__
|
||||
from redash.models import db
|
||||
from redash.metrics.request import provision_app
|
||||
from redash.admin import init_admin
|
||||
from werkzeug.routing import BaseConverter, ValidationError
|
||||
|
||||
|
||||
class SlugConverter(BaseConverter):
|
||||
def to_python(self, value):
|
||||
# This is an ugly workaround for when we enable multi-org and some files are being called by the index rule:
|
||||
if value in ('google_login.png', 'favicon.ico', 'robots.txt', 'views'):
|
||||
raise ValidationError()
|
||||
|
||||
return value
|
||||
|
||||
def to_url(self, value):
|
||||
return value
|
||||
|
||||
|
||||
app = Flask(__name__,
|
||||
@ -17,9 +30,24 @@ app = Flask(__name__,
|
||||
|
||||
# Make sure we get the right referral address even behind proxies like nginx.
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||
app.url_map.converters['org_slug'] = SlugConverter
|
||||
provision_app(app)
|
||||
|
||||
api = Api(app)
|
||||
|
||||
# TODO: remove duplication
|
||||
def org_scoped_rule(rule):
|
||||
if settings.MULTI_ORG:
|
||||
return "/<org_slug>{}".format(rule)
|
||||
|
||||
return rule
|
||||
|
||||
|
||||
class ApiExt(Api):
|
||||
def add_org_resource(self, resource, *urls, **kwargs):
|
||||
urls = [org_scoped_rule(url) for url in urls]
|
||||
return self.add_resource(resource, *urls, **kwargs)
|
||||
|
||||
api = ApiExt(app)
|
||||
init_admin(app)
|
||||
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
Flask==0.10.1
|
||||
Flask-Admin==1.1.0
|
||||
Flask-RESTful==0.3.5
|
||||
Flask-Login==0.2.11
|
||||
Flask-Login==0.3.2
|
||||
Flask-OAuthLib==0.9.2
|
||||
flask-mail==0.9.1
|
||||
passlib==1.6.2
|
||||
|
@ -6,6 +6,7 @@ os.environ['REDASH_CELERY_BROKER'] = "redis://localhost:6379/6"
|
||||
# Dummy values for oauth login
|
||||
os.environ['REDASH_GOOGLE_CLIENT_ID'] = "dummy"
|
||||
os.environ['REDASH_GOOGLE_CLIENT_SECRET'] = "dummy"
|
||||
os.environ['REDASH_MULTI_ORG'] = "true"
|
||||
|
||||
|
||||
import logging
|
||||
@ -19,8 +20,6 @@ settings.DATABASE_CONFIG = {
|
||||
'threadlocals': True
|
||||
}
|
||||
|
||||
settings.ORG_RESOLVING = "multi_org"
|
||||
|
||||
from redash import redis_connection
|
||||
import redash.models
|
||||
from tests.handlers import make_request
|
||||
@ -40,11 +39,19 @@ class BaseTestCase(TestCase):
|
||||
redash.models.create_db(False, True)
|
||||
redis_connection.flushdb()
|
||||
|
||||
def make_request(self, method, path, user=None, data=None, is_json=True):
|
||||
def make_request(self, method, path, org=None, user=None, data=None, is_json=True):
|
||||
if user is None:
|
||||
user = self.factory.user
|
||||
|
||||
if org is None:
|
||||
org = self.factory.org
|
||||
|
||||
if org is not False:
|
||||
path = "/{}{}".format(org.slug, path)
|
||||
|
||||
return make_request(method, path, user, data, is_json)
|
||||
|
||||
|
||||
def assertResponseEqual(self, expected, actual):
|
||||
for k, v in expected.iteritems():
|
||||
if isinstance(v, datetime.datetime) or isinstance(actual[k], datetime.datetime):
|
||||
|
@ -45,7 +45,7 @@ user_factory = ModelFactory(redash.models.User,
|
||||
|
||||
org_factory = ModelFactory(redash.models.Organization,
|
||||
name=Sequence("Org {}"),
|
||||
domain=Sequence("org{}.example.com"),
|
||||
slug=Sequence("org{}.example.com"),
|
||||
settings={})
|
||||
|
||||
data_source_factory = ModelFactory(redash.models.DataSource,
|
||||
|
File diff suppressed because one or more lines are too long
@ -44,8 +44,7 @@ def make_request(method, path, user, data=None, is_json=True):
|
||||
else:
|
||||
content_type = None
|
||||
|
||||
base_url = "http://{}".format(user.org.domain)
|
||||
response = method_fn(path, data=data, headers=headers, content_type=content_type, base_url=base_url)
|
||||
response = method_fn(path, data=data, headers=headers, content_type=content_type)
|
||||
|
||||
if response.data and is_json:
|
||||
response.json = json.loads(response.data)
|
||||
|
@ -20,13 +20,13 @@ class TestAlertResourceGet(BaseTestCase):
|
||||
rv = self.make_request('get', "/api/alerts/{}".format(alert.id))
|
||||
self.assertEqual(rv.status_code, 403)
|
||||
|
||||
def test_returns_403_if_admin_from_another_org(self):
|
||||
def test_returns_404_if_admin_from_another_org(self):
|
||||
second_org = self.factory.create_org()
|
||||
second_org_admin = self.factory.create_admin(org=second_org)
|
||||
|
||||
alert = self.factory.create_alert()
|
||||
|
||||
rv = self.make_request('get', "/api/alerts/{}".format(alert.id), user=second_org_admin)
|
||||
rv = self.make_request('get', "/api/alerts/{}".format(alert.id), org=second_org, user=second_org_admin)
|
||||
self.assertEqual(rv.status_code, 404)
|
||||
|
||||
|
||||
|
@ -1,14 +1,12 @@
|
||||
from tests import BaseTestCase
|
||||
from tests.factories import org_factory
|
||||
from tests.handlers import make_request
|
||||
|
||||
|
||||
class TestDataSourceGetSchema(BaseTestCase):
|
||||
def test_fails_if_user_doesnt_belong_to_org(self):
|
||||
other_user = self.factory.create_user(org=self.factory.create_org())
|
||||
response = make_request("get", "/api/data_sources/{}/schema".format(self.factory.data_source.id), user=other_user)
|
||||
response = self.make_request("get", "/api/data_sources/{}/schema".format(self.factory.data_source.id), user=other_user)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
other_admin = self.factory.create_admin(org=self.factory.create_org())
|
||||
response = make_request("get", "/api/data_sources/{}/schema".format(self.factory.data_source.id), user=other_admin)
|
||||
response = self.make_request("get", "/api/data_sources/{}/schema".format(self.factory.data_source.id), user=other_admin)
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
@ -18,43 +18,52 @@ class TestApiKeyAuthentication(BaseTestCase):
|
||||
super(TestApiKeyAuthentication, self).setUp()
|
||||
self.api_key = 10
|
||||
self.query = self.factory.create_query(api_key=self.api_key)
|
||||
self.query_url = '/{}/api/queries/{}'.format(self.factory.org.slug, self.query.id)
|
||||
self.queries_url = '/{}/api/queries'.format(self.factory.org.slug)
|
||||
|
||||
def test_no_api_key(self):
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/api/queries/{0}'.format(self.query.id))
|
||||
rv = c.get(self.query_url)
|
||||
self.assertIsNone(api_key_load_user_from_request(request))
|
||||
|
||||
def test_wrong_api_key(self):
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/api/queries/{0}'.format(self.query.id), query_string={'api_key': 'whatever'})
|
||||
rv = c.get(self.query_url, query_string={'api_key': 'whatever'})
|
||||
self.assertIsNone(api_key_load_user_from_request(request))
|
||||
|
||||
def test_correct_api_key(self):
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/api/queries/{0}'.format(self.query.id), query_string={'api_key': self.api_key})
|
||||
rv = c.get(self.query_url, query_string={'api_key': self.api_key})
|
||||
self.assertIsNotNone(api_key_load_user_from_request(request))
|
||||
|
||||
def test_no_query_id(self):
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/api/queries', query_string={'api_key': self.api_key})
|
||||
rv = c.get(self.queries_url, query_string={'api_key': self.api_key})
|
||||
self.assertIsNone(api_key_load_user_from_request(request))
|
||||
|
||||
def test_user_api_key(self):
|
||||
user = self.factory.create_user(api_key="user_key")
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/api/queries/', query_string={'api_key': user.api_key})
|
||||
rv = c.get(self.queries_url, query_string={'api_key': user.api_key})
|
||||
self.assertEqual(user.id, api_key_load_user_from_request(request).id)
|
||||
|
||||
def test_api_key_header(self):
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/api/queries/{}'.format(self.query.id), headers={'Authorization': "Key {}".format(self.api_key)})
|
||||
rv = c.get(self.query_url, headers={'Authorization': "Key {}".format(self.api_key)})
|
||||
self.assertIsNotNone(api_key_load_user_from_request(request))
|
||||
|
||||
def test_api_key_header_with_wrong_key(self):
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/api/queries/{}'.format(self.query.id), headers={'Authorization': "Key oops"})
|
||||
rv = c.get(self.query_url, headers={'Authorization': "Key oops"})
|
||||
self.assertIsNone(api_key_load_user_from_request(request))
|
||||
|
||||
def test_api_key_for_wrong_org(self):
|
||||
other_user = self.factory.create_admin(org=self.factory.create_org())
|
||||
|
||||
with app.test_client() as c:
|
||||
rv = c.get(self.query_url, headers={'Authorization': "Key {}".format(other_user.api_key)})
|
||||
self.assertEqual(404, rv.status_code)
|
||||
|
||||
|
||||
class TestHMACAuthentication(BaseTestCase):
|
||||
#
|
||||
|
@ -10,11 +10,11 @@ from redash.wsgi import app
|
||||
|
||||
|
||||
class AuthenticationTestMixin(object):
|
||||
def test_redirects_when_not_authenticated(self):
|
||||
def test_returns_404_when_not_unauthenticated(self):
|
||||
with app.test_client() as c:
|
||||
for path in self.paths:
|
||||
rv = c.get(path)
|
||||
self.assertEquals(302, rv.status_code)
|
||||
self.assertEquals(404, rv.status_code)
|
||||
|
||||
def test_returns_content_when_authenticated(self):
|
||||
for path in self.paths:
|
||||
@ -25,7 +25,7 @@ class AuthenticationTestMixin(object):
|
||||
class TestAuthentication(BaseTestCase):
|
||||
def test_redirects_for_nonsigned_in_user(self):
|
||||
with app.test_client() as c:
|
||||
rv = c.get("/")
|
||||
rv = c.get("/default/")
|
||||
self.assertEquals(302, rv.status_code)
|
||||
|
||||
|
||||
@ -37,21 +37,32 @@ class PingTest(TestCase):
|
||||
self.assertEquals('PONG.', rv.data)
|
||||
|
||||
|
||||
class IndexTest(BaseTestCase, AuthenticationTestMixin):
|
||||
class IndexTest(BaseTestCase):
|
||||
def setUp(self):
|
||||
self.paths = ['/', '/dashboard/example', '/queries/1', '/admin/status']
|
||||
self.paths = ['/default/', '/default/dashboard/example', '/default/queries/1', '/default/admin/status']
|
||||
super(IndexTest, self).setUp()
|
||||
|
||||
def test_redirect_to_login_when_not_authenticated(self):
|
||||
with app.test_client() as c:
|
||||
for path in self.paths:
|
||||
rv = c.get(path)
|
||||
self.assertEquals(302, rv.status_code)
|
||||
|
||||
def test_returns_content_when_authenticated(self):
|
||||
for path in self.paths:
|
||||
rv = self.make_request('get', path, org=False, is_json=False)
|
||||
self.assertEquals(200, rv.status_code)
|
||||
|
||||
|
||||
class StatusTest(BaseTestCase):
|
||||
def test_returns_data_for_super_admin(self):
|
||||
admin = self.factory.create_admin()
|
||||
|
||||
rv = self.make_request('get', '/status.json', user=admin, is_json=False)
|
||||
rv = self.make_request('get', '/status.json', org=False, user=admin, is_json=False)
|
||||
self.assertEqual(rv.status_code, 200)
|
||||
|
||||
def test_returns_403_for_non_admin(self):
|
||||
rv = self.make_request('get', '/status.json', is_json=False)
|
||||
rv = self.make_request('get', '/status.json', org=False, is_json=False)
|
||||
self.assertEqual(rv.status_code, 403)
|
||||
|
||||
def test_redirects_non_authenticated_user(self):
|
||||
@ -404,18 +415,18 @@ class TestLogin(BaseTestCase):
|
||||
|
||||
def test_redirects_to_google_login_if_password_disabled(self):
|
||||
with app.test_client() as c, patch.object(settings, 'PASSWORD_LOGIN_ENABLED', False):
|
||||
rv = c.get('/login')
|
||||
rv = c.get('/default/login')
|
||||
self.assertEquals(rv.status_code, 302)
|
||||
self.assertTrue(rv.location.endswith(url_for('google_oauth.authorize', next='/')))
|
||||
self.assertTrue(rv.location.endswith(url_for('google_oauth.authorize', next='/default/')))
|
||||
|
||||
def test_get_login_form(self):
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/login')
|
||||
rv = c.get('/default/login')
|
||||
self.assertEquals(rv.status_code, 200)
|
||||
|
||||
def test_submit_non_existing_user(self):
|
||||
with app.test_client() as c, patch('redash.handlers.authentication.login_user') as login_user_mock:
|
||||
rv = c.post('/login', data={'email': 'arik', 'password': 'password'})
|
||||
rv = c.post('/default/login', data={'email': 'arik', 'password': 'password'})
|
||||
self.assertEquals(rv.status_code, 200)
|
||||
self.assertFalse(login_user_mock.called)
|
||||
|
||||
@ -425,7 +436,7 @@ class TestLogin(BaseTestCase):
|
||||
user.save()
|
||||
|
||||
with app.test_client() as c, patch('redash.handlers.authentication.login_user') as login_user_mock:
|
||||
rv = c.post('/login', data={'email': user.email, 'password': 'password'})
|
||||
rv = c.post('/default/login', data={'email': user.email, 'password': 'password'})
|
||||
self.assertEquals(rv.status_code, 302)
|
||||
login_user_mock.assert_called_with(user, remember=False)
|
||||
|
||||
@ -435,7 +446,7 @@ class TestLogin(BaseTestCase):
|
||||
user.save()
|
||||
|
||||
with app.test_client() as c, patch('redash.handlers.authentication.login_user') as login_user_mock:
|
||||
rv = c.post('/login', data={'email': user.email, 'password': 'password', 'remember': True})
|
||||
rv = c.post('/default/login', data={'email': user.email, 'password': 'password', 'remember': True})
|
||||
self.assertEquals(rv.status_code, 302)
|
||||
login_user_mock.assert_called_with(user, remember=True)
|
||||
|
||||
@ -445,7 +456,7 @@ class TestLogin(BaseTestCase):
|
||||
user.save()
|
||||
|
||||
with app.test_client() as c, patch('redash.handlers.authentication.login_user') as login_user_mock:
|
||||
rv = c.post('/login?next=/test',
|
||||
rv = c.post('/default/login?next=/test',
|
||||
data={'email': user.email, 'password': 'password'})
|
||||
self.assertEquals(rv.status_code, 302)
|
||||
self.assertEquals(rv.location, 'http://localhost/test')
|
||||
@ -453,7 +464,7 @@ class TestLogin(BaseTestCase):
|
||||
|
||||
def test_submit_incorrect_user(self):
|
||||
with app.test_client() as c, patch('redash.handlers.authentication.login_user') as login_user_mock:
|
||||
rv = c.post('/login', data={'email': 'non-existing', 'password': 'password'})
|
||||
rv = c.post('/default/login', data={'email': 'non-existing', 'password': 'password'})
|
||||
self.assertEquals(rv.status_code, 200)
|
||||
self.assertFalse(login_user_mock.called)
|
||||
|
||||
@ -463,7 +474,7 @@ class TestLogin(BaseTestCase):
|
||||
user.save()
|
||||
|
||||
with app.test_client() as c, patch('redash.handlers.authentication.login_user') as login_user_mock:
|
||||
rv = c.post('/login', data={'email': user.email, 'password': 'badbadpassword'})
|
||||
rv = c.post('/default/login', data={'email': user.email, 'password': 'badbadpassword'})
|
||||
self.assertEquals(rv.status_code, 200)
|
||||
self.assertFalse(login_user_mock.called)
|
||||
|
||||
@ -471,13 +482,13 @@ class TestLogin(BaseTestCase):
|
||||
user = self.factory.user
|
||||
|
||||
with app.test_client() as c, patch('redash.handlers.authentication.login_user') as login_user_mock:
|
||||
rv = c.post('/login', data={'email': user.email, 'password': ''})
|
||||
rv = c.post('/default/login', data={'email': user.email, 'password': ''})
|
||||
self.assertEquals(rv.status_code, 200)
|
||||
self.assertFalse(login_user_mock.called)
|
||||
|
||||
def test_user_already_loggedin(self):
|
||||
with app.test_client() as c, authenticated_user(c), patch('redash.handlers.authentication.login_user') as login_user_mock:
|
||||
rv = c.get('/login')
|
||||
rv = c.get('/default/login')
|
||||
self.assertEquals(rv.status_code, 302)
|
||||
self.assertFalse(login_user_mock.called)
|
||||
|
||||
@ -485,27 +496,19 @@ class TestLogin(BaseTestCase):
|
||||
|
||||
|
||||
class TestLogout(BaseTestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
settings.ORG_RESOLVING = "single_org"
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
settings.ORG_RESOLVING = "multi_org"
|
||||
|
||||
def test_logout_when_not_loggedin(self):
|
||||
with app.test_client() as c:
|
||||
rv = c.get('/logout')
|
||||
rv = c.get('/default/logout')
|
||||
self.assertEquals(rv.status_code, 302)
|
||||
self.assertFalse(current_user.is_authenticated())
|
||||
self.assertFalse(current_user.is_authenticated)
|
||||
|
||||
def test_logout_when_loggedin(self):
|
||||
with app.test_client() as c, authenticated_user(c, user=self.factory.user):
|
||||
rv = c.get('/')
|
||||
self.assertTrue(current_user.is_authenticated())
|
||||
rv = c.get('/logout')
|
||||
rv = c.get('/default/')
|
||||
self.assertTrue(current_user.is_authenticated)
|
||||
rv = c.get('/default/logout')
|
||||
self.assertEquals(rv.status_code, 302)
|
||||
self.assertFalse(current_user.is_authenticated())
|
||||
self.assertFalse(current_user.is_authenticated)
|
||||
|
||||
|
||||
class DataSourceTypesTest(BaseTestCase):
|
||||
|
24
tests/test_utils.py
Normal file
24
tests/test_utils.py
Normal file
@ -0,0 +1,24 @@
|
||||
from redash.utils import build_url
|
||||
from collections import namedtuple
|
||||
from unittest import TestCase
|
||||
|
||||
DummyRequest = namedtuple('DummyRequest', ['host', 'scheme'])
|
||||
|
||||
|
||||
class TestBuildUrl(TestCase):
|
||||
def test_simple_case(self):
|
||||
self.assertEqual("http://example.com/test", build_url(DummyRequest("", "http"), "example.com", "/test"))
|
||||
|
||||
def test_uses_current_request_port(self):
|
||||
self.assertEqual("http://example.com:5000/test", build_url(DummyRequest("example.com:5000", "http"), "example.com", "/test"))
|
||||
|
||||
def test_uses_current_request_schema(self):
|
||||
self.assertEqual("https://example.com/test", build_url(DummyRequest("example.com", "https"), "example.com", "/test"))
|
||||
|
||||
def test_skips_port_for_default_ports(self):
|
||||
self.assertEqual("https://example.com/test", build_url(DummyRequest("example.com:443", "https"), "example.com", "/test"))
|
||||
self.assertEqual("http://example.com/test", build_url(DummyRequest("example.com:80", "http"), "example.com", "/test"))
|
||||
self.assertEqual("https://example.com:80/test", build_url(DummyRequest("example.com:80", "https"), "example.com", "/test"))
|
||||
self.assertEqual("http://example.com:443/test", build_url(DummyRequest("example.com:443", "http"), "example.com", "/test"))
|
||||
|
||||
# CALL LIOR!!!
|
Loading…
Reference in New Issue
Block a user