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 удалений

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

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