Allow API to assign new user to a specific organization (#21775)

* Allow API to assign new user to a specific organization

* Add defer block to test

* Add API tests and return 400 instead of 500 for bad orgId

* Minor test improvements
This commit is contained in:
Émile Fugulin
2020-04-15 05:11:45 -04:00
committed by GitHub
parent da6056d5e1
commit d721dd13cd
8 changed files with 187 additions and 1 deletions

View File

@@ -224,10 +224,13 @@ Content-Type: application/json
**Example Response**: **Example Response**:
```http ```http
HTTP/1.1 200
Content-Type: application/json
``` ```
## Logout User
`POST /api/admin/users/:id/logout` `POST /api/admin/users/:id/logout`
Logout user revokes all auth tokens (devices) for the user. User of issued auth tokens (devices) will no longer be logged in Logout user revokes all auth tokens (devices) for the user. User of issued auth tokens (devices) will no longer be logged in

View File

@@ -14,6 +14,7 @@ func AdminCreateUser(c *models.ReqContext, form dtos.AdminCreateUserForm) {
Email: form.Email, Email: form.Email,
Password: form.Password, Password: form.Password,
Name: form.Name, Name: form.Name,
OrgId: form.OrgId,
} }
if len(cmd.Login) == 0 { if len(cmd.Login) == 0 {
@@ -30,6 +31,11 @@ func AdminCreateUser(c *models.ReqContext, form dtos.AdminCreateUserForm) {
} }
if err := bus.Dispatch(&cmd); err != nil { if err := bus.Dispatch(&cmd); err != nil {
if err == models.ErrOrgNotFound {
c.JsonApiErr(400, models.ErrOrgNotFound.Error(), nil)
return
}
c.JsonApiErr(500, "failed to create user", err) c.JsonApiErr(500, "failed to create user", err)
return return
} }

View File

@@ -12,6 +12,12 @@ import (
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
) )
const (
TestLogin = "test@example.com"
TestPassword = "password"
nonExistingOrgID = 1000
)
func TestAdminApiEndpoint(t *testing.T) { func TestAdminApiEndpoint(t *testing.T) {
role := models.ROLE_ADMIN role := models.ROLE_ADMIN
Convey("Given a server admin attempts to remove themself as an admin", t, func() { Convey("Given a server admin attempts to remove themself as an admin", t, func() {
@@ -175,6 +181,85 @@ func TestAdminApiEndpoint(t *testing.T) {
So(userId, ShouldEqual, 42) So(userId, ShouldEqual, 42)
}) })
}) })
Convey("When a server admin attempts to create a user", t, func() {
var userLogin string
var orgId int64
bus.AddHandler("test", func(cmd *models.CreateUserCommand) error {
userLogin = cmd.Login
orgId = cmd.OrgId
if orgId == nonExistingOrgID {
return models.ErrOrgNotFound
}
cmd.Result = models.User{Id: TestUserID}
return nil
})
Convey("Without an organization", func() {
createCmd := dtos.AdminCreateUserForm{
Login: TestLogin,
Password: TestPassword,
}
adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("id").MustInt64(), ShouldEqual, TestUserID)
So(respJSON.Get("message").MustString(), ShouldEqual, "User created")
// test that userLogin and orgId were transmitted correctly to the handler
So(userLogin, ShouldEqual, TestLogin)
So(orgId, ShouldEqual, 0)
})
})
Convey("With an organization", func() {
createCmd := dtos.AdminCreateUserForm{
Login: TestLogin,
Password: TestPassword,
OrgId: TestOrgID,
}
adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 200)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("id").MustInt64(), ShouldEqual, TestUserID)
So(respJSON.Get("message").MustString(), ShouldEqual, "User created")
So(userLogin, ShouldEqual, TestLogin)
So(orgId, ShouldEqual, TestOrgID)
})
})
Convey("With a nonexistent organization", func() {
createCmd := dtos.AdminCreateUserForm{
Login: TestLogin,
Password: TestPassword,
OrgId: nonExistingOrgID,
}
adminCreateUserScenario("Should create the user", "/api/admin/users", "/api/admin/users", createCmd, func(sc *scenarioContext) {
sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec()
So(sc.resp.Code, ShouldEqual, 400)
respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes())
So(err, ShouldBeNil)
So(respJSON.Get("message").MustString(), ShouldEqual, "Organization not found")
So(userLogin, ShouldEqual, TestLogin)
So(orgId, ShouldEqual, 1000)
})
})
})
} }
func putAdminScenario(desc string, url string, routePattern string, role models.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) { func putAdminScenario(desc string, url string, routePattern string, role models.RoleType, cmd dtos.AdminUpdateUserPermissionsForm, fn scenarioFunc) {
@@ -324,3 +409,21 @@ func adminDeleteUserScenario(desc string, url string, routePattern string, fn sc
fn(sc) fn(sc)
}) })
} }
func adminCreateUserScenario(desc string, url string, routePattern string, cmd dtos.AdminCreateUserForm, fn scenarioFunc) {
Convey(desc+" "+url, func() {
defer bus.ClearBusHandlers()
sc := setupScenarioContext(url)
sc.defaultHandler = Wrap(func(c *models.ReqContext) {
sc.context = c
sc.context.UserId = TestUserID
AdminCreateUser(c, cmd)
})
sc.m.Post(routePattern, sc.defaultHandler)
fn(sc)
})
}

