From 59992ae4a4638006ec1489dd834151b258c1728c Mon Sep 17 00:00:00 2001 From: Joram Wilander Date: Mon, 31 Jul 2017 12:59:32 -0400 Subject: PLT-6763 Implement user access tokens and new roles (server-side) (#6972) * Implement user access tokens and new roles * Update config.json * Add public post permission to apiv3 * Remove old comment * Fix model unit test * Updates to store per feedback * Updates per feedback from CS --- model/authorization.go | 69 ++++++++++++++++++++++++++++++++++- model/client4.go | 58 +++++++++++++++++++++++++++++ model/config.go | 6 +++ model/session.go | 22 +++++++---- model/user_access_token.go | 81 +++++++++++++++++++++++++++++++++++++++++ model/user_access_token_test.go | 58 +++++++++++++++++++++++++++++ 6 files changed, 284 insertions(+), 10 deletions(-) create mode 100644 model/user_access_token.go create mode 100644 model/user_access_token_test.go (limited to 'model') diff --git a/model/authorization.go b/model/authorization.go index 880d25e27..cf7e2b481 100644 --- a/model/authorization.go +++ b/model/authorization.go @@ -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..., ), diff --git a/model/client4.go b/model/client4.go index 6f5eb03c6..2daca4dc9 100644 --- a/model/client4.go +++ b/model/client4.go @@ -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. diff --git a/model/config.go b/model/config.go index 475e512f9..0755f514f 100644 --- a/model/config.go +++ b/model/config.go @@ -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 diff --git a/model/session.go b/model/session.go index 4f3547582..960c18cbf 100644 --- a/model/session.go +++ b/model/session.go @@ -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 diff --git a/model/user_access_token.go b/model/user_access_token.go new file mode 100644 index 000000000..090780fd0 --- /dev/null +++ b/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 + } +} diff --git a/model/user_access_token_test.go b/model/user_access_token_test.go new file mode 100644 index 000000000..1b4a9ccfd --- /dev/null +++ b/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) + } +} -- cgit v1.2.3-1-g7c22