PLT-6763 Implement user access tokens and new roles (server-side) (#6972)
* Implement user access tokens and new roles * Update config.json * Add public post permission to apiv3 * Remove old comment * Fix model unit test * Updates to store per feedback * Updates per feedback from CS
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
ed62660e96
Коммит
59992ae4a4
@@ -48,6 +48,7 @@ var PERMISSION_MANAGE_OTHERS_WEBHOOKS *Permission
|
||||
var PERMISSION_MANAGE_OAUTH *Permission
|
||||
var PERMISSION_MANAGE_SYSTEM_WIDE_OAUTH *Permission
|
||||
var PERMISSION_CREATE_POST *Permission
|
||||
var PERMISSION_CREATE_POST_PUBLIC *Permission
|
||||
var PERMISSION_EDIT_POST *Permission
|
||||
var PERMISSION_EDIT_OTHERS_POSTS *Permission
|
||||
var PERMISSION_DELETE_POST *Permission
|
||||
@@ -59,6 +60,9 @@ var PERMISSION_IMPORT_TEAM *Permission
|
||||
var PERMISSION_VIEW_TEAM *Permission
|
||||
var PERMISSION_LIST_USERS_WITHOUT_TEAM *Permission
|
||||
var PERMISSION_MANAGE_JOBS *Permission
|
||||
var PERMISSION_CREATE_USER_ACCESS_TOKEN *Permission
|
||||
var PERMISSION_READ_USER_ACCESS_TOKEN *Permission
|
||||
var PERMISSION_REVOKE_USER_ACCESS_TOKEN *Permission
|
||||
|
||||
// General permission that encompases all system admin functions
|
||||
// in the future this could be broken up to allow access to some
|
||||
@@ -67,9 +71,12 @@ var PERMISSION_MANAGE_SYSTEM *Permission
|
||||
|
||||
var ROLE_SYSTEM_USER *Role
|
||||
var ROLE_SYSTEM_ADMIN *Role
|
||||
var ROLE_SYSTEM_POST_ALL_PUBLIC *Role
|
||||
var ROLE_SYSTEM_USER_ACCESS_TOKEN *Role
|
||||
|
||||
var ROLE_TEAM_USER *Role
|
||||
var ROLE_TEAM_ADMIN *Role
|
||||
var ROLE_TEAM_POST_ALL_PUBLIC *Role
|
||||
|
||||
var ROLE_CHANNEL_USER *Role
|
||||
var ROLE_CHANNEL_ADMIN *Role
|
||||
@@ -243,6 +250,11 @@ func InitalizePermissions() {
|
||||
"authentication.permissions.create_post.name",
|
||||
"authentication.permissions.create_post.description",
|
||||
}
|
||||
PERMISSION_CREATE_POST_PUBLIC = &Permission{
|
||||
"create_post_public",
|
||||
"authentication.permissions.create_post_public.name",
|
||||
"authentication.permissions.create_post_public.description",
|
||||
}
|
||||
PERMISSION_EDIT_POST = &Permission{
|
||||
"edit_post",
|
||||
"authentication.permissions.edit_post.name",
|
||||
@@ -290,8 +302,23 @@ func InitalizePermissions() {
|
||||
}
|
||||
PERMISSION_LIST_USERS_WITHOUT_TEAM = &Permission{
|
||||
"list_users_without_team",
|
||||
"authentication.permisssions.list_users_without_team.name",
|
||||
"authentication.permisssions.list_users_without_team.description",
|
||||
"authentication.permissions.list_users_without_team.name",
|
||||
"authentication.permissions.list_users_without_team.description",
|
||||
}
|
||||
PERMISSION_CREATE_USER_ACCESS_TOKEN = &Permission{
|
||||
"create_user_access_token",
|
||||
"authentication.permissions.create_user_access_token.name",
|
||||
"authentication.permissions.create_user_access_token.description",
|
||||
}
|
||||
PERMISSION_READ_USER_ACCESS_TOKEN = &Permission{
|
||||
"read_user_access_token",
|
||||
"authentication.permissions.read_user_access_token.name",
|
||||
"authentication.permissions.read_user_access_token.description",
|
||||
}
|
||||
PERMISSION_REVOKE_USER_ACCESS_TOKEN = &Permission{
|
||||
"revoke_user_access_token",
|
||||
"authentication.permissions.revoke_user_access_token.name",
|
||||
"authentication.permissions.revoke_user_access_token.description",
|
||||
}
|
||||
PERMISSION_MANAGE_JOBS = &Permission{
|
||||
"manage_jobs",
|
||||
@@ -348,6 +375,17 @@ func InitalizeRoles() {
|
||||
},
|
||||
}
|
||||
BuiltInRoles[ROLE_TEAM_USER.Id] = ROLE_TEAM_USER
|
||||
|
||||
ROLE_TEAM_POST_ALL_PUBLIC = &Role{
|
||||
"team_post_all_public",
|
||||
"authentication.roles.team_post_all_public.name",
|
||||
"authentication.roles.team_post_all_public.description",
|
||||
[]string{
|
||||
PERMISSION_CREATE_POST_PUBLIC.Id,
|
||||
},
|
||||
}
|
||||
BuiltInRoles[ROLE_TEAM_POST_ALL_PUBLIC.Id] = ROLE_TEAM_POST_ALL_PUBLIC
|
||||
|
||||
ROLE_TEAM_ADMIN = &Role{
|
||||
"team_admin",
|
||||
"authentication.roles.team_admin.name",
|
||||
@@ -378,6 +416,29 @@ func InitalizeRoles() {
|
||||
},
|
||||
}
|
||||
BuiltInRoles[ROLE_SYSTEM_USER.Id] = ROLE_SYSTEM_USER
|
||||
|
||||
ROLE_SYSTEM_POST_ALL_PUBLIC = &Role{
|
||||
"system_post_all_public",
|
||||
"authentication.roles.system_post_all_public.name",
|
||||
"authentication.roles.system_post_all_public.description",
|
||||
[]string{
|
||||
PERMISSION_CREATE_POST_PUBLIC.Id,
|
||||
},
|
||||
}
|
||||
BuiltInRoles[ROLE_SYSTEM_POST_ALL_PUBLIC.Id] = ROLE_SYSTEM_POST_ALL_PUBLIC
|
||||
|
||||
ROLE_SYSTEM_USER_ACCESS_TOKEN = &Role{
|
||||
"system_user_access_token",
|
||||
"authentication.roles.system_user_access_token.name",
|
||||
"authentication.roles.system_user_access_token.description",
|
||||
[]string{
|
||||
PERMISSION_CREATE_USER_ACCESS_TOKEN.Id,
|
||||
PERMISSION_READ_USER_ACCESS_TOKEN.Id,
|
||||
PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
|
||||
},
|
||||
}
|
||||
BuiltInRoles[ROLE_SYSTEM_USER_ACCESS_TOKEN.Id] = ROLE_SYSTEM_USER_ACCESS_TOKEN
|
||||
|
||||
ROLE_SYSTEM_ADMIN = &Role{
|
||||
"system_admin",
|
||||
"authentication.roles.global_admin.name",
|
||||
@@ -412,6 +473,10 @@ func InitalizeRoles() {
|
||||
PERMISSION_ADD_USER_TO_TEAM.Id,
|
||||
PERMISSION_LIST_USERS_WITHOUT_TEAM.Id,
|
||||
PERMISSION_MANAGE_JOBS.Id,
|
||||
PERMISSION_CREATE_POST_PUBLIC.Id,
|
||||
PERMISSION_CREATE_USER_ACCESS_TOKEN.Id,
|
||||
PERMISSION_READ_USER_ACCESS_TOKEN.Id,
|
||||
PERMISSION_REVOKE_USER_ACCESS_TOKEN.Id,
|
||||
},
|
||||
ROLE_TEAM_USER.Permissions...,
|
||||
),
|
||||
|
||||
@@ -70,6 +70,10 @@ func (c *Client4) GetUserRoute(userId string) string {
|
||||
return fmt.Sprintf(c.GetUsersRoute()+"/%v", userId)
|
||||
}
|
||||
|
||||
func (c *Client4) GetUserAccessTokenRoute(tokenId string) string {
|
||||
return fmt.Sprintf(c.GetUsersRoute()+"/tokens/%v", tokenId)
|
||||
}
|
||||
|
||||
func (c *Client4) GetUserByUsernameRoute(userName string) string {
|
||||
return fmt.Sprintf(c.GetUsersRoute()+"/username/%v", userName)
|
||||
}
|
||||
@@ -957,6 +961,60 @@ func (c *Client4) SetProfileImage(userId string, data []byte) (bool, *Response)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateUserAccessToken will generate a user access token that can be used in place
|
||||
// of a session token to access the REST API. Must have the 'create_user_access_token'
|
||||
// permission and if generating for another user, must have the 'edit_other_users'
|
||||
// permission. A non-blank description is required.
|
||||
func (c *Client4) CreateUserAccessToken(userId, description string) (*UserAccessToken, *Response) {
|
||||
requestBody := map[string]string{"description": description}
|
||||
if r, err := c.DoApiPost(c.GetUserRoute(userId)+"/tokens", MapToJson(requestBody)); err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return UserAccessTokenFromJson(r.Body), BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserAccessToken will get a user access token's id, description and the user_id
|
||||
// of the user it is for. The actual token will not be returned. Must have the
|
||||
// 'read_user_access_token' permission and if getting for another user, must have the
|
||||
// 'edit_other_users' permission.
|
||||
func (c *Client4) GetUserAccessToken(tokenId string) (*UserAccessToken, *Response) {
|
||||
if r, err := c.DoApiGet(c.GetUserAccessTokenRoute(tokenId), ""); err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return UserAccessTokenFromJson(r.Body), BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
// GetUserAccessTokensForUser will get a paged list of user access tokens showing id,
|
||||
// description and user_id for each. The actual tokens will not be returned. Must have
|
||||
// the 'read_user_access_token' permission and if getting for another user, must have the
|
||||
// 'edit_other_users' permission.
|
||||
func (c *Client4) GetUserAccessTokensForUser(userId string, page, perPage int) ([]*UserAccessToken, *Response) {
|
||||
query := fmt.Sprintf("?page=%v&per_page=%v", page, perPage)
|
||||
if r, err := c.DoApiGet(c.GetUserRoute(userId)+"/tokens"+query, ""); err != nil {
|
||||
return nil, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return UserAccessTokenListFromJson(r.Body), BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
// RevokeUserAccessToken will revoke a user access token by id. Must have the
|
||||
// 'revoke_user_access_token' permission and if revoking for another user, must have the
|
||||
// 'edit_other_users' permission.
|
||||
func (c *Client4) RevokeUserAccessToken(tokenId string) (bool, *Response) {
|
||||
requestBody := map[string]string{"token_id": tokenId}
|
||||
if r, err := c.DoApiPost(c.GetUsersRoute()+"/tokens/revoke", MapToJson(requestBody)); err != nil {
|
||||
return false, BuildErrorResponse(r, err)
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return CheckStatusOK(r), BuildResponse(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Team Section
|
||||
|
||||
// CreateTeam creates a team in the system based on the provided team struct.
|
||||
|
||||
@@ -154,6 +154,7 @@ type ServiceSettings struct {
|
||||
EnableInsecureOutgoingConnections *bool
|
||||
EnableMultifactorAuthentication *bool
|
||||
EnforceMultifactorAuthentication *bool
|
||||
EnableUserAccessTokens *bool
|
||||
AllowCorsFrom *string
|
||||
SessionLengthWebInDays *int
|
||||
SessionLengthMobileInDays *int
|
||||
@@ -618,6 +619,11 @@ func (o *Config) SetDefaults() {
|
||||
*o.ServiceSettings.EnforceMultifactorAuthentication = false
|
||||
}
|
||||
|
||||
if o.ServiceSettings.EnableUserAccessTokens == nil {
|
||||
o.ServiceSettings.EnableUserAccessTokens = new(bool)
|
||||
*o.ServiceSettings.EnableUserAccessTokens = false
|
||||
}
|
||||
|
||||
if o.PasswordSettings.MinimumLength == nil {
|
||||
o.PasswordSettings.MinimumLength = new(int)
|
||||
*o.PasswordSettings.MinimumLength = PASSWORD_MINIMUM_LENGTH
|
||||
|
||||
@@ -10,13 +10,17 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
|
||||
SESSION_COOKIE_USER = "MMUSERID"
|
||||
SESSION_CACHE_SIZE = 35000
|
||||
SESSION_PROP_PLATFORM = "platform"
|
||||
SESSION_PROP_OS = "os"
|
||||
SESSION_PROP_BROWSER = "browser"
|
||||
SESSION_ACTIVITY_TIMEOUT = 1000 * 60 * 5 // 5 minutes
|
||||
SESSION_COOKIE_TOKEN = "MMAUTHTOKEN"
|
||||
SESSION_COOKIE_USER = "MMUSERID"
|
||||
SESSION_CACHE_SIZE = 35000
|
||||
SESSION_PROP_PLATFORM = "platform"
|
||||
SESSION_PROP_OS = "os"
|
||||
SESSION_PROP_BROWSER = "browser"
|
||||
SESSION_PROP_TYPE = "type"
|
||||
SESSION_PROP_USER_ACCESS_TOKEN_ID = "user_access_token_id"
|
||||
SESSION_TYPE_USER_ACCESS_TOKEN = "UserAccessToken"
|
||||
SESSION_ACTIVITY_TIMEOUT = 1000 * 60 * 5 // 5 minutes
|
||||
SESSION_USER_ACCESS_TOKEN_EXPIRY = 100 * 365 // 100 years
|
||||
)
|
||||
|
||||
type Session struct {
|
||||
@@ -58,7 +62,9 @@ func (me *Session) PreSave() {
|
||||
me.Id = NewId()
|
||||
}
|
||||
|
||||
me.Token = NewId()
|
||||
if me.Token == "" {
|
||||
me.Token = NewId()
|
||||
}
|
||||
|
||||
me.CreateAt = GetMillis()
|
||||
me.LastActivityAt = me.CreateAt
|
||||
|
||||
81
model/user_access_token.go
Обычный файл
81
model/user_access_token.go
Обычный файл
@@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type UserAccessToken struct {
|
||||
Id string `json:"id"`
|
||||
Token string `json:"token,omitempty"`
|
||||
UserId string `json:"user_id"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
func (t *UserAccessToken) IsValid() *AppError {
|
||||
if len(t.Id) != 26 {
|
||||
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(t.Token) != 26 {
|
||||
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.token.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(t.UserId) != 26 {
|
||||
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.user_id.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
if len(t.Description) > 255 {
|
||||
return NewAppError("UserAccessToken.IsValid", "model.user_access_token.is_valid.description.app_error", nil, "", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *UserAccessToken) PreSave() {
|
||||
t.Id = NewId()
|
||||
}
|
||||
|
||||
func (t *UserAccessToken) ToJson() string {
|
||||
b, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return ""
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func UserAccessTokenFromJson(data io.Reader) *UserAccessToken {
|
||||
decoder := json.NewDecoder(data)
|
||||
var t UserAccessToken
|
||||
err := decoder.Decode(&t)
|
||||
if err == nil {
|
||||
return &t
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func UserAccessTokenListToJson(t []*UserAccessToken) string {
|
||||
b, err := json.Marshal(t)
|
||||
if err != nil {
|
||||
return ""
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func UserAccessTokenListFromJson(data io.Reader) []*UserAccessToken {
|
||||
decoder := json.NewDecoder(data)
|
||||
var t []*UserAccessToken
|
||||
err := decoder.Decode(&t)
|
||||
if err == nil {
|
||||
return t
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
58
model/user_access_token_test.go
Обычный файл
58
model/user_access_token_test.go
Обычный файл
@@ -0,0 +1,58 @@
|
||||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestUserAccessTokenJson(t *testing.T) {
|
||||
a1 := UserAccessToken{}
|
||||
a1.UserId = NewId()
|
||||
a1.Token = NewId()
|
||||
|
||||
json := a1.ToJson()
|
||||
ra1 := UserAccessTokenFromJson(strings.NewReader(json))
|
||||
|
||||
if a1.Token != ra1.Token {
|
||||
t.Fatal("tokens didn't match")
|
||||
}
|
||||
|
||||
tokens := []*UserAccessToken{&a1}
|
||||
json = UserAccessTokenListToJson(tokens)
|
||||
tokens = UserAccessTokenListFromJson(strings.NewReader(json))
|
||||
|
||||
if tokens[0].Token != a1.Token {
|
||||
t.Fatal("tokens didn't match")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserAccessTokenIsValid(t *testing.T) {
|
||||
ad := UserAccessToken{}
|
||||
|
||||
if err := ad.IsValid(); err == nil || err.Id != "model.user_access_token.is_valid.id.app_error" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ad.Id = NewRandomString(26)
|
||||
if err := ad.IsValid(); err == nil || err.Id != "model.user_access_token.is_valid.token.app_error" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ad.Token = NewRandomString(26)
|
||||
if err := ad.IsValid(); err == nil || err.Id != "model.user_access_token.is_valid.user_id.app_error" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ad.UserId = NewRandomString(26)
|
||||
if err := ad.IsValid(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
ad.Description = NewRandomString(256)
|
||||
if err := ad.IsValid(); err == nil || err.Id != "model.user_access_token.is_valid.description.app_error" {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Ссылка в новой задаче
Block a user