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() {
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
}
}

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

@@ -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
}

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

@@ -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()

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

@@ -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)
}

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

@@ -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,

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

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

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

@@ -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
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)