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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ed62660e96
Коммит
59992ae4a4
@@ -308,9 +308,13 @@ func (c *Context) LogDebug(err *model.AppError) {
|
||||
}
|
||||
|
||||
func (c *Context) UserRequired() {
|
||||
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.NewLocAppError("", "api.context.session_expired.app_error", nil, "UserRequired")
|
||||
c.Err.StatusCode = http.StatusUnauthorized
|
||||
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
12
api/post.go
12
api/post.go
@@ -51,7 +51,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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
12
api4/post.go
12
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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
134
api4/user.go
134
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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
130
app/session.go
130
app/session.go
@@ -16,6 +16,8 @@ import (
|
||||
var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE)
|
||||
|
||||
func CreateSession(session *model.Session) (*model.Session, *model.AppError) {
|
||||
session.Token = ""
|
||||
|
||||
if result := <-Srv.Store.Session().Save(session); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
@@ -43,22 +45,31 @@ func GetSession(token string) (*model.Session, *model.AppError) {
|
||||
}
|
||||
|
||||
if session == nil {
|
||||
if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err != nil {
|
||||
return nil, model.NewLocAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": sessionResult.Err.DetailedError}, "")
|
||||
} else {
|
||||
if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err == nil {
|
||||
session = sessionResult.Data.(*model.Session)
|
||||
|
||||
if session == nil || session.IsExpired() || session.Token != token {
|
||||
return nil, model.NewLocAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": ""}, "")
|
||||
} else {
|
||||
AddSessionToCache(session)
|
||||
return session, nil
|
||||
if session != nil {
|
||||
if session.Token != token {
|
||||
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": ""}, "", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
if !session.IsExpired() {
|
||||
AddSessionToCache(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if session == nil {
|
||||
var err *model.AppError
|
||||
session, err = createSessionForUserAccessToken(token)
|
||||
if err != nil {
|
||||
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token}, err.Error(), http.StatusUnauthorized)
|
||||
}
|
||||
}
|
||||
|
||||
if session == nil || session.IsExpired() {
|
||||
return nil, model.NewLocAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token}, "")
|
||||
return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token}, "", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
@@ -200,3 +211,104 @@ func UpdateLastActivityAtIfNeeded(session model.Session) {
|
||||
session.LastActivityAt = now
|
||||
AddSessionToCache(&session)
|
||||
}
|
||||
|
||||
func CreateUserAccessToken(token *model.UserAccessToken) (*model.UserAccessToken, *model.AppError) {
|
||||
if !*utils.Cfg.ServiceSettings.EnableUserAccessTokens {
|
||||
return nil, model.NewAppError("CreateUserAccessToken", "app.user_access_token.disabled", nil, "", http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
token.Token = model.NewId()
|
||||
|
||||
if result := <-Srv.Store.UserAccessToken().Save(token); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
return result.Data.(*model.UserAccessToken), nil
|
||||
}
|
||||
}
|
||||
|
||||
func createSessionForUserAccessToken(tokenString string) (*model.Session, *model.AppError) {
|
||||
if !*utils.Cfg.ServiceSettings.EnableUserAccessTokens {
|
||||
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, "EnableUserAccessTokens=false", http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
var token *model.UserAccessToken
|
||||
if result := <-Srv.Store.UserAccessToken().GetByToken(tokenString); result.Err != nil {
|
||||
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, result.Err.Error(), http.StatusUnauthorized)
|
||||
} else {
|
||||
token = result.Data.(*model.UserAccessToken)
|
||||
}
|
||||
|
||||
var user *model.User
|
||||
if result := <-Srv.Store.User().Get(token.UserId); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
if user.DeleteAt != 0 {
|
||||
return nil, model.NewAppError("createSessionForUserAccessToken", "app.user_access_token.invalid_or_missing", nil, "inactive_user_id="+user.Id, http.StatusUnauthorized)
|
||||
}
|
||||
|
||||
session := &model.Session{
|
||||
Token: token.Token,
|
||||
UserId: user.Id,
|
||||
Roles: user.GetRawRoles(),
|
||||
IsOAuth: false,
|
||||
}
|
||||
|
||||
session.AddProp(model.SESSION_PROP_USER_ACCESS_TOKEN_ID, token.Id)
|
||||
session.AddProp(model.SESSION_PROP_TYPE, model.SESSION_TYPE_USER_ACCESS_TOKEN)
|
||||
session.SetExpireInDays(model.SESSION_USER_ACCESS_TOKEN_EXPIRY)
|
||||
|
||||
if result := <-Srv.Store.Session().Save(session); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
session := result.Data.(*model.Session)
|
||||
|
||||
AddSessionToCache(session)
|
||||
|
||||
return session, nil
|
||||
}
|
||||
}
|
||||
|
||||
func RevokeUserAccessToken(token *model.UserAccessToken) *model.AppError {
|
||||
var session *model.Session
|
||||
if result := <-Srv.Store.Session().Get(token.Token); result.Err == nil {
|
||||
session = result.Data.(*model.Session)
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.UserAccessToken().Delete(token.Id); result.Err != nil {
|
||||
return result.Err
|
||||
}
|
||||
|
||||
if session == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return RevokeSession(session)
|
||||
}
|
||||
|
||||
func GetUserAccessTokensForUser(userId string, page, perPage int) ([]*model.UserAccessToken, *model.AppError) {
|
||||
if result := <-Srv.Store.UserAccessToken().GetByUser(userId, page*perPage, perPage); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
tokens := result.Data.([]*model.UserAccessToken)
|
||||
for _, token := range tokens {
|
||||
token.Token = ""
|
||||
}
|
||||
|
||||
return tokens, nil
|
||||
}
|
||||
}
|
||||
|
||||
func GetUserAccessToken(tokenId string, sanitize bool) (*model.UserAccessToken, *model.AppError) {
|
||||
if result := <-Srv.Store.UserAccessToken().Get(tokenId); result.Err != nil {
|
||||
return nil, result.Err
|
||||
} else {
|
||||
token := result.Data.(*model.UserAccessToken)
|
||||
if sanitize {
|
||||
token.Token = ""
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1224,6 +1224,10 @@ func PermanentDeleteUser(user *model.User) *model.AppError {
|
||||
return result.Err
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.UserAccessToken().DeleteAllForUser(user.Id); result.Err != nil {
|
||||
return result.Err
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.OAuth().PermanentDeleteAuthDataByUser(user.Id); result.Err != nil {
|
||||
return result.Err
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"EnableInsecureOutgoingConnections": false,
|
||||
"EnableMultifactorAuthentication": false,
|
||||
"EnforceMultifactorAuthentication": false,
|
||||
"EnableUserAccessTokens": false,
|
||||
"AllowCorsFrom": "",
|
||||
"SessionLengthWebInDays": 30,
|
||||
"SessionLengthMobileInDays": 30,
|
||||
|
||||
96
i18n/en.json
96
i18n/en.json
@@ -3091,6 +3091,14 @@
|
||||
"id": "api.websocket_handler.invalid_param.app_error",
|
||||
"translation": "Invalid {{.Name}} parameter"
|
||||
},
|
||||
{
|
||||
"id": "app.user_access_token.disabled",
|
||||
"translation": "User access tokens are disabled on this server. Please contact your system administrator for details."
|
||||
},
|
||||
{
|
||||
"id": "app.user_access_token.invalid_or_missing",
|
||||
"translation": "Invalid or missing token"
|
||||
},
|
||||
{
|
||||
"id": "app.channel.create_channel.no_team_id.app_error",
|
||||
"translation": "Must specify the team ID to create a channel"
|
||||
@@ -3495,6 +3503,62 @@
|
||||
"id": "app.import.validate_user_teams_import_data.team_name_missing.error",
|
||||
"translation": "Team name missing from User's Team Membership."
|
||||
},
|
||||
{
|
||||
"id": "authentication.roles.system_post_all_public.name",
|
||||
"translation": "Post in Public Channels"
|
||||
},
|
||||
{
|
||||
"id": "authentication.roles.system_post_all_public.description",
|
||||
"translation": "A role with the permission to post in any public channel on the system"
|
||||
},
|
||||
{
|
||||
"id": "authentication.roles.team_post_all_public.name",
|
||||
"translation": "Post in Public Channels"
|
||||
},
|
||||
{
|
||||
"id": "authentication.roles.team_post_all_public.description",
|
||||
"translation": "A role with the permission to post in any public channel on the team"
|
||||
},
|
||||
{
|
||||
"id": "authentication.roles.system_user_access_token.name",
|
||||
"translation": "User Access Token"
|
||||
},
|
||||
{
|
||||
"id": "authentication.roles.system_user_access_token.description",
|
||||
"translation": "A role with the permissions to create, read and revoke user access tokens"
|
||||
},
|
||||
{
|
||||
"id": "authentication.permissions.create_post_public.name",
|
||||
"translation": "Create Posts in Public Channels"
|
||||
},
|
||||
{
|
||||
"id": "authentication.permissions.create_post_public.description",
|
||||
"translation": "Ability to create posts in public channels"
|
||||
},
|
||||
{
|
||||
"id": "authentication.permissions.create_user_access_token.name",
|
||||
"translation": "Create User Access Token"
|
||||
},
|
||||
{
|
||||
"id": "authentication.permissions.create_user_access_token.description",
|
||||
"translation": "Ability to create user access tokens"
|
||||
},
|
||||
{
|
||||
"id": "authentication.permissions.read_user_access_token.name",
|
||||
"translation": "Read User Access Tokens"
|
||||
},
|
||||
{
|
||||
"id": "authentication.permissions.read_user_access_token.description",
|
||||
"translation": "Ability to read user access tokens' id, description and user_id fields"
|
||||
},
|
||||
{
|
||||
"id": "authentication.permissions.revoke_user_access_token.name",
|
||||
"translation": "Revoke User Access Token"
|
||||
},
|
||||
{
|
||||
"id": "authentication.permissions.revoke_user_access_token.description",
|
||||
"translation": "Ability to revoke user access tokens"
|
||||
},
|
||||
{
|
||||
"id": "authentication.permissions.create_group_channel.description",
|
||||
"translation": "Ability to create new group message channels"
|
||||
@@ -4015,6 +4079,18 @@
|
||||
"id": "model.access.is_valid.access_token.app_error",
|
||||
"translation": "Invalid access token"
|
||||
},
|
||||
{
|
||||
"id": "model.user_access_token.is_valid.token.app_error",
|
||||
"translation": "Invalid access token"
|
||||
},
|
||||
{
|
||||
"id": "model.user_access_token.is_valid.user_id.app_error",
|
||||
"translation": "Invalid user id"
|
||||
},
|
||||
{
|
||||
"id": "model.user_access_token.is_valid.description.app_error",
|
||||
"translation": "Invalid description, must be 255 or less characters"
|
||||
},
|
||||
{
|
||||
"id": "model.access.is_valid.client_id.app_error",
|
||||
"translation": "Invalid client id"
|
||||
@@ -6055,6 +6131,26 @@
|
||||
"id": "store.sql_team.update_display_name.app_error",
|
||||
"translation": "We couldn't update the team name"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_user_access_token.get_by_user.app_error",
|
||||
"translation": "We couldn't get the user access tokens by user"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_user_access_token.get_by_token.app_error",
|
||||
"translation": "We couldn't get the user access token by token"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_user_access_token.get.app_error",
|
||||
"translation": "We couldn't get the user access token"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_user_access_token.delete.app_error",
|
||||
"translation": "We couldn't delete the user access token"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_user_access_token.save.app_error",
|
||||
"translation": "We couldn't save the user access token"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_user.analytics_get_inactive_users_count.app_error",
|
||||
"translation": "We could not count the inactive users"
|
||||
|
||||
@@ -48,6 +48,7 @@ var PERMISSION_MANAGE_OTHERS_WEBHOOKS *Permission
|
||||
var PERMISSION_MANAGE_OAUTH *Permission
|
||||
var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission
|
||||
var PERMISSION_CREATE_POST *Permission
|
||||
var PERMISSION_CREATE_POST_PUBLIC *Permission
|
||||
var PERMISSION_EDIT_POST *Permission
|
||||
var PERMISSION_EDIT_OTHERS_POSTS *Permission
|
||||
var PERMISSION_DELETE_POST *Permission
|
||||
@@ -59,6 +60,9 @@ var PERMISSION_IMPORT_TEAM *Permission
|
||||
var PERMISSION_VIEW_TEAM *Permission
|
||||
var PERMISSION_LIST_USERS_WITHOUT_TEAM *Permission
|
||||
var PERMISSION_MANAGE_JOBS *Permission
|
||||
var PERMISSION_CREATE_USER_ACCESS_TOKEN *Permission
|
||||
var PERMISSION_READ_USER_ACCESS_TOKEN *Permission
|
||||
var PERMISSION_REVOKE_USER_ACCESS_TOKEN *Permission
|
||||
|
||||
// General permission that encompases all system admin functions
|
||||
// in the future this could be broken up to allow access to some
|
||||
@@ -67,9 +71,12 @@ var PERMISSION_MANAGE_SYSTEM *Permission
|
||||
|
||||
var ROLE_SYSTEM_USER *Role
|
||||
var ROLE_SYSTEM_ADMIN *Role
|
||||
var ROLE_SYSTEM_POST_ALL_PUBLIC *Role
|
||||
var ROLE_SYSTEM_USER_ACCESS_TOKEN *Role
|
||||
|
||||
var ROLE_TEAM_USER *Role
|
||||
var ROLE_TEAM_ADMIN *Role
|
||||
var ROLE_TEAM_POST_ALL_PUBLIC *Role
|
||||
|
||||
var ROLE_CHANNEL_USER *Role
|
||||
var ROLE_CHANNEL_ADMIN *Role
|
||||
@@ -243,6 +250,11 @@ func InitalizePermissions() {
|
||||
"authentication.permissions.create_post.name",
|
||||
"authentication.permissions.create_post.description",
|
||||
}
|
||||
PERMISSION_CREATE_POST_PUBLIC = &Permission{
|
||||
"create_post_public",
|
||||
"authentication.permissions.create_post_public.name",
|
||||
"authentication.permissions.create_post_public.description",
|
||||
}
|
||||
PERMISSION_EDIT_POST = &Permission{
|
||||
"edit_post",
|
||||
"authentication.permissions.edit_post.name",
|
||||
@@ -290,8 +302,23 @@ func InitalizePermissions() {
|
||||
}
|
||||
PERMISSION_LIST_USERS_WITHOUT_TEAM = &Permission{
|
||||
"list_users_without_team",
|
||||
"authentication.permisssions.list_users_without_team.name",
|
||||
"authentication.permisssions.list_users_without_team.description",
|
||||
"authentication.permissions.list_users_without_team.name",
|
||||
"authentication.permissions.list_users_without_team.description",
|
||||
}
|
||||
PERMISSION_CREATE_USER_ACCESS_TOKEN = &Permission{
|
||||
"create_user_access_token",
|
||||
"authentication.permissions.create_user_access_token.name",
|
||||
"authentication.permissions.create_user_access_token.description",
|
||||
}
|
||||
PERMISSION_READ_USER_ACCESS_TOKEN = &Permission{
|
||||
"read_user_access_token",
|
||||
"authentication.permissions.read_user_access_token.name",
|
||||
"authentication.permissions.read_user_access_token.description",
|
||||
}
|
||||
PERMISSION_REVOKE_USER_ACCESS_TOKEN = &Permission{
|
||||
"revoke_user_access_token",
|
||||
"authentication.permissions.revoke_user_access_token.name",
|
||||
"authentication.permissions.revoke_user_access_token.description",
|
||||
}
|
||||
PERMISSION_MANAGE_JOBS = &Permission{
|
||||
"manage_jobs",
|
||||
@@ -348,6 +375,17 @@ func InitalizeRoles() {
|
||||
},
|
||||
}
|
||||
BuiltInRoles[ROLE_TEAM_USER.Id] = ROLE_TEAM_USER
|
||||
|
||||
ROLE_TEAM_POST_ALL_PUBLIC = &Role{
|
||||
"team_post_all_public",
|
||||
"authentication.roles.team_post_all_public.name",
|
||||
"authentication.roles.team_post_all_public.description",
|
||||
[]string{
|
||||
PERMISSION_CREATE_POST_PUBLIC.Id,
|
||||
},
|
||||
}
|
||||
BuiltInRoles[ROLE_TEAM_POST_ALL_PUBLIC.Id] = ROLE_TEAM_POST_ALL_PUBLIC
|
||||
|
||||
ROLE_TEAM_ADMIN = &Role{
|
||||
"team_admin",
|
||||
"authentication.roles.team_admin.name",
|
||||
@@ -378,6 +416,29 @@ func InitalizeRoles() {
|
||||
},
|
||||
}
|
||||
BuiltInRoles[ROLE_SYSTEM_USER.Id] = ROLE_SYSTEM_USER
|
||||
|
||||
ROLE_SYSTEM_POST_ALL_PUBLIC = &Role{
|
||||
"system_post_all_public",
|
||||
"authentication.roles.system_post_all_public.name",
|
||||
"authentication.roles.system_post_all_public.description",
|
||||
[]string{
|
||||
PERMISSION_CREATE_POST_PUBLIC.Id,
|
||||
},
|
||||
}
|
||||
BuiltInRoles[ROLE_SYSTEM_POST_ALL_PUBLIC.Id] = ROLE_SYSTEM_POST_ALL_PUBLIC
|
||||
|
||||
ROLE_SYSTEM_USER_ACCESS_TOKEN = &Role{
|
||||
"system_user_access_token",
|
||||
"authentication.roles.system_user_access_token.name",
|
||||
"authentication.roles.system_user_access_token.description",
|
||||
[]string{
|
||||
PERMISSION_CREATE_USER_ACCESS_TOKEN.Id,
|
||||
PERMISSION_READ_USER_ACCESS_TOKEN.Id,
|
||||
PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
|
||||
},
|
||||
}
|
||||
BuiltInRoles[ROLE_SYSTEM_USER_ACCESS_TOKEN.Id] = ROLE_SYSTEM_USER_ACCESS_TOKEN
|
||||
|
||||
ROLE_SYSTEM_ADMIN = &Role{
|
||||
"system_admin",
|
||||
"authentication.roles.global_admin.name",
|
||||
@@ -412,6 +473,10 @@ func InitalizeRoles() {
|
||||
PERMISSION_ADD_USER_TO_TEAM.Id,
|
||||
PERMISSION_LIST_USERS_WITHOUT_TEAM.Id,
|
||||
PERMISSION_MANAGE_JOBS.Id,
|
||||
PERMISSION_CREATE_POST_PUBLIC.Id,
|
||||
PERMISSION_CREATE_USER_ACCESS_TOKEN.Id,
|
||||
PERMISSION_READ_USER_ACCESS_TOKEN.Id,
|
||||
PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
|
||||
},
|
||||
ROLE_TEAM_USER.Permissions...,
|
||||
),
|
||||
|
||||
@@ -70,6 +70,10 @@ func (c *Client4) GetUserRoute(userId string) string {
|
||||
return fmt.Sprintf(c.GetUsersRoute()+"/%v", userId)
|
||||
}
|
||||
|
||||
func (c *Client4) GetUserAccessTokenRoute(tokenId string) string {
|
||||
return fmt.Sprintf(c.GetUsersRoute()+"/tokens/%v", tokenId)
|
||||
}
|
||||
|
||||
func (c *Client4) GetUserByUsernameRoute(userName string) string {
|
||||
return fmt.Sprintf(c.GetUsersRoute()+"/username/%v", userName)
|
||||
}
|
||||
@@ -957,6 +961,60 @@ func (c *Client4) SetProfileImage(userId string, data []byte) (bool, *Response)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUserAccessToken will generate a user access token that can be used in place
|
||||
// of a session token to access the REST API. Must have the 'create_user_access_token'
|
||||
// permission and if generating for another user, must have the 'edit_other_users'
|
||||
// permission. A non-blank description is required.
|
||||
func (c *Client4) CreateUserAccessToken(userId, description string) (*UserAccessToken, *Response) {
|
||||
requestBody := map[string]string{"description": description}
|
||||
if r, err := c.DoApiPost(c.GetUserRoute(userId)+"/tokens", MapToJson(requestBody)); err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return UserAccessTokenFromJson(r.Body), BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserAccessToken will get a user access token's id, description and the user_id
|
||||
// of the user it is for. The actual token will not be returned. Must have the
|
||||
// 'read_user_access_token' permission and if getting for another user, must have the
|
||||
// 'edit_other_users' permission.
|
||||
func (c *Client4) GetUserAccessToken(tokenId string) (*UserAccessToken, *Response) {
|
||||
if r, err := c.DoApiGet(c.GetUserAccessTokenRoute(tokenId), ""); err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return UserAccessTokenFromJson(r.Body), BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserAccessTokensForUser will get a paged list of user access tokens showing id,
|
||||
// description and user_id for each. The actual tokens will not be returned. Must have
|
||||
// the 'read_user_access_token' permission and if getting for another user, must have the
|
||||
// 'edit_other_users' permission.
|
||||
func (c *Client4) GetUserAccessTokensForUser(userId string, page, perPage int) ([]*UserAccessToken, *Response) {
|
||||
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
|
||||
if r, err := c.DoApiGet(c.GetUserRoute(userId)+"/tokens"+query, ""); err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return UserAccessTokenListFromJson(r.Body), BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
// RevokeUserAccessToken will revoke a user access token by id. Must have the
|
||||
// 'revoke_user_access_token' permission and if revoking for another user, must have the
|
||||
// 'edit_other_users' permission.
|
||||
func (c *Client4) RevokeUserAccessToken(tokenId string) (bool, *Response) {
|
||||
requestBody := map[string]string{"token_id": tokenId}
|
||||
if r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/revoke", MapToJson(requestBody)); err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return CheckStatusOK(r), BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Team Section
|
||||
|
||||
// CreateTeam creates a team in the system based on the provided team struct.
|
||||
|
||||
@@ -154,6 +154,7 @@ type ServiceSettings struct {
|
||||
EnableInsecureOutgoingConnections *bool
|
||||
EnableMultifactorAuthentication *bool
|
||||
EnforceMultifactorAuthentication *bool
|
||||
EnableUserAccessTokens *bool
|
||||
AllowCorsFrom *string
|
||||
SessionLengthWebInDays *int
|
||||
SessionLengthMobileInDays *int
|
||||
@@ -618,6 +619,11 @@ func (o *Config) SetDefaults() {
|
||||
*o.ServiceSettings.EnforceMultifactorAuthentication = false
|
||||
}
|
||||
|
||||
if o.ServiceSettings.EnableUserAccessTokens == nil {
|
||||
o.ServiceSettings.EnableUserAccessTokens = new(bool)
|
||||
*o.ServiceSettings.EnableUserAccessTokens = false
|
||||
}
|
||||
|
||||
if o.PasswordSettings.MinimumLength == nil {
|
||||
o.PasswordSettings.MinimumLength = new(int)
|
||||
*o.PasswordSettings.MinimumLength = PASSWORD_MINIMUM_LENGTH
|
||||
|
||||
@@ -10,13 +10,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
|
||||
SESSION_COOKIE_USER = "MMUSERID"
|
||||
SESSION_CACHE_SIZE = 35000
|
||||
SESSION_PROP_PLATFORM = "platform"
|
||||
SESSION_PROP_OS = "os"
|
||||
SESSION_PROP_BROWSER = "browser"
|
||||
SESSION_ACTIVITY_TIMEOUT = 1000 * 60 * 5 // 5 minutes
|
||||
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
|
||||
SESSION_COOKIE_USER = "MMUSERID"
|
||||
SESSION_CACHE_SIZE = 35000
|
||||
SESSION_PROP_PLATFORM = "platform"
|
||||
SESSION_PROP_OS = "os"
|
||||
SESSION_PROP_BROWSER = "browser"
|
||||
SESSION_PROP_TYPE = "type"
|
||||
SESSION_PROP_USER_ACCESS_TOKEN_ID = "user_access_token_id"
|
||||
SESSION_TYPE_USER_ACCESS_TOKEN = "UserAccessToken"
|
||||
SESSION_ACTIVITY_TIMEOUT = 1000 * 60 * 5 // 5 minutes
|
||||
SESSION_USER_ACCESS_TOKEN_EXPIRY = 100 * 365 // 100 years
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
@@ -58,7 +62,9 @@ func (me *Session) PreSave() {
|
||||
me.Id = NewId()
|
||||
}
|
||||
|
||||
me.Token = NewId()
|
||||
if me.Token == "" {
|
||||
me.Token = NewId()
|
||||
}
|
||||
|
||||
me.CreateAt = GetMillis()
|
||||
me.LastActivityAt = me.CreateAt
|
||||
|
||||
81
model/user_access_token.go
Обычный файл
81
model/user_access_token.go
Обычный файл
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type UserAccessToken struct {
|
||||
Id string `json:"id"`
|
||||
Token string `json:"token,omitempty"`
|
||||
UserId string `json:"user_id"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (t *UserAccessToken) IsValid() *AppError {
|
||||
if len(t.Id) != 26 {
|
||||
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(t.Token) != 26 {
|
||||
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.token.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(t.UserId) != 26 {
|
||||
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(t.Description) > 255 {
|
||||
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.description.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *UserAccessToken) PreSave() {
|
||||
t.Id = NewId()
|
||||
}
|
||||
|
||||
func (t *UserAccessToken) ToJson() string {
|
||||
b, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return ""
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func UserAccessTokenFromJson(data io.Reader) *UserAccessToken {
|
||||
decoder := json.NewDecoder(data)
|
||||
var t UserAccessToken
|
||||
err := decoder.Decode(&t)
|
||||
if err == nil {
|
||||
return &t
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func UserAccessTokenListToJson(t []*UserAccessToken) string {
|
||||
b, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return ""
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func UserAccessTokenListFromJson(data io.Reader) []*UserAccessToken {
|
||||
decoder := json.NewDecoder(data)
|
||||
var t []*UserAccessToken
|
||||
err := decoder.Decode(&t)
|
||||
if err == nil {
|
||||
return t
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
58
model/user_access_token_test.go
Обычный файл
58
model/user_access_token_test.go
Обычный файл
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUserAccessTokenJson(t *testing.T) {
|
||||
a1 := UserAccessToken{}
|
||||
a1.UserId = NewId()
|
||||
a1.Token = NewId()
|
||||
|
||||
json := a1.ToJson()
|
||||
ra1 := UserAccessTokenFromJson(strings.NewReader(json))
|
||||
|
||||
if a1.Token != ra1.Token {
|
||||
t.Fatal("tokens didn't match")
|
||||
}
|
||||
|
||||
tokens := []*UserAccessToken{&a1}
|
||||
json = UserAccessTokenListToJson(tokens)
|
||||
tokens = UserAccessTokenListFromJson(strings.NewReader(json))
|
||||
|
||||
if tokens[0].Token != a1.Token {
|
||||
t.Fatal("tokens didn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAccessTokenIsValid(t *testing.T) {
|
||||
ad := UserAccessToken{}
|
||||
|
||||
if err := ad.IsValid(); err == nil || err.Id != "model.user_access_token.is_valid.id.app_error" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ad.Id = NewRandomString(26)
|
||||
if err := ad.IsValid(); err == nil || err.Id != "model.user_access_token.is_valid.token.app_error" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ad.Token = NewRandomString(26)
|
||||
if err := ad.IsValid(); err == nil || err.Id != "model.user_access_token.is_valid.user_id.app_error" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ad.UserId = NewRandomString(26)
|
||||
if err := ad.IsValid(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ad.Description = NewRandomString(256)
|
||||
if err := ad.IsValid(); err == nil || err.Id != "model.user_access_token.is_valid.description.app_error" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -139,6 +139,10 @@ func (s *LayeredStore) Job() JobStore {
|
||||
return s.DatabaseLayer.Job()
|
||||
}
|
||||
|
||||
func (s *LayeredStore) UserAccessToken() UserAccessTokenStore {
|
||||
return s.DatabaseLayer.UserAccessToken()
|
||||
}
|
||||
|
||||
func (s *LayeredStore) MarkSystemRanUnitTests() {
|
||||
s.DatabaseLayer.MarkSystemRanUnitTests()
|
||||
}
|
||||
|
||||
@@ -80,4 +80,5 @@ type SqlStore interface {
|
||||
FileInfo() FileInfoStore
|
||||
Reaction() ReactionStore
|
||||
Job() JobStore
|
||||
UserAccessToken() UserAccessTokenStore
|
||||
}
|
||||
|
||||
@@ -58,26 +58,27 @@ const (
|
||||
)
|
||||
|
||||
type SqlSupplierOldStores struct {
|
||||
team TeamStore
|
||||
channel ChannelStore
|
||||
post PostStore
|
||||
user UserStore
|
||||
audit AuditStore
|
||||
cluster ClusterDiscoveryStore
|
||||
compliance ComplianceStore
|
||||
session SessionStore
|
||||
oauth OAuthStore
|
||||
system SystemStore
|
||||
webhook WebhookStore
|
||||
command CommandStore
|
||||
preference PreferenceStore
|
||||
license LicenseStore
|
||||
token TokenStore
|
||||
emoji EmojiStore
|
||||
status StatusStore
|
||||
fileInfo FileInfoStore
|
||||
reaction ReactionStore
|
||||
job JobStore
|
||||
team TeamStore
|
||||
channel ChannelStore
|
||||
post PostStore
|
||||
user UserStore
|
||||
audit AuditStore
|
||||
cluster ClusterDiscoveryStore
|
||||
compliance ComplianceStore
|
||||
session SessionStore
|
||||
oauth OAuthStore
|
||||
system SystemStore
|
||||
webhook WebhookStore
|
||||
command CommandStore
|
||||
preference PreferenceStore
|
||||
license LicenseStore
|
||||
token TokenStore
|
||||
emoji EmojiStore
|
||||
status StatusStore
|
||||
fileInfo FileInfoStore
|
||||
reaction ReactionStore
|
||||
job JobStore
|
||||
userAccessToken UserAccessTokenStore
|
||||
}
|
||||
|
||||
type SqlSupplier struct {
|
||||
@@ -117,6 +118,7 @@ func NewSqlSupplier() *SqlSupplier {
|
||||
supplier.oldStores.status = NewSqlStatusStore(supplier)
|
||||
supplier.oldStores.fileInfo = NewSqlFileInfoStore(supplier)
|
||||
supplier.oldStores.job = NewSqlJobStore(supplier)
|
||||
supplier.oldStores.userAccessToken = NewSqlUserAccessTokenStore(supplier)
|
||||
|
||||
initSqlSupplierReactions(supplier)
|
||||
|
||||
@@ -147,6 +149,7 @@ func NewSqlSupplier() *SqlSupplier {
|
||||
supplier.oldStores.status.(*SqlStatusStore).CreateIndexesIfNotExists()
|
||||
supplier.oldStores.fileInfo.(*SqlFileInfoStore).CreateIndexesIfNotExists()
|
||||
supplier.oldStores.job.(*SqlJobStore).CreateIndexesIfNotExists()
|
||||
supplier.oldStores.userAccessToken.(*SqlUserAccessTokenStore).CreateIndexesIfNotExists()
|
||||
|
||||
supplier.oldStores.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
|
||||
|
||||
@@ -760,6 +763,10 @@ func (ss *SqlSupplier) Job() JobStore {
|
||||
return ss.oldStores.job
|
||||
}
|
||||
|
||||
func (ss *SqlSupplier) UserAccessToken() UserAccessTokenStore {
|
||||
return ss.oldStores.userAccessToken
|
||||
}
|
||||
|
||||
func (ss *SqlSupplier) DropAllTables() {
|
||||
ss.master.TruncateTables()
|
||||
}
|
||||
|
||||
262
store/sql_user_access_token_store.go
Обычный файл
262
store/sql_user_access_token_store.go
Обычный файл
@@ -0,0 +1,262 @@
|
||||
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"net/http"
|
||||
|
||||
"github.com/mattermost/gorp"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/utils"
|
||||
)
|
||||
|
||||
type SqlUserAccessTokenStore struct {
|
||||
SqlStore
|
||||
}
|
||||
|
||||
func NewSqlUserAccessTokenStore(sqlStore SqlStore) UserAccessTokenStore {
|
||||
s := &SqlUserAccessTokenStore{sqlStore}
|
||||
|
||||
for _, db := range sqlStore.GetAllConns() {
|
||||
table := db.AddTableWithName(model.UserAccessToken{}, "UserAccessTokens").SetKeys(false, "Id")
|
||||
table.ColMap("Id").SetMaxSize(26)
|
||||
table.ColMap("Token").SetMaxSize(26).SetUnique(true)
|
||||
table.ColMap("UserId").SetMaxSize(26)
|
||||
table.ColMap("Description").SetMaxSize(512)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s SqlUserAccessTokenStore) CreateIndexesIfNotExists() {
|
||||
s.CreateIndexIfNotExists("idx_user_access_tokens_token", "UserAccessTokens", "Token")
|
||||
s.CreateIndexIfNotExists("idx_user_access_tokens_user_id", "UserAccessTokens", "UserId")
|
||||
}
|
||||
|
||||
func (s SqlUserAccessTokenStore) Save(token *model.UserAccessToken) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
token.PreSave()
|
||||
|
||||
if result.Err = token.IsValid(); result.Err != nil {
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.GetMaster().Insert(token); err != nil {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.Save", "store.sql_user_access_token.save.app_error", nil, "", http.StatusInternalServerError)
|
||||
} else {
|
||||
result.Data = token
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlUserAccessTokenStore) Delete(tokenId string) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
transaction, err := s.GetMaster().Begin()
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.Delete", "store.sql_user_access_token.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
if extrasResult := s.deleteSessionsAndTokensById(transaction, tokenId); extrasResult.Err != nil {
|
||||
result = extrasResult
|
||||
}
|
||||
|
||||
if result.Err == nil {
|
||||
if err := transaction.Commit(); err != nil {
|
||||
// don't need to rollback here since the transaction is already closed
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.Delete", "store.sql_user_access_token.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
if err := transaction.Rollback(); err != nil {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.Delete", "store.sql_user_access_token.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlUserAccessTokenStore) deleteSessionsAndTokensById(transaction *gorp.Transaction, tokenId string) StoreResult {
|
||||
result := StoreResult{}
|
||||
|
||||
query := ""
|
||||
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
|
||||
query = "DELETE FROM Sessions s USING UserAccessTokens o WHERE o.Token = s.Token AND o.Id = :Id"
|
||||
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
|
||||
query = "DELETE s.* FROM Sessions s INNER JOIN UserAccessTokens o ON o.Token = s.Token WHERE o.Id = :Id"
|
||||
}
|
||||
|
||||
if _, err := transaction.Exec(query, map[string]interface{}{"Id": tokenId}); err != nil {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.deleteSessionsById", "store.sql_user_access_token.delete.app_error", nil, "id="+tokenId+", err="+err.Error(), http.StatusInternalServerError)
|
||||
return result
|
||||
}
|
||||
|
||||
return s.deleteTokensById(transaction, tokenId)
|
||||
}
|
||||
|
||||
func (s SqlUserAccessTokenStore) deleteTokensById(transaction *gorp.Transaction, tokenId string) StoreResult {
|
||||
result := StoreResult{}
|
||||
|
||||
if _, err := transaction.Exec("DELETE FROM UserAccessTokens WHERE Id = :Id", map[string]interface{}{"Id": tokenId}); err != nil {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.deleteTokensById", "store.sql_user_access_token.delete.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s SqlUserAccessTokenStore) DeleteAllForUser(userId string) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
transaction, err := s.GetMaster().Begin()
|
||||
if err != nil {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.DeleteAllForUser", "store.sql_user_access_token.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
} else {
|
||||
if extrasResult := s.deleteSessionsandTokensByUser(transaction, userId); extrasResult.Err != nil {
|
||||
result = extrasResult
|
||||
}
|
||||
|
||||
if result.Err == nil {
|
||||
if err := transaction.Commit(); err != nil {
|
||||
// don't need to rollback here since the transaction is already closed
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.DeleteAllForUser", "store.sql_user_access_token.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
if err := transaction.Rollback(); err != nil {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.DeleteAllForUser", "store.sql_user_access_token.delete.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlUserAccessTokenStore) deleteSessionsandTokensByUser(transaction *gorp.Transaction, userId string) StoreResult {
|
||||
result := StoreResult{}
|
||||
|
||||
query := ""
|
||||
if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_POSTGRES {
|
||||
query = "DELETE FROM Sessions s USING UserAccessTokens o WHERE o.Token = s.Token AND o.UserId = :UserId"
|
||||
} else if utils.Cfg.SqlSettings.DriverName == model.DATABASE_DRIVER_MYSQL {
|
||||
query = "DELETE s.* FROM Sessions s INNER JOIN UserAccessTokens o ON o.Token = s.Token WHERE o.UserId = :UserId"
|
||||
}
|
||||
|
||||
if _, err := transaction.Exec(query, map[string]interface{}{"UserId": userId}); err != nil {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.deleteSessionsByUser", "store.sql_user_access_token.delete.app_error", nil, "user_id="+userId+", err="+err.Error(), http.StatusInternalServerError)
|
||||
return result
|
||||
}
|
||||
|
||||
return s.deleteTokensByUser(transaction, userId)
|
||||
}
|
||||
|
||||
func (s SqlUserAccessTokenStore) deleteTokensByUser(transaction *gorp.Transaction, userId string) StoreResult {
|
||||
result := StoreResult{}
|
||||
|
||||
if _, err := transaction.Exec("DELETE FROM UserAccessTokens WHERE UserId = :UserId", map[string]interface{}{"UserId": userId}); err != nil {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.deleteTokensByUser", "store.sql_user_access_token.delete.app_error", nil, "", http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (s SqlUserAccessTokenStore) Get(tokenId string) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
token := model.UserAccessToken{}
|
||||
|
||||
if err := s.GetReplica().SelectOne(&token, "SELECT * FROM UserAccessTokens WHERE Id = :Id", map[string]interface{}{"Id": tokenId}); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.Get", "store.sql_user_access_token.get.app_error", nil, err.Error(), http.StatusNotFound)
|
||||
} else {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.Get", "store.sql_user_access_token.get.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
result.Data = &token
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlUserAccessTokenStore) GetByToken(tokenString string) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
token := model.UserAccessToken{}
|
||||
|
||||
if err := s.GetReplica().SelectOne(&token, "SELECT * FROM UserAccessTokens WHERE Token = :Token", map[string]interface{}{"Token": tokenString}); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.GetByToken", "store.sql_user_access_token.get_by_token.app_error", nil, err.Error(), http.StatusNotFound)
|
||||
} else {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.GetByToken", "store.sql_user_access_token.get_by_token.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
result.Data = &token
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (s SqlUserAccessTokenStore) GetByUser(userId string, offset, limit int) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel, 1)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
tokens := []*model.UserAccessToken{}
|
||||
|
||||
if _, err := s.GetReplica().Select(&tokens, "SELECT * FROM UserAccessTokens WHERE UserId = :UserId LIMIT :Limit OFFSET :Offset", map[string]interface{}{"UserId": userId, "Offset": offset, "Limit": limit}); err != nil {
|
||||
result.Err = model.NewAppError("SqlUserAccessTokenStore.GetByUser", "store.sql_user_access_token.get_by_user.app_error", nil, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
|
||||
result.Data = tokens
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
86
store/sql_user_access_token_store_test.go
Обычный файл
86
store/sql_user_access_token_store_test.go
Обычный файл
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/mattermost/platform/model"
|
||||
)
|
||||
|
||||
func TestUserAccessTokenSaveGetDelete(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
uat := &model.UserAccessToken{
|
||||
Token: model.NewId(),
|
||||
UserId: model.NewId(),
|
||||
Description: "testtoken",
|
||||
}
|
||||
|
||||
s1 := model.Session{}
|
||||
s1.UserId = uat.UserId
|
||||
s1.Token = uat.Token
|
||||
|
||||
Must(store.Session().Save(&s1))
|
||||
|
||||
if result := <-store.UserAccessToken().Save(uat); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
if result := <-store.UserAccessToken().Get(uat.Id); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if received := result.Data.(*model.UserAccessToken); received.Token != uat.Token {
|
||||
t.Fatal("received incorrect token after save")
|
||||
}
|
||||
|
||||
if result := <-store.UserAccessToken().GetByToken(uat.Token); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if received := result.Data.(*model.UserAccessToken); received.Token != uat.Token {
|
||||
t.Fatal("received incorrect token after save")
|
||||
}
|
||||
|
||||
if result := <-store.UserAccessToken().GetByToken("notarealtoken"); result.Err == nil {
|
||||
t.Fatal("should have failed on bad token")
|
||||
}
|
||||
|
||||
if result := <-store.UserAccessToken().GetByUser(uat.UserId, 0, 100); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
} else if received := result.Data.([]*model.UserAccessToken); len(received) != 1 {
|
||||
t.Fatal("received incorrect number of tokens after save")
|
||||
}
|
||||
|
||||
if result := <-store.UserAccessToken().Delete(uat.Id); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
if err := (<-store.Session().Get(s1.Token)).Err; err == nil {
|
||||
t.Fatal("should error - session should be deleted")
|
||||
}
|
||||
|
||||
if err := (<-store.UserAccessToken().GetByToken(s1.Token)).Err; err == nil {
|
||||
t.Fatal("should error - access token should be deleted")
|
||||
}
|
||||
|
||||
s2 := model.Session{}
|
||||
s2.UserId = uat.UserId
|
||||
s2.Token = uat.Token
|
||||
|
||||
Must(store.Session().Save(&s2))
|
||||
|
||||
if result := <-store.UserAccessToken().Save(uat); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
if result := <-store.UserAccessToken().DeleteAllForUser(uat.UserId); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
}
|
||||
|
||||
if err := (<-store.Session().Get(s2.Token)).Err; err == nil {
|
||||
t.Fatal("should error - session should be deleted")
|
||||
}
|
||||
|
||||
if err := (<-store.UserAccessToken().GetByToken(s2.Token)).Err; err == nil {
|
||||
t.Fatal("should error - access token should be deleted")
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,7 @@ type Store interface {
|
||||
FileInfo() FileInfoStore
|
||||
Reaction() ReactionStore
|
||||
Job() JobStore
|
||||
UserAccessToken() UserAccessTokenStore
|
||||
MarkSystemRanUnitTests()
|
||||
Close()
|
||||
DropAllTables()
|
||||
@@ -398,3 +399,12 @@ type JobStore interface {
|
||||
GetAllByStatus(status string) StoreChannel
|
||||
Delete(id string) StoreChannel
|
||||
}
|
||||
|
||||
type UserAccessTokenStore interface {
|
||||
Save(token *model.UserAccessToken) StoreChannel
|
||||
Delete(tokenId string) StoreChannel
|
||||
DeleteAllForUser(userId string) StoreChannel
|
||||
Get(tokenId string) StoreChannel
|
||||
GetByToken(tokenString string) StoreChannel
|
||||
GetByUser(userId string, page, perPage int) StoreChannel
|
||||
}
|
||||
|
||||
@@ -421,6 +421,7 @@ func getClientConfig(c *model.Config) map[string]string {
|
||||
props["EnableOnlyAdminIntegrations"] = strconv.FormatBool(*c.ServiceSettings.EnableOnlyAdminIntegrations)
|
||||
props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride)
|
||||
props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride)
|
||||
props["EnableUserAccessTokens"] = strconv.FormatBool(*c.ServiceSettings.EnableUserAccessTokens)
|
||||
props["EnableLinkPreviews"] = strconv.FormatBool(*c.ServiceSettings.EnableLinkPreviews)
|
||||
props["EnableTesting"] = strconv.FormatBool(c.ServiceSettings.EnableTesting)
|
||||
props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper)
|
||||
|
||||
Ссылка в новой задаче
Block a user