View File

@@ -18,6 +18,7 @@ type AdminCreateUserForm struct {
Login string `json:"login"` Login string `json:"login"`
Name string `json:"name"` Name string `json:"name"`
Password string `json:"password" binding:"Required"` Password string `json:"password" binding:"Required"`
OrgId int64 `json:"orgId"`
} }
type AdminUpdateUserForm struct { type AdminUpdateUserForm struct {

View File

@@ -58,6 +58,7 @@ type CreateUserCommand struct {
Login string Login string
Name string Name string
Company string Company string
OrgId int64
OrgName string OrgName string
Password string Password string
EmailVerified bool EmailVerified bool

View File

@@ -220,6 +220,18 @@ func DeleteOrg(cmd *models.DeleteOrgCommand) error {
}) })
} }
func verifyExistingOrg(sess *DBSession, orgId int64) error {
var org models.Org
has, err := sess.Where("id=?", orgId).Get(&org)
if err != nil {
return err
}
if !has {
return models.ErrOrgNotFound
}
return nil
}
func getOrCreateOrg(sess *DBSession, orgName string) (int64, error) { func getOrCreateOrg(sess *DBSession, orgName string) (int64, error) {
var org models.Org var org models.Org

View File

@@ -41,6 +41,14 @@ func getOrgIdForNewUser(sess *DBSession, cmd *models.CreateUserCommand) (int64,
return -1, nil return -1, nil
} }
if setting.AutoAssignOrg && cmd.OrgId != 0 {
err := verifyExistingOrg(sess, cmd.OrgId)
if err != nil {
return -1, err
}
return cmd.OrgId, nil
}
orgName := cmd.OrgName orgName := cmd.OrgName
if len(orgName) == 0 { if len(orgName) == 0 {
orgName = util.StringsFallback2(cmd.Email, cmd.Login) orgName = util.StringsFallback2(cmd.Email, cmd.Login)

View File

@@ -6,6 +6,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/grafana/grafana/pkg/setting"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
@@ -63,6 +65,56 @@ func TestUserDataAccess(t *testing.T) {
}) })
}) })
Convey("Given an organization", func() {
autoAssignOrg := setting.AutoAssignOrg
setting.AutoAssignOrg = true
defer func() {
setting.AutoAssignOrg = autoAssignOrg
}()
orgCmd := &models.CreateOrgCommand{Name: "Some Test Org"}
err := CreateOrg(orgCmd)
So(err, ShouldBeNil)
Convey("Creates user assigned to other organization", func() {
cmd := &models.CreateUserCommand{
Email: "usertest@test.com",
Name: "user name",
Login: "user_test_login",
OrgId: orgCmd.Result.Id,
}
err := CreateUser(context.Background(), cmd)
So(err, ShouldBeNil)
Convey("Loading a user", func() {
query := models.GetUserByIdQuery{Id: cmd.Result.Id}
err := GetUserById(&query)
So(err, ShouldBeNil)
So(query.Result.Email, ShouldEqual, "usertest@test.com")
So(query.Result.Password, ShouldEqual, "")
So(query.Result.Rands, ShouldHaveLength, 10)
So(query.Result.Salt, ShouldHaveLength, 10)
So(query.Result.IsDisabled, ShouldBeFalse)
So(query.Result.OrgId, ShouldEqual, orgCmd.Result.Id)
})
})
Convey("Don't create user assigned to unknown organization", func() {
const nonExistingOrgID = 10000
cmd := &models.CreateUserCommand{
Email: "usertest@test.com",
Name: "user name",
Login: "user_test_login",
OrgId: nonExistingOrgID,
}
err := CreateUser(context.Background(), cmd)
So(err, ShouldEqual, models.ErrOrgNotFound)
})
})
Convey("Given 5 users", func() { Convey("Given 5 users", func() {
users := createFiveTestUsers(func(i int) *models.CreateUserCommand { users := createFiveTestUsers(func(i int) *models.CreateUserCommand {
return &models.CreateUserCommand{ return &models.CreateUserCommand{