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
Этот коммит содержится в:
Joram Wilander
2017-07-31 12:59:32 -04:00
коммит произвёл GitHub
родитель ed62660e96
Коммит 59992ae4a4
25 изменённых файлов: 1378 добавлений и 44 удалений

Просмотреть файл

@@ -308,9 +308,13 @@ func (c *Context) LogDebug(err *model.AppError) {
} }
func (c *Context) UserRequired() { 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 { if len(c.Session.UserId) == 0 {
c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "UserRequired") c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized)
c.Err.StatusCode = http.StatusUnauthorized
return return
} }
} }

Просмотреть файл

@@ -51,7 +51,17 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
post.UserId = c.Session.UserId 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) c.SetPermissionError(model.PERMISSION_CREATE_POST)
return return
} }

Просмотреть файл

@@ -239,6 +239,11 @@ func (c *Context) IsSystemAdmin() bool {
} }
func (c *Context) SessionRequired() { 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 { if len(c.Session.UserId) == 0 {
c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized) c.Err = model.NewAppError("", "api.context.session_expired.app_error", nil, "UserRequired", http.StatusUnauthorized)
return return
@@ -361,6 +366,17 @@ func (c *Context) RequireInviteId() *Context {
return c 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 { func (c *Context) RequireChannelId() *Context {
if c.Err != nil { if c.Err != nil {
return c return c

Просмотреть файл

@@ -20,6 +20,7 @@ type ApiParams struct {
UserId string UserId string
TeamId string TeamId string
InviteId string InviteId string
TokenId string
ChannelId string ChannelId string
PostId string PostId string
FileId string FileId string
@@ -60,6 +61,10 @@ func ApiParamsFromRequest(r *http.Request) *ApiParams {
params.InviteId = val params.InviteId = val
} }
if val, ok := props["token_id"]; ok {
params.TokenId = val
}
if val, ok := props["channel_id"]; ok { if val, ok := props["channel_id"]; ok {
params.ChannelId = val params.ChannelId = val
} }

Просмотреть файл

@@ -40,7 +40,17 @@ func createPost(c *Context, w http.ResponseWriter, r *http.Request) {
post.UserId = c.Session.UserId 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) c.SetPermissionError(model.PERMISSION_CREATE_POST)
return return
} }

Просмотреть файл

@@ -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) 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) { func TestUpdatePost(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin() th := Setup().InitBasic().InitSystemAdmin()
defer TearDown() defer TearDown()

Просмотреть файл

@@ -55,6 +55,11 @@ func InitUser() {
BaseRoutes.User.Handle("/sessions/revoke", ApiSessionRequired(revokeSession)).Methods("POST") BaseRoutes.User.Handle("/sessions/revoke", ApiSessionRequired(revokeSession)).Methods("POST")
BaseRoutes.Users.Handle("/sessions/device", ApiSessionRequired(attachDeviceId)).Methods("PUT") BaseRoutes.Users.Handle("/sessions/device", ApiSessionRequired(attachDeviceId)).Methods("PUT")
BaseRoutes.User.Handle("/audits", ApiSessionRequired(getUserAudits)).Methods("GET") 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) { 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") c.LogAudit("success")
w.Write([]byte(model.MapToJson(map[string]string{"follow_link": link}))) 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) _, resp = Client.SwitchAccountType(sr)
CheckUnauthorizedStatus(t, resp) 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)
}

Просмотреть файл

@@ -16,6 +16,8 @@ import (
var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE) var sessionCache *utils.Cache = utils.NewLru(model.SESSION_CACHE_SIZE)
func CreateSession(session *model.Session) (*model.Session, *model.AppError) { func CreateSession(session *model.Session) (*model.Session, *model.AppError) {
session.Token = ""
if result := <-Srv.Store.Session().Save(session); result.Err != nil { if result := <-Srv.Store.Session().Save(session); result.Err != nil {
return nil, result.Err return nil, result.Err
} else { } else {
@@ -43,22 +45,31 @@ func GetSession(token string) (*model.Session, *model.AppError) {
} }
if session == nil { if session == nil {
if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err != 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 {
session = sessionResult.Data.(*model.Session) session = sessionResult.Data.(*model.Session)
if session == nil || session.IsExpired() || session.Token != token { if session != nil {
return nil, model.NewLocAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": ""}, "") if session.Token != token {
} else { return nil, model.NewAppError("GetSession", "api.context.invalid_token.error", map[string]interface{}{"Token": token, "Error": ""}, "", http.StatusUnauthorized)
AddSessionToCache(session) }
return session, nil
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() { 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 return session, nil
@@ -200,3 +211,104 @@ func UpdateLastActivityAtIfNeeded(session model.Session) {
session.LastActivityAt = now session.LastActivityAt = now
AddSessionToCache(&session) 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 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 { if result := <-Srv.Store.OAuth().PermanentDeleteAuthDataByUser(user.Id); result.Err != nil {
return result.Err return result.Err
} }

Просмотреть файл

@@ -29,6 +29,7 @@
"EnableInsecureOutgoingConnections": false, "EnableInsecureOutgoingConnections": false,
"EnableMultifactorAuthentication": false, "EnableMultifactorAuthentication": false,
"EnforceMultifactorAuthentication": false, "EnforceMultifactorAuthentication": false,
"EnableUserAccessTokens": false,
"AllowCorsFrom": "", "AllowCorsFrom": "",
"SessionLengthWebInDays": 30, "SessionLengthWebInDays": 30,
"SessionLengthMobileInDays": 30, "SessionLengthMobileInDays": 30,

Просмотреть файл

@@ -3091,6 +3091,14 @@
"id": "api.websocket_handler.invalid_param.app_error", "id": "api.websocket_handler.invalid_param.app_error",
"translation": "Invalid {{.Name}} parameter" "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", "id": "app.channel.create_channel.no_team_id.app_error",
"translation": "Must specify the team ID to create a channel" "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", "id": "app.import.validate_user_teams_import_data.team_name_missing.error",
"translation": "Team name missing from User's Team Membership." "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", "id": "authentication.permissions.create_group_channel.description",
"translation": "Ability to create new group message channels" "translation": "Ability to create new group message channels"
@@ -4015,6 +4079,18 @@
"id": "model.access.is_valid.access_token.app_error", "id": "model.access.is_valid.access_token.app_error",
"translation": "Invalid access token" "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", "id": "model.access.is_valid.client_id.app_error",
"translation": "Invalid client id" "translation": "Invalid client id"
@@ -6055,6 +6131,26 @@
"id": "store.sql_team.update_display_name.app_error", "id": "store.sql_team.update_display_name.app_error",
"translation": "We couldn't update the team name" "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", "id": "store.sql_user.analytics_get_inactive_users_count.app_error",
"translation": "We could not count the inactive users" "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_OAUTH *Permission
var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission
var PERMISSION_CREATE_POST *Permission var PERMISSION_CREATE_POST *Permission
var PERMISSION_CREATE_POST_PUBLIC *Permission
var PERMISSION_EDIT_POST *Permission var PERMISSION_EDIT_POST *Permission
var PERMISSION_EDIT_OTHERS_POSTS *Permission var PERMISSION_EDIT_OTHERS_POSTS *Permission
var PERMISSION_DELETE_POST *Permission var PERMISSION_DELETE_POST *Permission
@@ -59,6 +60,9 @@ var PERMISSION_IMPORT_TEAM *Permission
var PERMISSION_VIEW_TEAM *Permission var PERMISSION_VIEW_TEAM *Permission
var PERMISSION_LIST_USERS_WITHOUT_TEAM *Permission var PERMISSION_LIST_USERS_WITHOUT_TEAM *Permission
var PERMISSION_MANAGE_JOBS *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 // General permission that encompases all system admin functions
// in the future this could be broken up to allow access to some // 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_USER *Role
var ROLE_SYSTEM_ADMIN *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_USER *Role
var ROLE_TEAM_ADMIN *Role var ROLE_TEAM_ADMIN *Role
var ROLE_TEAM_POST_ALL_PUBLIC *Role
var ROLE_CHANNEL_USER *Role var ROLE_CHANNEL_USER *Role
var ROLE_CHANNEL_ADMIN *Role var ROLE_CHANNEL_ADMIN *Role
@@ -243,6 +250,11 @@ func InitalizePermissions() {
"authentication.permissions.create_post.name", "authentication.permissions.create_post.name",
"authentication.permissions.create_post.description", "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{ PERMISSION_EDIT_POST = &Permission{
"edit_post", "edit_post",
"authentication.permissions.edit_post.name", "authentication.permissions.edit_post.name",
@@ -290,8 +302,23 @@ func InitalizePermissions() {
} }
PERMISSION_LIST_USERS_WITHOUT_TEAM = &Permission{ PERMISSION_LIST_USERS_WITHOUT_TEAM = &Permission{
"list_users_without_team", "list_users_without_team",
"authentication.permisssions.list_users_without_team.name", "authentication.permissions.list_users_without_team.name",
"authentication.permisssions.list_users_without_team.description", "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{ PERMISSION_MANAGE_JOBS = &Permission{
"manage_jobs", "manage_jobs",
@@ -348,6 +375,17 @@ func InitalizeRoles() {
}, },
} }
BuiltInRoles[ROLE_TEAM_USER.Id] = ROLE_TEAM_USER 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{ ROLE_TEAM_ADMIN = &Role{
"team_admin", "team_admin",
"authentication.roles.team_admin.name", "authentication.roles.team_admin.name",
@@ -378,6 +416,29 @@ func InitalizeRoles() {
}, },
} }
BuiltInRoles[ROLE_SYSTEM_USER.Id] = ROLE_SYSTEM_USER 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{ ROLE_SYSTEM_ADMIN = &Role{
"system_admin", "system_admin",
"authentication.roles.global_admin.name", "authentication.roles.global_admin.name",
@@ -412,6 +473,10 @@ func InitalizeRoles() {
PERMISSION_ADD_USER_TO_TEAM.Id, PERMISSION_ADD_USER_TO_TEAM.Id,
PERMISSION_LIST_USERS_WITHOUT_TEAM.Id, PERMISSION_LIST_USERS_WITHOUT_TEAM.Id,
PERMISSION_MANAGE_JOBS.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..., ROLE_TEAM_USER.Permissions...,
), ),

Просмотреть файл

@@ -70,6 +70,10 @@ func (c *Client4) GetUserRoute(userId string) string {
return fmt.Sprintf(c.GetUsersRoute()+"/%v", userId) 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 { func (c *Client4) GetUserByUsernameRoute(userName string) string {
return fmt.Sprintf(c.GetUsersRoute()+"/username/%v", userName) 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 // Team Section
// CreateTeam creates a team in the system based on the provided team struct. // CreateTeam creates a team in the system based on the provided team struct.

Просмотреть файл

@@ -154,6 +154,7 @@ type ServiceSettings struct {
EnableInsecureOutgoingConnections *bool EnableInsecureOutgoingConnections *bool
EnableMultifactorAuthentication *bool EnableMultifactorAuthentication *bool
EnforceMultifactorAuthentication *bool EnforceMultifactorAuthentication *bool
EnableUserAccessTokens *bool
AllowCorsFrom *string AllowCorsFrom *string
SessionLengthWebInDays *int SessionLengthWebInDays *int
SessionLengthMobileInDays *int SessionLengthMobileInDays *int
@@ -618,6 +619,11 @@ func (o *Config) SetDefaults() {
*o.ServiceSettings.EnforceMultifactorAuthentication = false *o.ServiceSettings.EnforceMultifactorAuthentication = false
} }
if o.ServiceSettings.EnableUserAccessTokens == nil {
o.ServiceSettings.EnableUserAccessTokens = new(bool)
*o.ServiceSettings.EnableUserAccessTokens = false
}
if o.PasswordSettings.MinimumLength == nil { if o.PasswordSettings.MinimumLength == nil {
o.PasswordSettings.MinimumLength = new(int) o.PasswordSettings.MinimumLength = new(int)
*o.PasswordSettings.MinimumLength = PASSWORD_MINIMUM_LENGTH *o.PasswordSettings.MinimumLength = PASSWORD_MINIMUM_LENGTH

Просмотреть файл

@@ -10,13 +10,17 @@ import (
) )
const ( const (
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN" SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
SESSION_COOKIE_USER = "MMUSERID" SESSION_COOKIE_USER = "MMUSERID"
SESSION_CACHE_SIZE = 35000 SESSION_CACHE_SIZE = 35000
SESSION_PROP_PLATFORM = "platform" SESSION_PROP_PLATFORM = "platform"
SESSION_PROP_OS = "os" SESSION_PROP_OS = "os"
SESSION_PROP_BROWSER = "browser" SESSION_PROP_BROWSER = "browser"
SESSION_ACTIVITY_TIMEOUT = 1000 * 60 * 5 // 5 minutes 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 { type Session struct {
@@ -58,7 +62,9 @@ func (me *Session) PreSave() {
me.Id = NewId() me.Id = NewId()
} }
me.Token = NewId() if me.Token == "" {
me.Token = NewId()
}
me.CreateAt = GetMillis() me.CreateAt = GetMillis()
me.LastActivityAt = me.CreateAt me.LastActivityAt = me.CreateAt

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 Обычный файл
Просмотреть файл

@@ -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() return s.DatabaseLayer.Job()
} }
func (s *LayeredStore) UserAccessToken() UserAccessTokenStore {
return s.DatabaseLayer.UserAccessToken()
}
func (s *LayeredStore) MarkSystemRanUnitTests() { func (s *LayeredStore) MarkSystemRanUnitTests() {
s.DatabaseLayer.MarkSystemRanUnitTests() s.DatabaseLayer.MarkSystemRanUnitTests()
} }

Просмотреть файл

@@ -80,4 +80,5 @@ type SqlStore interface {
FileInfo() FileInfoStore FileInfo() FileInfoStore
Reaction() ReactionStore Reaction() ReactionStore
Job() JobStore Job() JobStore
UserAccessToken() UserAccessTokenStore
} }

Просмотреть файл

@@ -58,26 +58,27 @@ const (
) )
type SqlSupplierOldStores struct { type SqlSupplierOldStores struct {
team TeamStore team TeamStore
channel ChannelStore channel ChannelStore
post PostStore post PostStore
user UserStore user UserStore
audit AuditStore audit AuditStore
cluster ClusterDiscoveryStore cluster ClusterDiscoveryStore
compliance ComplianceStore compliance ComplianceStore
session SessionStore session SessionStore
oauth OAuthStore oauth OAuthStore
system SystemStore system SystemStore
webhook WebhookStore webhook WebhookStore
command CommandStore command CommandStore
preference PreferenceStore preference PreferenceStore
license LicenseStore license LicenseStore
token TokenStore token TokenStore
emoji EmojiStore emoji EmojiStore
status StatusStore status StatusStore
fileInfo FileInfoStore fileInfo FileInfoStore
reaction ReactionStore reaction ReactionStore
job JobStore job JobStore
userAccessToken UserAccessTokenStore
} }
type SqlSupplier struct { type SqlSupplier struct {
@@ -117,6 +118,7 @@ func NewSqlSupplier() *SqlSupplier {
supplier.oldStores.status = NewSqlStatusStore(supplier) supplier.oldStores.status = NewSqlStatusStore(supplier)
supplier.oldStores.fileInfo = NewSqlFileInfoStore(supplier) supplier.oldStores.fileInfo = NewSqlFileInfoStore(supplier)
supplier.oldStores.job = NewSqlJobStore(supplier) supplier.oldStores.job = NewSqlJobStore(supplier)
supplier.oldStores.userAccessToken = NewSqlUserAccessTokenStore(supplier)
initSqlSupplierReactions(supplier) initSqlSupplierReactions(supplier)
@@ -147,6 +149,7 @@ func NewSqlSupplier() *SqlSupplier {
supplier.oldStores.status.(*SqlStatusStore).CreateIndexesIfNotExists() supplier.oldStores.status.(*SqlStatusStore).CreateIndexesIfNotExists()
supplier.oldStores.fileInfo.(*SqlFileInfoStore).CreateIndexesIfNotExists() supplier.oldStores.fileInfo.(*SqlFileInfoStore).CreateIndexesIfNotExists()
supplier.oldStores.job.(*SqlJobStore).CreateIndexesIfNotExists() supplier.oldStores.job.(*SqlJobStore).CreateIndexesIfNotExists()
supplier.oldStores.userAccessToken.(*SqlUserAccessTokenStore).CreateIndexesIfNotExists()
supplier.oldStores.preference.(*SqlPreferenceStore).DeleteUnusedFeatures() supplier.oldStores.preference.(*SqlPreferenceStore).DeleteUnusedFeatures()
@@ -760,6 +763,10 @@ func (ss *SqlSupplier) Job() JobStore {
return ss.oldStores.job return ss.oldStores.job
} }
func (ss *SqlSupplier) UserAccessToken() UserAccessTokenStore {
return ss.oldStores.userAccessToken
}
func (ss *SqlSupplier) DropAllTables() { func (ss *SqlSupplier) DropAllTables() {
ss.master.TruncateTables() ss.master.TruncateTables()
} }

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 Обычный файл
Просмотреть файл

@@ -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 FileInfo() FileInfoStore
Reaction() ReactionStore Reaction() ReactionStore
Job() JobStore Job() JobStore
UserAccessToken() UserAccessTokenStore
MarkSystemRanUnitTests() MarkSystemRanUnitTests()
Close() Close()
DropAllTables() DropAllTables()
@@ -398,3 +399,12 @@ type JobStore interface {
GetAllByStatus(status string) StoreChannel GetAllByStatus(status string) StoreChannel
Delete(id 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["EnableOnlyAdminIntegrations"] = strconv.FormatBool(*c.ServiceSettings.EnableOnlyAdminIntegrations)
props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride) props["EnablePostUsernameOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostUsernameOverride)
props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride) props["EnablePostIconOverride"] = strconv.FormatBool(c.ServiceSettings.EnablePostIconOverride)
props["EnableUserAccessTokens"] = strconv.FormatBool(*c.ServiceSettings.EnableUserAccessTokens)
props["EnableLinkPreviews"] = strconv.FormatBool(*c.ServiceSettings.EnableLinkPreviews) props["EnableLinkPreviews"] = strconv.FormatBool(*c.ServiceSettings.EnableLinkPreviews)
props["EnableTesting"] = strconv.FormatBool(c.ServiceSettings.EnableTesting) props["EnableTesting"] = strconv.FormatBool(c.ServiceSettings.EnableTesting)
props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper) props["EnableDeveloper"] = strconv.FormatBool(*c.ServiceSettings.EnableDeveloper)