Commit a8aab0cb by Torkel Ödegaard

Worked on user administration page, a grafana server admin can now add and edit…

Worked on user administration page, a grafana server admin can now add and edit organization roles for any user, #2014
parent 788e7fd3
...@@ -9,16 +9,6 @@ import ( ...@@ -9,16 +9,6 @@ import (
"github.com/grafana/grafana/pkg/util" "github.com/grafana/grafana/pkg/util"
) )
func AdminSearchUsers(c *middleware.Context) {
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
if err := bus.Dispatch(&query); err != nil {
c.JsonApiErr(500, "Failed to fetch users", err)
return
}
c.JSON(200, query.Result)
}
func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) { func AdminCreateUser(c *middleware.Context, form dtos.AdminCreateUserForm) {
cmd := m.CreateUserCommand{ cmd := m.CreateUserCommand{
Login: form.Login, Login: form.Login,
......
...@@ -53,7 +53,8 @@ func Register(r *macaron.Macaron) { ...@@ -53,7 +53,8 @@ func Register(r *macaron.Macaron) {
// authed api // authed api
r.Group("/api", func() { r.Group("/api", func() {
// user
// user (signed in)
r.Group("/user", func() { r.Group("/user", func() {
r.Get("/", wrap(GetSignedInUser)) r.Get("/", wrap(GetSignedInUser))
r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser)) r.Put("/", bind(m.UpdateUserCommand{}), wrap(UpdateSignedInUser))
...@@ -64,8 +65,9 @@ func Register(r *macaron.Macaron) { ...@@ -64,8 +65,9 @@ func Register(r *macaron.Macaron) {
r.Put("/password", bind(m.ChangeUserPasswordCommand{}), ChangeUserPassword) r.Put("/password", bind(m.ChangeUserPasswordCommand{}), ChangeUserPassword)
}) })
// users // users (admin permission required)
r.Group("/users", func() { r.Group("/users", func() {
r.Get("/", wrap(SearchUsers))
r.Get("/:id", wrap(GetUserById)) r.Get("/:id", wrap(GetUserById))
r.Get("/:id/orgs", wrap(GetUserOrgList)) r.Get("/:id/orgs", wrap(GetUserOrgList))
r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser)) r.Put("/:id", bind(m.UpdateUserCommand{}), wrap(UpdateUser))
...@@ -84,6 +86,9 @@ func Register(r *macaron.Macaron) { ...@@ -84,6 +86,9 @@ func Register(r *macaron.Macaron) {
// create new org // create new org
r.Post("/orgs", bind(m.CreateOrgCommand{}), wrap(CreateOrg)) r.Post("/orgs", bind(m.CreateOrgCommand{}), wrap(CreateOrg))
// search all orgs
r.Get("/orgs", reqGrafanaAdmin, wrap(SearchOrgs))
// orgs (admin routes) // orgs (admin routes)
r.Group("/orgs/:orgId", func() { r.Group("/orgs/:orgId", func() {
r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrg)) r.Put("/", bind(m.UpdateOrgCommand{}), wrap(UpdateOrg))
...@@ -133,7 +138,6 @@ func Register(r *macaron.Macaron) { ...@@ -133,7 +138,6 @@ func Register(r *macaron.Macaron) {
// admin api // admin api
r.Group("/api/admin", func() { r.Group("/api/admin", func() {
r.Get("/settings", AdminGetSettings) r.Get("/settings", AdminGetSettings)
r.Get("/users", AdminSearchUsers)
r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser) r.Post("/users", bind(dtos.AdminCreateUserForm{}), AdminCreateUser)
r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword) r.Put("/users/:id/password", bind(dtos.AdminUpdateUserPasswordForm{}), AdminUpdateUserPassword)
r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions) r.Put("/users/:id/permissions", bind(dtos.AdminUpdateUserPermissionsForm{}), AdminUpdateUserPermissions)
......
...@@ -72,3 +72,18 @@ func updateOrgHelper(cmd m.UpdateOrgCommand) Response { ...@@ -72,3 +72,18 @@ func updateOrgHelper(cmd m.UpdateOrgCommand) Response {
return ApiSuccess("Organization updated") return ApiSuccess("Organization updated")
} }
func SearchOrgs(c *middleware.Context) Response {
query := m.SearchOrgsQuery{
Query: c.Query("query"),
Name: c.Query("name"),
Page: 0,
Limit: 1000,
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to search orgs", err)
}
return Json(200, query.Result)
}
...@@ -84,6 +84,9 @@ func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response { ...@@ -84,6 +84,9 @@ func updateOrgUserHelper(cmd m.UpdateOrgUserCommand) Response {
} }
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
if err == m.ErrLastOrgAdmin {
return ApiError(400, "Cannot change role so that there is no organization admin left", nil)
}
return ApiError(500, "Failed update org user", err) return ApiError(500, "Failed update org user", err)
} }
......
...@@ -142,3 +142,13 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand) ...@@ -142,3 +142,13 @@ func ChangeUserPassword(c *middleware.Context, cmd m.ChangeUserPasswordCommand)
c.JsonOK("User password changed") c.JsonOK("User password changed")
} }
// GET /api/users
func SearchUsers(c *middleware.Context) Response {
query := m.SearchUsersQuery{Query: "", Page: 0, Limit: 1000}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to fetch users", err)
}
return Json(200, query.Result)
}
...@@ -48,8 +48,13 @@ type GetOrgByNameQuery struct { ...@@ -48,8 +48,13 @@ type GetOrgByNameQuery struct {
Result *Org Result *Org
} }
type GetOrgListQuery struct { type SearchOrgsQuery struct {
Result []*Org Query string
Name string
Limit int
Page int
Result []*OrgDTO
} }
type OrgDTO struct { type OrgDTO struct {
......
...@@ -14,12 +14,23 @@ func init() { ...@@ -14,12 +14,23 @@ func init() {
bus.AddHandler("sql", CreateOrg) bus.AddHandler("sql", CreateOrg)
bus.AddHandler("sql", UpdateOrg) bus.AddHandler("sql", UpdateOrg)
bus.AddHandler("sql", GetOrgByName) bus.AddHandler("sql", GetOrgByName)
bus.AddHandler("sql", GetOrgList) bus.AddHandler("sql", SearchOrgs)
bus.AddHandler("sql", DeleteOrg) bus.AddHandler("sql", DeleteOrg)
} }
func GetOrgList(query *m.GetOrgListQuery) error { func SearchOrgs(query *m.SearchOrgsQuery) error {
return x.Find(&query.Result) query.Result = make([]*m.OrgDTO, 0)
sess := x.Table("org")
if query.Query != "" {
sess.Where("name LIKE ?", query.Query+"%")
}
if query.Name != "" {
sess.Where("name=?", query.Name)
}
sess.Limit(query.Limit, query.Limit*query.Page)
sess.Cols("id", "name")
err := sess.Find(&query.Result)
return err
} }
func GetOrgById(query *m.GetOrgByIdQuery) error { func GetOrgById(query *m.GetOrgByIdQuery) error {
......
...@@ -142,11 +142,18 @@ func TestAccountDataAccess(t *testing.T) { ...@@ -142,11 +142,18 @@ func TestAccountDataAccess(t *testing.T) {
}) })
}) })
Convey("Cannot delete last admin account user", func() { Convey("Cannot delete last admin org user", func() {
cmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac1.Id} cmd := m.RemoveOrgUserCommand{OrgId: ac1.OrgId, UserId: ac1.Id}
err := RemoveOrgUser(&cmd) err := RemoveOrgUser(&cmd)
So(err, ShouldEqual, m.ErrLastOrgAdmin) So(err, ShouldEqual, m.ErrLastOrgAdmin)
}) })
Convey("Cannot update role so no one is admin user", func() {
cmd := m.UpdateOrgUserCommand{OrgId: ac1.OrgId, UserId: ac1.Id, Role: m.ROLE_VIEWER}
err := UpdateOrgUser(&cmd)
So(err, ShouldEqual, m.ErrLastOrgAdmin)
})
}) })
}) })
}) })
......
...@@ -48,7 +48,11 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error { ...@@ -48,7 +48,11 @@ func UpdateOrgUser(cmd *m.UpdateOrgUserCommand) error {
orgUser.Role = cmd.Role orgUser.Role = cmd.Role
orgUser.Updated = time.Now() orgUser.Updated = time.Now()
_, err = sess.Id(orgUser.Id).Update(&orgUser) _, err = sess.Id(orgUser.Id).Update(&orgUser)
return err if err != nil {
return err
}
return validateOneAdminLeftInOrg(cmd.OrgId, sess)
}) })
} }
...@@ -72,16 +76,20 @@ func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error { ...@@ -72,16 +76,20 @@ func RemoveOrgUser(cmd *m.RemoveOrgUserCommand) error {
return err return err
} }
// validate that there is an admin user left return validateOneAdminLeftInOrg(cmd.OrgId, sess)
res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and role='Admin'", cmd.OrgId) })
if err != nil { }
return err
}
if len(res) == 0 {
return m.ErrLastOrgAdmin
}
func validateOneAdminLeftInOrg(orgId int64, sess *xorm.Session) error {
// validate that there is an admin user left
res, err := sess.Query("SELECT 1 from org_user WHERE org_id=? and role='Admin'", orgId)
if err != nil {
return err return err
}) }
if len(res) == 0 {
return m.ErrLastOrgAdmin
}
return err
} }
define([ define([
'angular', 'angular',
'lodash',
], ],
function (angular) { function (angular, _) {
'use strict'; 'use strict';
var module = angular.module('grafana.controllers'); var module = angular.module('grafana.controllers');
module.controller('AdminEditUserCtrl', function($scope, $routeParams, backendSrv, $location) { module.controller('AdminEditUserCtrl', function($scope, $routeParams, backendSrv, $location) {
$scope.user = {}; $scope.user = {};
$scope.newOrg = { name: '', role: 'Editor' };
$scope.permissions = {}; $scope.permissions = {};
$scope.init = function() { $scope.init = function() {
...@@ -64,6 +66,44 @@ function (angular) { ...@@ -64,6 +66,44 @@ function (angular) {
}); });
}; };
$scope.updateOrgUser= function(orgUser) {
backendSrv.patch('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id, orgUser).then(function() {
});
};
$scope.removeOrgUser = function(orgUser) {
backendSrv.delete('/api/orgs/' + orgUser.orgId + '/users/' + $scope.user_id).then(function() {
$scope.getUserOrgs($scope.user_id);
});
};
$scope.orgsSearchCache = [];
$scope.searchOrgs = function(queryStr, callback) {
if ($scope.orgsSearchCache.length > 0) {
callback(_.pluck($scope.orgsSearchCache, "name"));
return;
}
backendSrv.get('/api/orgs', {query: ''}).then(function(result) {
$scope.orgsSearchCache = result;
callback(_.pluck(result, "name"));
});
};
$scope.addOrgUser = function() {
if (!$scope.addOrgForm.$valid) { return; }
var orgInfo = _.findWhere($scope.orgsSearchCache, {name: $scope.newOrg.name});
if (!orgInfo) { return; }
$scope.newOrg.loginOrEmail = $scope.user.login;
backendSrv.post('/api/orgs/' + orgInfo.id + '/users/', $scope.newOrg).then(function() {
$scope.getUserOrgs($scope.user_id);
});
};
$scope.init(); $scope.init();
}); });
......
...@@ -13,7 +13,7 @@ function (angular) { ...@@ -13,7 +13,7 @@ function (angular) {
}; };
$scope.getUsers = function() { $scope.getUsers = function() {
backendSrv.get('/api/admin/users').then(function(users) { backendSrv.get('/api/users').then(function(users) {
$scope.users = users; $scope.users = users;
}); });
}; };
......
...@@ -80,24 +80,52 @@ ...@@ -80,24 +80,52 @@
Permissions Permissions
</h2> </h2>
<div class="tight-form last"> <div>
<ul class="tight-form-list"> <div class="tight-form last">
<li class="tight-form-item last"> <ul class="tight-form-list">
Grafana Admin&nbsp; <li class="tight-form-item last">
<input class="cr1" id="permissions.isGrafanaAdmin" type="checkbox" Grafana Admin&nbsp;
ng-model="permissions.isGrafanaAdmin" ng-checked="permissions.isGrafanaAdmin"> <input class="cr1" id="permissions.isGrafanaAdmin" type="checkbox"
<label for="permissions.isGrafanaAdmin" class="cr1"></label> ng-model="permissions.isGrafanaAdmin" ng-checked="permissions.isGrafanaAdmin">
</li> <label for="permissions.isGrafanaAdmin" class="cr1"></label>
</ul> </li>
<div class="clearfix"></div> </ul>
<div class="clearfix"></div>
</div>
<br>
<button type="submit" class="pull-right btn btn-success" ng-click="updatePermissions()">Update</button>
<br>
</div> </div>
<br>
<button type="submit" class="pull-right btn btn-success" ng-click="updatePermissions()">Update</button>
<h2> <h2>
Organizations Organizations
</h2> </h2>
<form name="addOrgForm">
<div class="tight-form">
<ul class="tight-form-list">
<li class="tight-form-item" style="width: 160px">
Add organization
</li>
<li>
<input type="text" ng-model="newOrg.name" bs-typeahead="searchOrgs"
required class="input-xlarge tight-form-input" placeholder="organization name">
</li>
<li class="tight-form-item">
Role
</li>
<li>
<select type="text" ng-model="newOrg.role" class="input-small tight-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Admin']">
</select>
</li>
<li>
<button class="btn btn-success tight-form-btn" ng-click="addOrgUser()">Add</button>
</li>
<div class="clearfix"></div>
</ul>
</div>
</form>
<table class="grafana-options-table form-inline"> <table class="grafana-options-table form-inline">
<tr> <tr>
<th>Name</th> <th>Name</th>
...@@ -109,15 +137,16 @@ ...@@ -109,15 +137,16 @@
{{org.name}} <span class="label label-info" ng-show="org.orgId === user.orgId">Current</span> {{org.name}} <span class="label label-info" ng-show="org.orgId === user.orgId">Current</span>
</td> </td>
<td> <td>
<select type="text" ng-model="org.role" class="input-small" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="updateOrgRole(org)"> <select type="text" ng-model="org.role" class="input-small" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="updateOrgUser(org)">
</select> </select>
</td> </td>
<td style="width: 1%"> <td style="width: 1%">
<a ng-click="removeUser(user)" class="btn btn-danger btn-mini"> <a ng-click="removeOrgUser(org)" class="btn btn-danger btn-mini">
<i class="fa fa-remove"></i> <i class="fa fa-remove"></i>
</a> </a>
</td> </td>
</tr> </tr>
</table> </table>
</div> </div>
</div> </div>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment