From 59992ae4a4638006ec1489dd834151b258c1728c Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Mon, 31 Jul 2017 12:59:32 -0400 Subject: PLT-6763 Implement user access tokens and new roles (server-side) (#6972) * Implement user access tokens and new roles * Update config.json * Add public post permission to apiv3 * Remove old comment * Fix model unit test * Updates to store per feedback * Updates per feedback from CS --- api4/context.go | 16 ++++ api4/params.go | 5 ++ api4/post.go | 12 ++- api4/post_test.go | 47 +++++++++- api4/user.go | 134 +++++++++++++++++++++++++++++ api4/user_test.go | 252 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 464 insertions(+), 2 deletions(-) (limited to 'api4') diff --git a/api4/context.go b/api4/context.go index 61c318266..d72b3593d 100644 --- a/api4/context.go +++ b/api4/context.go @@ -239,6 +239,11 @@ func (c *Context) IsSystemAdmin() bool { } func (c *Context) SessionRequired() { + if !*utils.Cfg.ServiceSettings.EnableUserAccessTokens && c.Session.Props[model.SESSION_PROP_TYPE] == model.SESSION_TYPE_USER_ACCESS_TOKEN { + c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserAccessToken", http.StatusUnauthorized) + return + } + if len(c.Session.UserId) == 0 { c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized) return @@ -361,6 +366,17 @@ func (c *Context) RequireInviteId() *Context { return c } +func (c *Context) RequireTokenId() *Context { + if c.Err != nil { + return c + } + + if len(c.Params.TokenId) != 26 { + c.SetInvalidUrlParam("token_id") + } + return c +} + func (c *Context) RequireChannelId() *Context { if c.Err != nil { return c diff --git a/api4/params.go b/api4/params.go index 999bf8e77..b48e5fc1b 100644 --- a/api4/params.go +++ b/api4/params.go @@ -20,6 +20,7 @@ type ApiParams struct { UserId string TeamId string InviteId string + TokenId string ChannelId string PostId string FileId string @@ -60,6 +61,10 @@ func ApiParamsFromRequest(r *http.Request) *ApiParams { params.InviteId = val } + if val, ok := props["token_id"]; ok { + params.TokenId = val + } + if val, ok := props["channel_id"]; ok { params.ChannelId = val } diff --git a/api4/post.go b/api4/post.go index 3d0c681d1..deaad1e1c 100644 --- a/api4/post.go +++ b/api4/post.go @@ -40,7 +40,17 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) { post.UserId = c.Session.UserId - if !app.SessionHasPermissionToChannel(c.Session, post.ChannelId, model.PERMISSION_CREATE_POST) { + hasPermission := false + if app.SessionHasPermissionToChannel(c.Session, post.ChannelId, model.PERMISSION_CREATE_POST) { + hasPermission = true + } else if channel, err := app.GetChannel(post.ChannelId); err == nil { + // Temporary permission check method until advanced permissions, please do not copy + if channel.Type == model.CHANNEL_OPEN && app.SessionHasPermissionToTeam(c.Session, channel.TeamId, model.PERMISSION_CREATE_POST_PUBLIC) { + hasPermission = true + } + } + + if !hasPermission { c.SetPermissionError(model.PERMISSION_CREATE_POST) return } diff --git a/api4/post_test.go b/api4/post_test.go index d554ca472..53babc6e6 100644 --- a/api4/post_test.go +++ b/api4/post_test.go @@ -197,7 +197,7 @@ func testCreatePostWithOutgoingHook( if triggerWord != "" { triggerWords = []string{triggerWord} } - + hook = &model.OutgoingWebhook{ ChannelId: channel.Id, TeamId: team.Id, @@ -257,6 +257,51 @@ func TestCreatePostWithOutgoingHook_no_content_type(t *testing.T) { testCreatePostWithOutgoingHook(t, "", "application/x-www-form-urlencoded", "triggerwordaaazzz lorem ipsum", "", []string{"file_id_1, file_id_2"}, app.TRIGGERWORDS_STARTS_WITH) } +func TestCreatePostPublic(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + post := &model.Post{ChannelId: th.BasicChannel.Id, Message: "#hashtag a" + model.NewId() + "a"} + + user := model.User{Email: GenerateTestEmail(), Nickname: "Joram Wilander", Password: "hello1", Username: GenerateTestUsername(), Roles: model.ROLE_SYSTEM_USER.Id} + + ruser, resp := Client.CreateUser(&user) + CheckNoError(t, resp) + + Client.Login(user.Email, user.Password) + + _, resp = Client.CreatePost(post) + CheckForbiddenStatus(t, resp) + + app.UpdateUserRoles(ruser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_POST_ALL_PUBLIC.Id) + app.InvalidateAllCaches() + + Client.Login(user.Email, user.Password) + + _, resp = Client.CreatePost(post) + CheckNoError(t, resp) + + post.ChannelId = th.BasicPrivateChannel.Id + _, resp = Client.CreatePost(post) + CheckForbiddenStatus(t, resp) + + app.UpdateUserRoles(ruser.Id, model.ROLE_SYSTEM_USER.Id) + app.JoinUserToTeam(th.BasicTeam, ruser, "") + app.UpdateTeamMemberRoles(th.BasicTeam.Id, ruser.Id, model.ROLE_TEAM_USER.Id+" "+model.ROLE_TEAM_POST_ALL_PUBLIC.Id) + app.InvalidateAllCaches() + + Client.Login(user.Email, user.Password) + + post.ChannelId = th.BasicPrivateChannel.Id + _, resp = Client.CreatePost(post) + CheckForbiddenStatus(t, resp) + + post.ChannelId = th.BasicChannel.Id + _, resp = Client.CreatePost(post) + CheckNoError(t, resp) +} + func TestUpdatePost(t *testing.T) { th := Setup().InitBasic().InitSystemAdmin() defer TearDown() diff --git a/api4/user.go b/api4/user.go index f13c33f0b..16c1f4a74 100644 --- a/api4/user.go +++ b/api4/user.go @@ -55,6 +55,11 @@ func InitUser() { BaseRoutes.User.Handle("/sessions/revoke", ApiSessionRequired(revokeSession)).Methods("POST") BaseRoutes.Users.Handle("/sessions/device", ApiSessionRequired(attachDeviceId)).Methods("PUT") BaseRoutes.User.Handle("/audits", ApiSessionRequired(getUserAudits)).Methods("GET") + + BaseRoutes.User.Handle("/tokens", ApiSessionRequired(createUserAccessToken)).Methods("POST") + BaseRoutes.User.Handle("/tokens", ApiSessionRequired(getUserAccessTokens)).Methods("GET") + BaseRoutes.Users.Handle("/tokens/{token_id:[A-Za-z0-9]+}", ApiSessionRequired(getUserAccessToken)).Methods("GET") + BaseRoutes.Users.Handle("/tokens/revoke", ApiSessionRequired(revokeUserAccessToken)).Methods("POST") } func createUser(c *Context, w http.ResponseWriter, r *http.Request) { @@ -1081,3 +1086,132 @@ func switchAccountType(c *Context, w http.ResponseWriter, r *http.Request) { c.LogAudit("success") w.Write([]byte(model.MapToJson(map[string]string{"follow_link": link}))) } + +func createUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + accessToken := model.UserAccessTokenFromJson(r.Body) + if accessToken == nil { + c.SetInvalidParam("user_access_token") + return + } + + if accessToken.Description == "" { + c.SetInvalidParam("description") + return + } + + c.LogAudit("") + + if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_CREATE_USER_ACCESS_TOKEN) { + c.SetPermissionError(model.PERMISSION_CREATE_USER_ACCESS_TOKEN) + return + } + + if !app.SessionHasPermissionToUser(c.Session, c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + accessToken.UserId = c.Params.UserId + accessToken.Token = "" + + var err *model.AppError + accessToken, err = app.CreateUserAccessToken(accessToken) + if err != nil { + c.Err = err + return + } + + c.LogAudit("success - token_id=" + accessToken.Id) + w.Write([]byte(accessToken.ToJson())) +} + +func getUserAccessTokens(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireUserId() + if c.Err != nil { + return + } + + if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_READ_USER_ACCESS_TOKEN) { + c.SetPermissionError(model.PERMISSION_READ_USER_ACCESS_TOKEN) + return + } + + if !app.SessionHasPermissionToUser(c.Session, c.Params.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + accessTokens, err := app.GetUserAccessTokensForUser(c.Params.UserId, c.Params.Page, c.Params.PerPage) + if err != nil { + c.Err = err + return + } + + w.Write([]byte(model.UserAccessTokenListToJson(accessTokens))) +} + +func getUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { + c.RequireTokenId() + if c.Err != nil { + return + } + + if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_READ_USER_ACCESS_TOKEN) { + c.SetPermissionError(model.PERMISSION_READ_USER_ACCESS_TOKEN) + return + } + + accessToken, err := app.GetUserAccessToken(c.Params.TokenId, true) + if err != nil { + c.Err = err + return + } + + if !app.SessionHasPermissionToUser(c.Session, accessToken.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + w.Write([]byte(accessToken.ToJson())) +} + +func revokeUserAccessToken(c *Context, w http.ResponseWriter, r *http.Request) { + props := model.MapFromJson(r.Body) + tokenId := props["token_id"] + + if tokenId == "" { + c.SetInvalidParam("token_id") + } + + c.LogAudit("") + + if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_REVOKE_USER_ACCESS_TOKEN) { + c.SetPermissionError(model.PERMISSION_REVOKE_USER_ACCESS_TOKEN) + return + } + + accessToken, err := app.GetUserAccessToken(tokenId, false) + if err != nil { + c.Err = err + return + } + + if !app.SessionHasPermissionToUser(c.Session, accessToken.UserId) { + c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS) + return + } + + err = app.RevokeUserAccessToken(accessToken) + if err != nil { + c.Err = err + return + } + + c.LogAudit("success - token_id=" + accessToken.Id) + ReturnStatusOK(w) +} diff --git a/api4/user_test.go b/api4/user_test.go index 77157e250..37ecd660d 100644 --- a/api4/user_test.go +++ b/api4/user_test.go @@ -2139,3 +2139,255 @@ func TestSwitchAccount(t *testing.T) { _, resp = Client.SwitchAccountType(sr) CheckUnauthorizedStatus(t, resp) } + +func TestCreateUserAccessToken(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + AdminClient := th.SystemAdminClient + + testDescription := "test token" + + enableUserAccessTokens := *utils.Cfg.ServiceSettings.EnableUserAccessTokens + defer func() { + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = enableUserAccessTokens + }() + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = true + + _, resp := Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) + CheckForbiddenStatus(t, resp) + + _, resp = Client.CreateUserAccessToken("notarealuserid", testDescription) + CheckBadRequestStatus(t, resp) + + _, resp = Client.CreateUserAccessToken(th.BasicUser.Id, "") + CheckBadRequestStatus(t, resp) + + app.UpdateUserRoles(th.BasicUser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_USER_ACCESS_TOKEN.Id) + + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = false + _, resp = Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) + CheckNotImplementedStatus(t, resp) + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = true + + rtoken, resp := Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) + CheckNoError(t, resp) + + if rtoken.UserId != th.BasicUser.Id { + t.Fatal("wrong user id") + } else if rtoken.Token == "" { + t.Fatal("token should not be empty") + } else if rtoken.Id == "" { + t.Fatal("id should not be empty") + } else if rtoken.Description != testDescription { + t.Fatal("description did not match") + } + + oldSessionToken := Client.AuthToken + Client.AuthToken = rtoken.Token + ruser, resp := Client.GetMe("") + CheckNoError(t, resp) + + if ruser.Id != th.BasicUser.Id { + t.Fatal("returned wrong user") + } + + Client.AuthToken = oldSessionToken + + _, resp = Client.CreateUserAccessToken(th.BasicUser2.Id, testDescription) + CheckForbiddenStatus(t, resp) + + rtoken, resp = AdminClient.CreateUserAccessToken(th.BasicUser.Id, testDescription) + CheckNoError(t, resp) + + if rtoken.UserId != th.BasicUser.Id { + t.Fatal("wrong user id") + } + + oldSessionToken = Client.AuthToken + Client.AuthToken = rtoken.Token + ruser, resp = Client.GetMe("") + CheckNoError(t, resp) + + if ruser.Id != th.BasicUser.Id { + t.Fatal("returned wrong user") + } +} + +func TestGetUserAccessToken(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + AdminClient := th.SystemAdminClient + + testDescription := "test token" + + enableUserAccessTokens := *utils.Cfg.ServiceSettings.EnableUserAccessTokens + defer func() { + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = enableUserAccessTokens + }() + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = true + + _, resp := Client.GetUserAccessToken("123") + CheckBadRequestStatus(t, resp) + + _, resp = Client.GetUserAccessToken(model.NewId()) + CheckForbiddenStatus(t, resp) + + app.UpdateUserRoles(th.BasicUser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_USER_ACCESS_TOKEN.Id) + token, resp := Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) + CheckNoError(t, resp) + + rtoken, resp := Client.GetUserAccessToken(token.Id) + CheckNoError(t, resp) + + if rtoken.UserId != th.BasicUser.Id { + t.Fatal("wrong user id") + } else if rtoken.Token != "" { + t.Fatal("token should be blank") + } else if rtoken.Id == "" { + t.Fatal("id should not be empty") + } else if rtoken.Description != testDescription { + t.Fatal("description did not match") + } + + _, resp = AdminClient.GetUserAccessToken(token.Id) + CheckNoError(t, resp) + + token, resp = Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) + CheckNoError(t, resp) + + rtokens, resp := Client.GetUserAccessTokensForUser(th.BasicUser.Id, 0, 100) + CheckNoError(t, resp) + + if len(rtokens) != 2 { + t.Fatal("should have 2 tokens") + } + + for _, uat := range rtokens { + if uat.UserId != th.BasicUser.Id { + t.Fatal("wrong user id") + } + } + + rtokens, resp = Client.GetUserAccessTokensForUser(th.BasicUser.Id, 1, 1) + CheckNoError(t, resp) + + if len(rtokens) != 1 { + t.Fatal("should have 1 token") + } + + rtokens, resp = AdminClient.GetUserAccessTokensForUser(th.BasicUser.Id, 0, 100) + CheckNoError(t, resp) + + if len(rtokens) != 2 { + t.Fatal("should have 2 tokens") + } +} + +func TestRevokeUserAccessToken(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + AdminClient := th.SystemAdminClient + + testDescription := "test token" + + enableUserAccessTokens := *utils.Cfg.ServiceSettings.EnableUserAccessTokens + defer func() { + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = enableUserAccessTokens + }() + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = true + + app.UpdateUserRoles(th.BasicUser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_USER_ACCESS_TOKEN.Id) + token, resp := Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) + CheckNoError(t, resp) + + oldSessionToken := Client.AuthToken + Client.AuthToken = token.Token + _, resp = Client.GetMe("") + CheckNoError(t, resp) + Client.AuthToken = oldSessionToken + + ok, resp := Client.RevokeUserAccessToken(token.Id) + CheckNoError(t, resp) + + if !ok { + t.Fatal("should have passed") + } + + oldSessionToken = Client.AuthToken + Client.AuthToken = token.Token + _, resp = Client.GetMe("") + CheckUnauthorizedStatus(t, resp) + Client.AuthToken = oldSessionToken + + token, resp = AdminClient.CreateUserAccessToken(th.BasicUser2.Id, testDescription) + CheckNoError(t, resp) + + ok, resp = Client.RevokeUserAccessToken(token.Id) + CheckForbiddenStatus(t, resp) + + if ok { + t.Fatal("should have failed") + } +} + +func TestUserAccessTokenInactiveUser(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + testDescription := "test token" + + enableUserAccessTokens := *utils.Cfg.ServiceSettings.EnableUserAccessTokens + defer func() { + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = enableUserAccessTokens + }() + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = true + + app.UpdateUserRoles(th.BasicUser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_USER_ACCESS_TOKEN.Id) + token, resp := Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) + CheckNoError(t, resp) + + Client.AuthToken = token.Token + _, resp = Client.GetMe("") + CheckNoError(t, resp) + + app.UpdateActive(th.BasicUser, false) + + _, resp = Client.GetMe("") + CheckUnauthorizedStatus(t, resp) +} + +func TestUserAccessTokenDisableConfig(t *testing.T) { + th := Setup().InitBasic().InitSystemAdmin() + defer TearDown() + Client := th.Client + + testDescription := "test token" + + enableUserAccessTokens := *utils.Cfg.ServiceSettings.EnableUserAccessTokens + defer func() { + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = enableUserAccessTokens + }() + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = true + + app.UpdateUserRoles(th.BasicUser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_USER_ACCESS_TOKEN.Id) + token, resp := Client.CreateUserAccessToken(th.BasicUser.Id, testDescription) + CheckNoError(t, resp) + + oldSessionToken := Client.AuthToken + Client.AuthToken = token.Token + _, resp = Client.GetMe("") + CheckNoError(t, resp) + + *utils.Cfg.ServiceSettings.EnableUserAccessTokens = false + + _, resp = Client.GetMe("") + CheckUnauthorizedStatus(t, resp) + + Client.AuthToken = oldSessionToken + _, resp = Client.GetMe("") + CheckNoError(t, resp) +} -- cgit v1.2.3-1-g7c22