Implement APIv4 infrastructure (#5191)

* Implement APIv4 infrastructure

* Update parameter requirement functions per feedback
Этот коммит содержится в:
Joram Wilander
2017-01-30 08:30:02 -05:00
коммит произвёл GitHub
родитель 3e2f879b77
Коммит c01d9ad6cf
31 изменённых файлов: 1826 добавлений и 272 удалений

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

@@ -203,6 +203,7 @@ test-server: start-docker prepare-enterprise
echo "mode: count" > cover.out
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=650s -covermode=count -coverprofile=capi.out ./api || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=650s -covermode=count -coverprofile=capi4.out ./api4 || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=capp.out ./app || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=60s -covermode=count -coverprofile=cmodel.out ./model || exit 1
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=180s -covermode=count -coverprofile=cstore.out ./store || exit 1
@@ -210,12 +211,13 @@ test-server: start-docker prepare-enterprise
$(GO) test $(GOFLAGS) -run=$(TESTS) -test.v -test.timeout=120s -covermode=count -coverprofile=cweb.out ./web || exit 1
tail -n +2 capi.out >> cover.out
tail -n +2 capi4.out >> cover.out
tail -n +2 capp.out >> cover.out
tail -n +2 cmodel.out >> cover.out
tail -n +2 cstore.out >> cover.out
tail -n +2 cutils.out >> cover.out
tail -n +2 cweb.out >> cover.out
rm -f capi.out capp.out cmodel.out cstore.out cutils.out cweb.out
rm -f capi.out capi4.out capp.out cmodel.out cstore.out cutils.out cweb.out
ifeq ($(BUILD_ENTERPRISE_READY),true)
@echo Running Enterprise tests

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

@@ -67,7 +67,7 @@ func InitRouter() {
func InitApi() {
BaseRoutes = &Routes{}
BaseRoutes.Root = app.Srv.Router
BaseRoutes.ApiRoot = app.Srv.Router.PathPrefix(model.API_URL_SUFFIX).Subrouter()
BaseRoutes.ApiRoot = app.Srv.Router.PathPrefix(model.API_URL_SUFFIX_V3).Subrouter()
BaseRoutes.Users = BaseRoutes.ApiRoot.PathPrefix("/users").Subrouter()
BaseRoutes.NeedUser = BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.Teams = BaseRoutes.ApiRoot.PathPrefix("/teams").Subrouter()

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

@@ -239,7 +239,7 @@ func TestTestCommand(t *testing.T) {
*utils.Cfg.ServiceSettings.EnableCommands = true
cmd1 := &model.Command{
URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX + "/teams/command_test",
URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX_V3 + "/teams/command_test",
Method: model.COMMAND_METHOD_POST,
Trigger: "test",
}
@@ -259,7 +259,7 @@ func TestTestCommand(t *testing.T) {
}
cmd2 := &model.Command{
URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX + "/teams/command_test",
URL: "http://localhost" + utils.Cfg.ServiceSettings.ListenAddress + model.API_URL_SUFFIX_V3 + "/teams/command_test",
Method: model.COMMAND_METHOD_GET,
Trigger: "test2",
}

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

@@ -21,17 +21,18 @@ import (
)
type Context struct {
Session model.Session
RequestId string
IpAddress string
Path string
Err *model.AppError
siteURL string
teamURLValid bool
teamURL string
T goi18n.TranslateFunc
Locale string
TeamId string
Session model.Session
RequestId string
IpAddress string
Path string
Err *model.AppError
siteURL string
teamURLValid bool
teamURL string
T goi18n.TranslateFunc
Locale string
TeamId string
isSystemAdmin bool
}
func ApiAppHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
@@ -142,7 +143,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if utils.GetSiteURL() == "" {
protocol := GetProtocol(r)
protocol := app.GetProtocol(r)
c.SetSiteURL(protocol + "://" + r.Host)
} else {
c.SetSiteURL(utils.GetSiteURL())
@@ -251,21 +252,13 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if h.isApi && einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementHttpRequest()
if r.URL.Path != model.API_URL_SUFFIX+"/users/websocket" {
if r.URL.Path != model.API_URL_SUFFIX_V3+"/users/websocket" {
elapsed := float64(time.Since(now)) / float64(time.Second)
einterfaces.GetMetricsInterface().ObserveHttpRequestDuration(elapsed)
}
}
}
func GetProtocol(r *http.Request) string {
if r.Header.Get(model.HEADER_FORWARDED_PROTO) == "https" {
return "https"
} else {
return "http"
}
}
func (c *Context) LogAudit(extraInfo string) {
audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
if r := <-app.Srv.Store.Audit().Save(audit); r.Err != nil {
@@ -347,13 +340,17 @@ func (c *Context) SystemAdminRequired() {
c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "SystemAdminRequired")
c.Err.StatusCode = http.StatusUnauthorized
return
} else if !app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM) {
} else if !c.IsSystemAdmin() {
c.Err = model.NewLocAppError("", "api.context.permissions.app_error", nil, "AdminRequired")
c.Err.StatusCode = http.StatusForbidden
return
}
}
func (c *Context) IsSystemAdmin() bool {
return app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM)
}
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,

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

@@ -406,6 +406,7 @@ func TestGetPublicFile(t *testing.T) {
time.Sleep(2 * time.Second)
if resp, err := http.Get(link); err != nil || resp.StatusCode != http.StatusOK {
t.Log(link)
t.Fatal("failed to get image with public link", err)
}
@@ -509,7 +510,7 @@ func TestGetPublicFileOld(t *testing.T) {
func generatePublicLinkOld(siteURL, teamId, channelId, userId, filename string) string {
hash := app.GeneratePublicLinkHash(filename, *utils.Cfg.FileSettings.PublicLinkSalt)
return fmt.Sprintf("%s%s/public/files/get/%s/%s/%s/%s?h=%s", siteURL, model.API_URL_SUFFIX, teamId, channelId, userId, filename, hash)
return fmt.Sprintf("%s%s/public/files/get/%s/%s/%s/%s?h=%s", siteURL, model.API_URL_SUFFIX_V3, teamId, channelId, userId, filename, hash)
}
func TestGetPublicLink(t *testing.T) {

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

@@ -291,7 +291,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
doLogin(c, w, r, user, "")
}
if c.Err == nil {
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_LOGIN:
@@ -304,25 +304,25 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, c.GetSiteURL()+val, http.StatusTemporaryRedirect)
return
}
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_EMAIL_TO_SSO:
CompleteSwitchWithOAuth(c, w, r, service, body, props["email"])
if c.Err == nil {
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/login?extra=signin_change", http.StatusTemporaryRedirect)
http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host+"/login?extra=signin_change", http.StatusTemporaryRedirect)
}
break
case model.OAUTH_ACTION_SSO_TO_EMAIL:
LoginByOAuth(c, w, r, service, body)
if c.Err == nil {
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect)
http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host+"/claim?email="+url.QueryEscape(props["email"]), http.StatusTemporaryRedirect)
}
break
default:
LoginByOAuth(c, w, r, service, body)
if c.Err == nil {
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
}
break
}

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

@@ -20,7 +20,6 @@ import (
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
)
func InitUser() {
@@ -77,12 +76,6 @@ func InitUser() {
}
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.EmailSettings.EnableSignUpWithEmail || !utils.Cfg.TeamSettings.EnableUserCreation {
c.Err = model.NewLocAppError("createUser", "api.user.create_user.signup_email_disabled.app_error", nil, "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
user := model.UserFromJson(r.Body)
if user == nil {
@@ -90,45 +83,25 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
user.EmailVerified = false
hash := r.URL.Query().Get("h")
inviteId := r.URL.Query().Get("iid")
var ruser *model.User
var err *model.AppError
if len(hash) > 0 {
data := r.URL.Query().Get("d")
ruser, err = app.CreateUserWithHash(user, hash, data)
if err != nil {
c.Err = err
return
}
ruser, err = app.CreateUserWithHash(user, hash, r.URL.Query().Get("d"))
} else if len(inviteId) > 0 {
ruser, err = app.CreateUserWithInviteId(user, inviteId, c.GetSiteURL())
if err != nil {
c.Err = err
return
}
} else {
if !app.IsFirstUserAccount() && !*utils.Cfg.TeamSettings.EnableOpenServer {
c.Err = model.NewLocAppError("createUser", "api.user.create_user.no_open_server", nil, "email="+user.Email)
return
}
ruser, err = app.CreateUserFromSignup(user, c.GetSiteURL())
}
ruser, err = app.CreateUser(user)
if err != nil {
c.Err = err
return
}
if err := app.SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, c.GetSiteURL()); err != nil {
l4g.Error(err.Error())
}
if err != nil {
c.Err = err
return
}
w.Write([]byte(ruser.ToJson()))
}
func login(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -141,56 +114,15 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
deviceId := props["device_id"]
ldapOnly := props["ldap_only"] == "true"
if len(password) == 0 {
c.Err = model.NewLocAppError("login", "api.user.login.blank_pwd.app_error", nil, "")
c.Err.StatusCode = http.StatusBadRequest
return
}
var user *model.User
var err *model.AppError
if len(id) != 0 {
c.LogAuditWithUserId(id, "attempt")
if user, err = app.GetUser(id); err != nil {
c.LogAuditWithUserId(id, "failure")
c.Err = err
c.Err.StatusCode = http.StatusBadRequest
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementLoginFail()
}
return
}
} else {
c.LogAudit("attempt")
if user, err = app.GetUserForLogin(loginId, ldapOnly); err != nil {
c.LogAudit("failure")
c.Err = err
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementLoginFail()
}
return
}
c.LogAuditWithUserId(user.Id, "attempt")
}
// and then authenticate them
if user, err = authenticateUser(user, password, mfaToken); err != nil {
c.LogAuditWithUserId(user.Id, "failure")
c.LogAudit("attempt - user_id=" + id + " login_id=" + loginId)
user, err := app.AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId, ldapOnly)
if err != nil {
c.LogAudit("failure - user_id=" + id + " login_id=" + loginId)
c.Err = err
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementLoginFail()
}
return
}
c.LogAuditWithUserId(user.Id, "success")
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementLogin()
}
doLogin(c, w, r, user, deviceId)
if c.Err != nil {
@@ -244,77 +176,12 @@ func LoginByOAuth(c *Context, w http.ResponseWriter, r *http.Request, service st
// User MUST be authenticated completely before calling Login
func doLogin(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) {
session := &model.Session{UserId: user.Id, Roles: user.GetRawRoles(), DeviceId: deviceId, IsOAuth: false}
maxAge := *utils.Cfg.ServiceSettings.SessionLengthWebInDays * 60 * 60 * 24
if len(deviceId) > 0 {
session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthMobileInDays)
maxAge = *utils.Cfg.ServiceSettings.SessionLengthMobileInDays * 60 * 60 * 24
// A special case where we logout of all other sessions with the same Id
if err := app.RevokeSessionsForDeviceId(user.Id, deviceId, ""); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusInternalServerError
return
}
} else {
session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthWebInDays)
}
ua := user_agent.New(r.UserAgent())
plat := ua.Platform()
if plat == "" {
plat = "unknown"
}
os := ua.OS()
if os == "" {
os = "unknown"
}
bname, bversion := ua.Browser()
if bname == "" {
bname = "unknown"
}
if bversion == "" {
bversion = "0.0"
}
session.AddProp(model.SESSION_PROP_PLATFORM, plat)
session.AddProp(model.SESSION_PROP_OS, os)
session.AddProp(model.SESSION_PROP_BROWSER, fmt.Sprintf("%v/%v", bname, bversion))
var err *model.AppError
if session, err = app.CreateSession(session); err != nil {
session, err := app.DoLogin(w, r, user, deviceId)
if err != nil {
c.Err = err
c.Err.StatusCode = http.StatusInternalServerError
return
}
w.Header().Set(model.HEADER_TOKEN, session.Token)
secure := false
if GetProtocol(r) == "https" {
secure = true
}
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
sessionCookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
Value: session.Token,
Path: "/",
MaxAge: maxAge,
Expires: expiresAt,
HttpOnly: true,
Secure: secure,
}
http.SetCookie(w, sessionCookie)
c.Session = *session
}
@@ -357,7 +224,7 @@ func attachDeviceId(c *Context, w http.ResponseWriter, r *http.Request) {
maxAge := *utils.Cfg.ServiceSettings.SessionLengthMobileInDays * 60 * 60 * 24
secure := false
if GetProtocol(r) == "https" {
if app.GetProtocol(r) == "https" {
secure = true
}
@@ -502,12 +369,15 @@ func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
if user, err = app.GetUser(id); err != nil {
c.Err = err
return
} else if HandleEtag(user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress), "Get User", w, r) {
}
etag := user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress)
if HandleEtag(etag, "Get User", w, r) {
return
} else {
sanitizeProfile(c, user)
w.Header().Set(model.HEADER_ETAG_SERVER, user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress))
app.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.Write([]byte(user.ToJson()))
return
}
@@ -829,24 +699,11 @@ func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if ruser, err := app.UpdateUser(user, c.GetSiteURL()); err != nil {
if ruser, err := app.UpdateUserAsUser(user, c.GetSiteURL(), c.IsSystemAdmin()); err != nil {
c.Err = err
return
} else {
c.LogAudit("")
updatedUser := ruser
updatedUser = sanitizeProfile(c, updatedUser)
omitUsers := make(map[string]bool, 1)
omitUsers[user.Id] = true
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", omitUsers)
message.Add("user", updatedUser)
go app.Publish(message)
ruser.Password = ""
ruser.AuthData = new(string)
*ruser.AuthData = ""
w.Write([]byte(ruser.ToJson()))
}
}
@@ -875,38 +732,8 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
var user *model.User
var err *model.AppError
if user, err = app.GetUser(userId); err != nil {
c.Err = err
return
}
if user == nil {
c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.valid_account.app_error", nil, "")
c.Err.StatusCode = http.StatusBadRequest
return
}
if user.AuthData != nil && *user.AuthData != "" {
c.LogAudit("failed - tried to update user password who was logged in through oauth")
c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService)
c.Err.StatusCode = http.StatusBadRequest
return
}
if err := doubleCheckPassword(user, currentPassword); err != nil {
if err.Id == "api.user.check_user_password.invalid.app_error" {
c.Err = model.NewLocAppError("updatePassword", "api.user.update_password.incorrect.app_error", nil, "")
} else {
c.Err = err
}
c.Err.StatusCode = http.StatusForbidden
return
}
if err := app.UpdatePasswordSendEmail(user, newPassword, c.T("api.user.update_password.menu"), c.GetSiteURL()); err != nil {
if err := app.UpdatePasswordAsUser(userId, currentPassword, newPassword, c.GetSiteURL()); err != nil {
c.LogAudit("failed")
c.Err = err
return
} else {
@@ -1110,7 +937,7 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if err := checkPasswordAndAllCriteria(user, password, mfaToken); err != nil {
if err := app.CheckPasswordAndAllCriteria(user, password, mfaToken); err != nil {
c.LogAuditWithUserId(user.Id, "failed - bad authentication")
c.Err = err
return
@@ -1238,7 +1065,7 @@ func emailToLdap(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if err := checkPasswordAndAllCriteria(user, emailPassword, token); err != nil {
if err := app.CheckPasswordAndAllCriteria(user, emailPassword, token); err != nil {
c.LogAuditWithUserId(user.Id, "failed - bad authentication")
c.Err = err
return
@@ -1332,7 +1159,7 @@ func ldapToEmail(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
if err := checkUserMfa(user, token); err != nil {
if err := app.CheckUserMfa(user, token); err != nil {
c.LogAuditWithUserId(user.Id, "fail - mfa token failed")
c.Err = err
return
@@ -1600,7 +1427,7 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
c.Err.StatusCode = http.StatusFound
return
} else {
if err := checkUserAdditionalAuthenticationCriteria(user, ""); err != nil {
if err := app.CheckUserAdditionalAuthenticationCriteria(user, ""); err != nil {
c.Err = err
c.Err.StatusCode = http.StatusFound
return
@@ -1635,7 +1462,7 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, c.GetSiteURL()+val, http.StatusFound)
return
}
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusFound)
http.Redirect(w, r, app.GetProtocol(r)+"://"+r.Host, http.StatusFound)
}
}

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

@@ -634,7 +634,7 @@ func TestGetAudits(t *testing.T) {
t.Fatal(err)
} else {
if len(result.Data.(model.Audits)) != 2 {
if len(result.Data.(model.Audits)) != 1 {
t.Fatal(result.Data.(model.Audits))
}

189
api4/api.go Обычный файл
Просмотреть файл

@@ -0,0 +1,189 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api4
import (
"net/http"
l4g "github.com/alecthomas/log4go"
"github.com/gorilla/mux"
"github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
_ "github.com/nicksnyder/go-i18n/i18n"
)
type Routes struct {
Root *mux.Router // ''
ApiRoot *mux.Router // 'api/v4'
Users *mux.Router // 'api/v4/users'
User *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}'
UserByUsername *mux.Router // 'api/v4/users/username/{username:[A-Za-z0-9_-\.]+}'
UserByEmail *mux.Router // 'api/v4/users/email/{email}'
Teams *mux.Router // 'api/v4/teams'
TeamsForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/teams'
Team *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}'
TeamByName *mux.Router // 'api/v4/teams/name/{team_name:[A-Za-z0-9_-]+}'
TeamMembers *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9_-]+}/members'
TeamMember *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9_-]+}/members/{user_id:[A-Za-z0-9_-]+}'
Channels *mux.Router // 'api/v4/channels'
Channel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}'
ChannelByName *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels/name/{channel_name:[A-Za-z0-9_-]+}'
ChannelsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/channels'
ChannelMembers *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members'
ChannelMember *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/members/{user_id:[A-Za-z0-9]+}'
ChannelMembersForUser *mux.Router // 'api/v4/users/{user_id:[A-Za-z0-9]+}/channels/members'
Posts *mux.Router // 'api/v4/posts'
Post *mux.Router // 'api/v4/posts/{post_id:[A-Za-z0-9]+}'
PostsForChannel *mux.Router // 'api/v4/channels/{channel_id:[A-Za-z0-9]+}/posts'
Files *mux.Router // 'api/v4/files'
File *mux.Router // 'api/v4/files/{file_id:[A-Za-z0-9]+}'
Commands *mux.Router // 'api/v4/commands'
Command *mux.Router // 'api/v4/commands/{command_id:[A-Za-z0-9]+}'
CommandsForTeam *mux.Router // 'api/v4/teams/{team_id:[A-Za-z0-9]+}/commands'
Hooks *mux.Router // 'api/v4/teams/hooks'
IncomingHooks *mux.Router // 'api/v4/teams/hooks/incoming'
IncomingHook *mux.Router // 'api/v4/teams/hooks/incoming/{hook_id:[A-Za-z0-9]+}'
OutgoingHooks *mux.Router // 'api/v4/teams/hooks/outgoing'
OutgoingHook *mux.Router // 'api/v4/teams/hooks/outgoing/{hook_id:[A-Za-z0-9]+}'
OAuth *mux.Router // 'api/v4/oauth'
Admin *mux.Router // 'api/v4/admin'
System *mux.Router // 'api/v4/system'
Preferences *mux.Router // 'api/v4/preferences'
License *mux.Router // 'api/v4/license'
Public *mux.Router // 'api/v4/public'
Emojis *mux.Router // 'api/v4/emoji'
Emoji *mux.Router // 'api/v4/emoji/{emoji_id:[A-Za-z0-9]+}'
Webrtc *mux.Router // 'api/v4/webrtc'
}
var BaseRoutes *Routes
func InitRouter() {
app.Srv.Router = mux.NewRouter()
app.Srv.Router.NotFoundHandler = http.HandlerFunc(Handle404)
app.Srv.WebSocketRouter = app.NewWebSocketRouter()
}
func InitApi(full bool) {
BaseRoutes = &Routes{}
BaseRoutes.Root = app.Srv.Router
BaseRoutes.ApiRoot = app.Srv.Router.PathPrefix(model.API_URL_SUFFIX).Subrouter()
BaseRoutes.Users = BaseRoutes.ApiRoot.PathPrefix("/users").Subrouter()
BaseRoutes.User = BaseRoutes.Users.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.UserByUsername = BaseRoutes.Users.PathPrefix("/username/{username:[A-Za-z0-9_-.]+}").Subrouter()
BaseRoutes.UserByEmail = BaseRoutes.Users.PathPrefix("/email/{email}").Subrouter()
BaseRoutes.Teams = BaseRoutes.ApiRoot.PathPrefix("/teams").Subrouter()
BaseRoutes.TeamsForUser = BaseRoutes.Users.PathPrefix("/teams").Subrouter()
BaseRoutes.Team = BaseRoutes.Teams.PathPrefix("/{team_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.TeamByName = BaseRoutes.Teams.PathPrefix("/name/{team_name:[A-Za-z0-9_-]+}").Subrouter()
BaseRoutes.TeamMembers = BaseRoutes.Team.PathPrefix("/members").Subrouter()
BaseRoutes.TeamMember = BaseRoutes.TeamMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.Channels = BaseRoutes.ApiRoot.PathPrefix("/channels").Subrouter()
BaseRoutes.Channel = BaseRoutes.Channels.PathPrefix("/{channel_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.ChannelByName = BaseRoutes.Team.PathPrefix("/name/{channel_name:[A-Za-z0-9_-]+}").Subrouter()
BaseRoutes.ChannelsForTeam = BaseRoutes.Team.PathPrefix("/channels").Subrouter()
BaseRoutes.ChannelMembers = BaseRoutes.Channel.PathPrefix("/members").Subrouter()
BaseRoutes.ChannelMember = BaseRoutes.ChannelMembers.PathPrefix("/{user_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.ChannelMembersForUser = BaseRoutes.User.PathPrefix("/channels/members").Subrouter()
BaseRoutes.Posts = BaseRoutes.ApiRoot.PathPrefix("/posts").Subrouter()
BaseRoutes.Post = BaseRoutes.Posts.PathPrefix("/{post_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.PostsForChannel = BaseRoutes.Channel.PathPrefix("/posts").Subrouter()
BaseRoutes.Files = BaseRoutes.ApiRoot.PathPrefix("/files").Subrouter()
BaseRoutes.File = BaseRoutes.Files.PathPrefix("/{file_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.Commands = BaseRoutes.ApiRoot.PathPrefix("/commands").Subrouter()
BaseRoutes.Command = BaseRoutes.Commands.PathPrefix("/{command_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.CommandsForTeam = BaseRoutes.Team.PathPrefix("/commands").Subrouter()
BaseRoutes.Hooks = BaseRoutes.ApiRoot.PathPrefix("/hooks").Subrouter()
BaseRoutes.IncomingHooks = BaseRoutes.Hooks.PathPrefix("/incoming").Subrouter()
BaseRoutes.IncomingHook = BaseRoutes.IncomingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.OutgoingHooks = BaseRoutes.Hooks.PathPrefix("/outgoing").Subrouter()
BaseRoutes.OutgoingHook = BaseRoutes.OutgoingHooks.PathPrefix("/{hook_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.OAuth = BaseRoutes.ApiRoot.PathPrefix("/oauth").Subrouter()
BaseRoutes.Admin = BaseRoutes.ApiRoot.PathPrefix("/admin").Subrouter()
BaseRoutes.System = BaseRoutes.ApiRoot.PathPrefix("/system").Subrouter()
BaseRoutes.Preferences = BaseRoutes.ApiRoot.PathPrefix("/preferences").Subrouter()
BaseRoutes.License = BaseRoutes.ApiRoot.PathPrefix("/license").Subrouter()
BaseRoutes.Public = BaseRoutes.ApiRoot.PathPrefix("/public").Subrouter()
BaseRoutes.Emojis = BaseRoutes.ApiRoot.PathPrefix("/emoji").Subrouter()
BaseRoutes.Emoji = BaseRoutes.Emojis.PathPrefix("/{emoji_id:[A-Za-z0-9]+}").Subrouter()
BaseRoutes.Webrtc = BaseRoutes.ApiRoot.PathPrefix("/webrtc").Subrouter()
InitUser()
// REMOVE CONDITION WHEN APIv3 REMOVED
if full {
// 404 on any api route before web.go has a chance to serve it
app.Srv.Router.Handle("/api/{anything:.*}", http.HandlerFunc(Handle404))
utils.InitHTML()
app.InitEmailBatching()
}
}
func HandleEtag(etag string, routeName string, w http.ResponseWriter, r *http.Request) bool {
metrics := einterfaces.GetMetricsInterface()
if et := r.Header.Get(model.HEADER_ETAG_CLIENT); len(etag) > 0 {
if et == etag {
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.WriteHeader(http.StatusNotModified)
if metrics != nil {
metrics.IncrementEtagHitCounter(routeName)
}
return true
}
}
if metrics != nil {
metrics.IncrementEtagMissCounter(routeName)
}
return false
}
func Handle404(w http.ResponseWriter, r *http.Request) {
err := model.NewLocAppError("Handle404", "api.context.404.app_error", nil, "")
err.Translate(utils.T)
err.StatusCode = http.StatusNotFound
l4g.Debug("%v: code=404 ip=%v", r.URL.Path, utils.GetIpAddress(r))
w.WriteHeader(err.StatusCode)
err.DetailedError = "There doesn't appear to be an api call for the url='" + r.URL.Path + "'."
w.Write([]byte(err.ToJson()))
}
func ReturnStatusOK(w http.ResponseWriter) {
m := make(map[string]string)
m[model.STATUS] = model.STATUS_OK
w.Write([]byte(model.MapToJson(m)))
}

211
api4/apitestlib.go Обычный файл
Просмотреть файл

@@ -0,0 +1,211 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api4
import (
"net/http"
"reflect"
"strconv"
"strings"
"testing"
"github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
)
type TestHelper struct {
Client *model.Client4
BasicUser *model.User
SystemAdminUser *model.User
}
func Setup() *TestHelper {
if app.Srv == nil {
utils.TranslationsPreInit()
utils.LoadConfig("config.json")
utils.InitTranslations(utils.Cfg.LocalizationSettings)
utils.Cfg.TeamSettings.MaxUsersPerTeam = 50
*utils.Cfg.RateLimitSettings.Enable = false
utils.Cfg.EmailSettings.SendEmailNotifications = true
utils.Cfg.EmailSettings.SMTPServer = "dockerhost"
utils.Cfg.EmailSettings.SMTPPort = "2500"
utils.Cfg.EmailSettings.FeedbackEmail = "test@example.com"
utils.DisableDebugLogForTest()
app.NewServer()
app.InitStores()
InitRouter()
app.StartServer()
InitApi(true)
utils.EnableDebugLogForTest()
app.Srv.Store.MarkSystemRanUnitTests()
*utils.Cfg.TeamSettings.EnableOpenServer = true
}
th := &TestHelper{}
th.Client = th.CreateClient()
return th
}
func (me *TestHelper) InitBasic() *TestHelper {
me.BasicUser = me.CreateUser()
app.UpdateUserRoles(me.BasicUser.Id, model.ROLE_SYSTEM_USER.Id)
me.LoginBasic()
return me
}
func (me *TestHelper) InitSystemAdmin() *TestHelper {
me.SystemAdminUser = me.CreateUser()
app.UpdateUserRoles(me.SystemAdminUser.Id, model.ROLE_SYSTEM_USER.Id+" "+model.ROLE_SYSTEM_ADMIN.Id)
return me
}
func (me *TestHelper) CreateClient() *model.Client4 {
return model.NewAPIv4Client("http://localhost" + utils.Cfg.ServiceSettings.ListenAddress)
}
func (me *TestHelper) CreateUser() *model.User {
id := model.NewId()
user := &model.User{
Email: GenerateTestEmail(),
Username: GenerateTestUsername(),
Nickname: "nn_" + id,
FirstName: "f_" + id,
LastName: "l_" + id,
Password: "Password1",
}
utils.DisableDebugLogForTest()
ruser, _ := me.Client.CreateUser(user)
ruser.Password = "Password1"
VerifyUserEmail(ruser.Id)
utils.EnableDebugLogForTest()
return ruser
}
func (me *TestHelper) LoginBasic() {
utils.DisableDebugLogForTest()
me.Client.Login(me.BasicUser.Email, me.BasicUser.Password)
utils.EnableDebugLogForTest()
}
func (me *TestHelper) LoginSystemAdmin() {
utils.DisableDebugLogForTest()
me.Client.Login(me.SystemAdminUser.Email, me.SystemAdminUser.Password)
utils.EnableDebugLogForTest()
}
func GenerateTestEmail() string {
return strings.ToLower("success+" + model.NewId() + "@simulator.amazonses.com")
}
func GenerateTestUsername() string {
return "n" + model.NewId()
}
func VerifyUserEmail(userId string) {
store.Must(app.Srv.Store.User().VerifyEmail(userId))
}
func CheckUserSanitization(t *testing.T, user *model.User) {
if user.Password != "" {
t.Fatal("password wasn't blank")
}
if user.AuthData != nil && *user.AuthData != "" {
t.Fatal("auth data wasn't blank")
}
if user.MfaSecret != "" {
t.Fatal("mfa secret wasn't blank")
}
}
func CheckEtag(t *testing.T, data interface{}, resp *model.Response) {
if !reflect.ValueOf(data).IsNil() {
t.Fatal("etag data was not nil")
}
if resp.StatusCode != http.StatusNotModified {
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusNotModified))
t.Fatal("wrong status code for etag")
}
}
func CheckNoError(t *testing.T, resp *model.Response) {
if resp.Error != nil {
t.Fatal(resp.Error)
}
}
func CheckForbiddenStatus(t *testing.T, resp *model.Response) {
if resp.Error == nil {
t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusForbidden))
return
}
if resp.StatusCode != http.StatusForbidden {
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusForbidden))
t.Fatal("wrong status code")
}
}
func CheckUnauthorizedStatus(t *testing.T, resp *model.Response) {
if resp.Error == nil {
t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusUnauthorized))
return
}
if resp.StatusCode != http.StatusUnauthorized {
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusUnauthorized))
t.Fatal("wrong status code")
}
}
func CheckNotFoundStatus(t *testing.T, resp *model.Response) {
if resp.Error == nil {
t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusNotFound))
return
}
if resp.StatusCode != http.StatusNotFound {
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusNotFound))
t.Fatal("wrong status code")
}
}
func CheckBadRequestStatus(t *testing.T, resp *model.Response) {
if resp.Error == nil {
t.Fatal("should have errored with status:" + strconv.Itoa(http.StatusBadRequest))
return
}
if resp.StatusCode != http.StatusBadRequest {
t.Log("actual: " + strconv.Itoa(resp.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusBadRequest))
t.Fatal("wrong status code")
}
}
func CheckErrorMessage(t *testing.T, resp *model.Response, message string) {
if resp.Error == nil {
t.Fatal("should have errored with message:" + message)
return
}
if resp.Error.Message != message {
t.Log("actual: " + resp.Error.Message)
t.Log("expected: " + message)
t.Fatal("incorrect error message")
}
}

360
api4/context.go Обычный файл
Просмотреть файл

@@ -0,0 +1,360 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api4
import (
"fmt"
"net/http"
"strings"
"time"
l4g "github.com/alecthomas/log4go"
goi18n "github.com/nicksnyder/go-i18n/i18n"
"github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
type Context struct {
Session model.Session
Params *ApiParams
Err *model.AppError
T goi18n.TranslateFunc
RequestId string
IpAddress string
Path string
siteURL string
}
func ApiHandler(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &handler{
handleFunc: h,
requireSession: false,
trustRequester: false,
requireMfa: false,
}
}
func ApiSessionRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &handler{
handleFunc: h,
requireSession: true,
trustRequester: false,
requireMfa: true,
}
}
func ApiSessionRequiredMfa(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &handler{
handleFunc: h,
requireSession: true,
trustRequester: false,
requireMfa: false,
}
}
func ApiHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &handler{
handleFunc: h,
requireSession: false,
trustRequester: true,
requireMfa: false,
}
}
func ApiSessionRequiredTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
return &handler{
handleFunc: h,
requireSession: true,
trustRequester: true,
requireMfa: true,
}
}
type handler struct {
handleFunc func(*Context, http.ResponseWriter, *http.Request)
requireSession bool
trustRequester bool
requireMfa bool
}
func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
now := time.Now()
l4g.Debug("%v - %v", r.Method, r.URL.Path)
c := &Context{}
c.T, _ = utils.GetTranslationsAndLocale(w, r)
c.RequestId = model.NewId()
c.IpAddress = utils.GetIpAddress(r)
c.Params = ApiParamsFromRequest(r)
token := ""
isTokenFromQueryString := false
// Attempt to parse token out of the header
authHeader := r.Header.Get(model.HEADER_AUTH)
if len(authHeader) > 6 && strings.ToUpper(authHeader[0:6]) == model.HEADER_BEARER {
// Default session token
token = authHeader[7:]
} else if len(authHeader) > 5 && strings.ToLower(authHeader[0:5]) == model.HEADER_TOKEN {
// OAuth token
token = authHeader[6:]
}
// Attempt to parse the token from the cookie
if len(token) == 0 {
if cookie, err := r.Cookie(model.SESSION_COOKIE_TOKEN); err == nil {
token = cookie.Value
if h.requireSession && !h.trustRequester {
if r.Header.Get(model.HEADER_REQUESTED_WITH) != model.HEADER_REQUESTED_WITH_XML {
c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token+" Appears to be a CSRF attempt")
token = ""
}
}
}
}
// Attempt to parse token out of the query string
if len(token) == 0 {
token = r.URL.Query().Get("access_token")
isTokenFromQueryString = true
}
if utils.GetSiteURL() == "" {
protocol := app.GetProtocol(r)
c.SetSiteURL(protocol + "://" + r.Host)
}
w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v.%v", model.CurrentVersion, model.BuildNumber, utils.CfgHash))
if einterfaces.GetClusterInterface() != nil {
w.Header().Set(model.HEADER_CLUSTER_ID, einterfaces.GetClusterInterface().GetClusterId())
}
w.Header().Set("Content-Type", "application/json")
if r.Method == "GET" {
w.Header().Set("Expires", "0")
}
if len(token) != 0 {
session, err := app.GetSession(token)
if err != nil {
l4g.Error(utils.T("api.context.invalid_session.error"), err.Error())
c.RemoveSessionCookie(w, r)
if h.requireSession {
c.Err = model.NewLocAppError("ServeHTTP", "api.context.session_expired.app_error", nil, "token="+token)
c.Err.StatusCode = http.StatusUnauthorized
}
} else if !session.IsOAuth && isTokenFromQueryString {
c.Err = model.NewLocAppError("ServeHTTP", "api.context.token_provided.app_error", nil, "token="+token)
c.Err.StatusCode = http.StatusUnauthorized
} else {
c.Session = *session
}
}
c.Path = r.URL.Path
if c.Err == nil && h.requireSession {
c.SessionRequired()
}
if c.Err == nil && h.requireMfa {
c.MfaRequired()
}
if c.Err == nil {
h.handleFunc(c, w, r)
}
// Handle errors that have occured
if c.Err != nil {
c.Err.Translate(c.T)
c.Err.RequestId = c.RequestId
c.LogError(c.Err)
c.Err.Where = r.URL.Path
// Block out detailed error when not in developer mode
if !*utils.Cfg.ServiceSettings.EnableDeveloper {
c.Err.DetailedError = ""
}
w.WriteHeader(c.Err.StatusCode)
w.Write([]byte(c.Err.ToJson()))
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementHttpError()
}
}
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementHttpRequest()
if r.URL.Path != model.API_URL_SUFFIX+"/users/websocket" {
elapsed := float64(time.Since(now)) / float64(time.Second)
einterfaces.GetMetricsInterface().ObserveHttpRequestDuration(elapsed)
}
}
}
func (c *Context) LogAudit(extraInfo string) {
audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
if r := <-app.Srv.Store.Audit().Save(audit); r.Err != nil {
c.LogError(r.Err)
}
}
func (c *Context) LogAuditWithUserId(userId, extraInfo string) {
if len(c.Session.UserId) > 0 {
extraInfo = strings.TrimSpace(extraInfo + " session_user=" + c.Session.UserId)
}
audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
if r := <-app.Srv.Store.Audit().Save(audit); r.Err != nil {
c.LogError(r.Err)
}
}
func (c *Context) LogError(err *model.AppError) {
// filter out endless reconnects
if c.Path == "/api/v3/users/websocket" && err.StatusCode == 401 || err.Id == "web.check_browser_compatibility.app_error" {
c.LogDebug(err)
} else {
l4g.Error(utils.T("api.context.log.error"), c.Path, err.Where, err.StatusCode,
c.RequestId, c.Session.UserId, c.IpAddress, err.SystemMessage(utils.T), err.DetailedError)
}
}
func (c *Context) LogDebug(err *model.AppError) {
l4g.Debug(utils.T("api.context.log.error"), c.Path, err.Where, err.StatusCode,
c.RequestId, c.Session.UserId, c.IpAddress, err.SystemMessage(utils.T), err.DetailedError)
}
func (c *Context) IsSystemAdmin() bool {
return app.SessionHasPermissionTo(c.Session, model.PERMISSION_MANAGE_SYSTEM)
}
func (c *Context) SessionRequired() {
if len(c.Session.UserId) == 0 {
c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "UserRequired")
c.Err.StatusCode = http.StatusUnauthorized
return
}
}
func (c *Context) MfaRequired() {
// Must be licensed for MFA and have it configured for enforcement
if !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication || !*utils.Cfg.ServiceSettings.EnforceMultifactorAuthentication {
return
}
// OAuth integrations are excepted
if c.Session.IsOAuth {
return
}
if user, err := app.GetUser(c.Session.UserId); err != nil {
c.Err = model.NewLocAppError("", "api.context.session_expired.app_error", nil, "MfaRequired")
c.Err.StatusCode = http.StatusUnauthorized
return
} else {
// Only required for email and ldap accounts
if user.AuthService != "" &&
user.AuthService != model.USER_AUTH_SERVICE_EMAIL &&
user.AuthService != model.USER_AUTH_SERVICE_LDAP {
return
}
if !user.MfaActive {
c.Err = model.NewLocAppError("", "api.context.mfa_required.app_error", nil, "MfaRequired")
c.Err.StatusCode = http.StatusUnauthorized
return
}
}
}
func (c *Context) RemoveSessionCookie(w http.ResponseWriter, r *http.Request) {
cookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
Value: "",
Path: "/",
MaxAge: -1,
HttpOnly: true,
}
http.SetCookie(w, cookie)
}
func (c *Context) SetInvalidParam(parameter string) {
c.Err = NewInvalidParamError(parameter)
}
func (c *Context) SetInvalidUrlParam(parameter string) {
c.Err = NewInvalidUrlParamError(parameter)
}
func NewInvalidParamError(parameter string) *model.AppError {
err := model.NewLocAppError("Context", "api.context.invalid_body_param.app_error", map[string]interface{}{"Name": parameter}, "")
err.StatusCode = http.StatusBadRequest
return err
}
func NewInvalidUrlParamError(parameter string) *model.AppError {
err := model.NewLocAppError("Context", "api.context.invalid_url_param.app_error", map[string]interface{}{"Name": parameter}, "")
err.StatusCode = http.StatusBadRequest
return err
}
func (c *Context) SetPermissionError(permission *model.Permission) {
c.Err = model.NewLocAppError("Permissions", "api.context.permissions.app_error", nil, "userId="+c.Session.UserId+", "+"permission="+permission.Id)
c.Err.StatusCode = http.StatusForbidden
}
func (c *Context) SetSiteURL(url string) {
c.siteURL = strings.TrimRight(url, "/")
}
func (c *Context) GetSiteURL() string {
return c.siteURL
}
func (c *Context) RequireUserId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.UserId) != 26 {
c.SetInvalidUrlParam("user_id")
}
return c
}
func (c *Context) RequireTeamId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.TeamId) != 26 {
c.SetInvalidUrlParam("team_id")
}
return c
}
func (c *Context) RequireChannelId() *Context {
if c.Err != nil {
return c
}
if len(c.Params.ChannelId) != 26 {
c.SetInvalidUrlParam("channel_id")
}
return c
}

61
api4/params.go Обычный файл
Просмотреть файл

@@ -0,0 +1,61 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api4
import (
"net/http"
"github.com/gorilla/mux"
)
type ApiParams struct {
UserId string
TeamId string
ChannelId string
PostId string
FileId string
CommandId string
HookId string
EmojiId string
}
func ApiParamsFromRequest(r *http.Request) *ApiParams {
params := &ApiParams{}
props := mux.Vars(r)
if val, ok := props["user_id"]; ok {
params.UserId = val
}
if val, ok := props["team_id"]; ok {
params.TeamId = val
}
if val, ok := props["channel_id"]; ok {
params.ChannelId = val
}
if val, ok := props["post_id"]; ok {
params.PostId = val
}
if val, ok := props["file_id"]; ok {
params.FileId = val
}
if val, ok := props["command_id"]; ok {
params.CommandId = val
}
if val, ok := props["hook_id"]; ok {
params.HookId = val
}
if val, ok := props["emoji_id"]; ok {
params.EmojiId = val
}
return params
}

169
api4/user.go Обычный файл
Просмотреть файл

@@ -0,0 +1,169 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api4
import (
"net/http"
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/app"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
func InitUser() {
l4g.Debug(utils.T("api.user.init.debug"))
BaseRoutes.Users.Handle("", ApiHandler(createUser)).Methods("POST")
BaseRoutes.User.Handle("", ApiSessionRequired(getUser)).Methods("GET")
BaseRoutes.User.Handle("", ApiSessionRequired(updateUser)).Methods("PUT")
BaseRoutes.Users.Handle("/login", ApiHandler(login)).Methods("POST")
BaseRoutes.Users.Handle("/logout", ApiHandler(logout)).Methods("POST")
}
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
user := model.UserFromJson(r.Body)
if user == nil {
c.SetInvalidParam("user")
return
}
hash := r.URL.Query().Get("h")
inviteId := r.URL.Query().Get("iid")
// No permission check required
var ruser *model.User
var err *model.AppError
if len(hash) > 0 {
ruser, err = app.CreateUserWithHash(user, hash, r.URL.Query().Get("d"))
} else if len(inviteId) > 0 {
ruser, err = app.CreateUserWithInviteId(user, inviteId, c.GetSiteURL())
} else {
ruser, err = app.CreateUserFromSignup(user, c.GetSiteURL())
}
if err != nil {
c.Err = err
return
}
w.WriteHeader(http.StatusCreated)
w.Write([]byte(ruser.ToJson()))
}
func getUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
// No permission check required
var user *model.User
var err *model.AppError
if user, err = app.GetUser(c.Params.UserId); err != nil {
c.Err = err
return
}
etag := user.Etag(utils.Cfg.PrivacySettings.ShowFullName, utils.Cfg.PrivacySettings.ShowEmailAddress)
if HandleEtag(etag, "Get User", w, r) {
return
} else {
app.SanitizeProfile(user, c.IsSystemAdmin())
w.Header().Set(model.HEADER_ETAG_SERVER, etag)
w.Write([]byte(user.ToJson()))
return
}
}
func updateUser(c *Context, w http.ResponseWriter, r *http.Request) {
c.RequireUserId()
if c.Err != nil {
return
}
user := model.UserFromJson(r.Body)
if user == nil {
c.SetInvalidParam("user")
return
}
if !app.SessionHasPermissionToUser(c.Session, user.Id) {
c.SetPermissionError(model.PERMISSION_EDIT_OTHER_USERS)
return
}
if ruser, err := app.UpdateUserAsUser(user, c.GetSiteURL(), c.IsSystemAdmin()); err != nil {
c.Err = err
return
} else {
c.LogAudit("")
w.Write([]byte(ruser.ToJson()))
}
}
func login(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
id := props["id"]
loginId := props["login_id"]
password := props["password"]
mfaToken := props["token"]
deviceId := props["device_id"]
ldapOnly := props["ldap_only"] == "true"
c.LogAuditWithUserId(id, "attempt - login_id="+loginId)
user, err := app.AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId, ldapOnly)
if err != nil {
c.LogAuditWithUserId(id, "failure - login_id="+loginId)
c.Err = err
return
}
c.LogAuditWithUserId(user.Id, "authenticated")
var session *model.Session
session, err = app.DoLogin(w, r, user, deviceId)
if err != nil {
c.Err = err
return
}
c.LogAuditWithUserId(user.Id, "success")
c.Session = *session
user.Sanitize(map[string]bool{})
w.Write([]byte(user.ToJson()))
}
func logout(c *Context, w http.ResponseWriter, r *http.Request) {
data := make(map[string]string)
data["user_id"] = c.Session.UserId
Logout(c, w, r)
if c.Err == nil {
w.Write([]byte(model.MapToJson(data)))
}
}
func Logout(c *Context, w http.ResponseWriter, r *http.Request) {
c.LogAudit("")
c.RemoveSessionCookie(w, r)
if c.Session.Id != "" {
if err := app.RevokeSessionById(c.Session.Id); err != nil {
c.Err = err
return
}
}
ReturnStatusOK(w)
}

186
api4/user_test.go Обычный файл
Просмотреть файл

@@ -0,0 +1,186 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api4
import (
"net/http"
"strconv"
"testing"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
)
func TestCreateUser(t *testing.T) {
th := Setup()
Client := th.Client
user := model.User{Email: GenerateTestEmail(), Nickname: "Corey Hulen", Password: "hello1", Username: GenerateTestUsername(), Roles: model.ROLE_SYSTEM_ADMIN.Id + " " + model.ROLE_SYSTEM_USER.Id}
ruser, resp := Client.CreateUser(&user)
CheckNoError(t, resp)
Client.Login(user.Email, user.Password)
if ruser.Nickname != user.Nickname {
t.Fatal("nickname didn't match")
}
if ruser.Roles != model.ROLE_SYSTEM_USER.Id {
t.Fatal("did not clear roles")
}
CheckUserSanitization(t, ruser)
_, resp = Client.CreateUser(ruser)
CheckBadRequestStatus(t, resp)
ruser.Id = ""
ruser.Username = GenerateTestUsername()
ruser.Password = "passwd1"
_, resp = Client.CreateUser(ruser)
CheckErrorMessage(t, resp, "An account with that email already exists.")
CheckBadRequestStatus(t, resp)
ruser.Email = GenerateTestEmail()
ruser.Username = user.Username
_, resp = Client.CreateUser(ruser)
CheckErrorMessage(t, resp, "An account with that username already exists.")
CheckBadRequestStatus(t, resp)
ruser.Email = ""
_, resp = Client.CreateUser(ruser)
CheckErrorMessage(t, resp, "Invalid email")
CheckBadRequestStatus(t, resp)
if r, err := Client.DoApiPost("/users", "garbage"); err == nil {
t.Fatal("should have errored")
} else {
if r.StatusCode != http.StatusBadRequest {
t.Log("actual: " + strconv.Itoa(r.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusBadRequest))
t.Fatal("wrong status code")
}
}
}
func TestGetUser(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
Client := th.Client
user := th.CreateUser()
ruser, resp := Client.GetUser(user.Id, "")
CheckNoError(t, resp)
CheckUserSanitization(t, ruser)
if ruser.Email != user.Email {
t.Fatal("emails did not match")
}
ruser, resp = Client.GetUser(user.Id, resp.Etag)
CheckEtag(t, ruser, resp)
_, resp = Client.GetUser("junk", "")
CheckBadRequestStatus(t, resp)
_, resp = Client.GetUser(model.NewId(), "")
CheckNotFoundStatus(t, resp)
// Check against privacy config settings
emailPrivacy := utils.Cfg.PrivacySettings.ShowEmailAddress
namePrivacy := utils.Cfg.PrivacySettings.ShowFullName
defer func() {
utils.Cfg.PrivacySettings.ShowEmailAddress = emailPrivacy
utils.Cfg.PrivacySettings.ShowFullName = namePrivacy
}()
utils.Cfg.PrivacySettings.ShowEmailAddress = false
utils.Cfg.PrivacySettings.ShowFullName = false
ruser, resp = Client.GetUser(user.Id, "")
CheckNoError(t, resp)
if ruser.Email != "" {
t.Fatal("email should be blank")
}
if ruser.FirstName != "" {
t.Fatal("first name should be blank")
}
if ruser.LastName != "" {
t.Fatal("last name should be blank")
}
Client.Logout()
_, resp = Client.GetUser(user.Id, "")
CheckUnauthorizedStatus(t, resp)
// System admins should ignore privacy settings
th.LoginSystemAdmin()
ruser, resp = Client.GetUser(user.Id, resp.Etag)
if ruser.Email == "" {
t.Fatal("email should not be blank")
}
if ruser.FirstName == "" {
t.Fatal("first name should not be blank")
}
if ruser.LastName == "" {
t.Fatal("last name should not be blank")
}
}
func TestUpdateUser(t *testing.T) {
th := Setup().InitBasic().InitSystemAdmin()
Client := th.Client
user := th.CreateUser()
Client.Login(user.Email, user.Password)
user.Nickname = "Joram Wilander"
user.Roles = model.ROLE_SYSTEM_ADMIN.Id
user.LastPasswordUpdate = 123
ruser, resp := Client.UpdateUser(user)
CheckNoError(t, resp)
CheckUserSanitization(t, ruser)
if ruser.Nickname != "Joram Wilander" {
t.Fatal("Nickname did not update properly")
}
if ruser.Roles != model.ROLE_SYSTEM_USER.Id {
t.Fatal("Roles should not have updated")
}
if ruser.LastPasswordUpdate == 123 {
t.Fatal("LastPasswordUpdate should not have updated")
}
ruser.Id = "junk"
_, resp = Client.UpdateUser(ruser)
CheckBadRequestStatus(t, resp)
ruser.Id = model.NewId()
_, resp = Client.UpdateUser(ruser)
CheckForbiddenStatus(t, resp)
if r, err := Client.DoApiPut("/users/"+ruser.Id, "garbage"); err == nil {
t.Fatal("should have errored")
} else {
if r.StatusCode != http.StatusBadRequest {
t.Log("actual: " + strconv.Itoa(r.StatusCode))
t.Log("expected: " + strconv.Itoa(http.StatusBadRequest))
t.Fatal("wrong status code")
}
}
Client.Logout()
_, resp = Client.UpdateUser(user)
CheckUnauthorizedStatus(t, resp)
th.LoginBasic()
_, resp = Client.UpdateUser(user)
CheckForbiddenStatus(t, resp)
th.LoginSystemAdmin()
_, resp = Client.UpdateUser(user)
CheckNoError(t, resp)
}

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

@@ -1,10 +1,9 @@
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package api
package app
import (
"github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
@@ -13,8 +12,8 @@ import (
"strings"
)
func checkPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
if err := checkUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil {
func CheckPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
if err := CheckUserAdditionalAuthenticationCriteria(user, mfaToken); err != nil {
return err
}
@@ -40,13 +39,13 @@ func doubleCheckPassword(user *model.User, password string) *model.AppError {
func checkUserPassword(user *model.User, password string) *model.AppError {
if !model.ComparePassword(user.Password, password) {
if result := <-app.Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil {
if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, user.FailedAttempts+1); result.Err != nil {
return result.Err
}
return model.NewLocAppError("checkUserPassword", "api.user.check_user_password.invalid.app_error", nil, "user_id="+user.Id)
} else {
if result := <-app.Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil {
if result := <-Srv.Store.User().UpdateFailedPasswordAttempts(user.Id, 0); result.Err != nil {
return result.Err
}
@@ -71,7 +70,7 @@ func checkLdapUserPasswordAndAllCriteria(ldapId *string, password string, mfaTok
user = ldapUser
}
if err := checkUserMfa(user, mfaToken); err != nil {
if err := CheckUserMfa(user, mfaToken); err != nil {
return nil, err
}
@@ -83,8 +82,8 @@ func checkLdapUserPasswordAndAllCriteria(ldapId *string, password string, mfaTok
return user, nil
}
func checkUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
if err := checkUserMfa(user, mfaToken); err != nil {
func CheckUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string) *model.AppError {
if err := CheckUserMfa(user, mfaToken); err != nil {
return err
}
@@ -103,7 +102,7 @@ func checkUserAdditionalAuthenticationCriteria(user *model.User, mfaToken string
return nil
}
func checkUserMfa(user *model.User, token string) *model.AppError {
func CheckUserMfa(user *model.User, token string) *model.AppError {
if !user.MfaActive || !utils.IsLicensed || !*utils.License.Features.MFA || !*utils.Cfg.ServiceSettings.EnableMultifactorAuthentication {
return nil
}
@@ -168,7 +167,7 @@ func authenticateUser(user *model.User, password, mfaToken string) (*model.User,
err.StatusCode = http.StatusBadRequest
return user, err
} else {
if err := checkPasswordAndAllCriteria(user, password, mfaToken); err != nil {
if err := CheckPasswordAndAllCriteria(user, password, mfaToken); err != nil {
err.StatusCode = http.StatusUnauthorized
return user, err
} else {

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

@@ -5,13 +5,13 @@ package app
import (
"bytes"
_ "image/gif"
"crypto/sha256"
"encoding/base64"
"fmt"
"image"
"image/color"
"image/draw"
_ "image/gif"
"image/jpeg"
"io"
"io/ioutil"
@@ -24,9 +24,9 @@ import (
"sync"
l4g "github.com/alecthomas/log4go"
"github.com/disintegration/imaging"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"github.com/disintegration/imaging"
s3 "github.com/minio/minio-go"
"github.com/rwcarlsen/goexif/exif"
_ "golang.org/x/image/bmp"
@@ -359,7 +359,7 @@ func MigrateFilenamesToFileInfos(post *model.Post) []*model.FileInfo {
func GeneratePublicLink(siteURL string, info *model.FileInfo) string {
hash := GeneratePublicLinkHash(info.Id, *utils.Cfg.FileSettings.PublicLinkSalt)
return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX, info.Id, hash)
return fmt.Sprintf("%s%s/public/files/%v/get?h=%s", siteURL, model.API_URL_SUFFIX_V3, info.Id, hash)
}
func GeneratePublicLinkHash(fileId, salt string) string {

136
app/login.go Обычный файл
Просмотреть файл

@@ -0,0 +1,136 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package app
import (
"fmt"
"net/http"
"time"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
)
func AuthenticateUserForLogin(id, loginId, password, mfaToken, deviceId string, ldapOnly bool) (*model.User, *model.AppError) {
if len(password) == 0 {
err := model.NewLocAppError("AuthenticateUserForLogin", "api.user.login.blank_pwd.app_error", nil, "")
err.StatusCode = http.StatusBadRequest
return nil, err
}
var user *model.User
var err *model.AppError
if len(id) != 0 {
if user, err = GetUser(id); err != nil {
err.StatusCode = http.StatusBadRequest
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementLoginFail()
}
return nil, err
}
} else {
if user, err = GetUserForLogin(loginId, ldapOnly); err != nil {
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementLoginFail()
}
return nil, err
}
}
// and then authenticate them
if user, err = authenticateUser(user, password, mfaToken); err != nil {
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementLoginFail()
}
return nil, err
}
if einterfaces.GetMetricsInterface() != nil {
einterfaces.GetMetricsInterface().IncrementLogin()
}
return user, nil
}
func DoLogin(w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) (*model.Session, *model.AppError) {
session := &model.Session{UserId: user.Id, Roles: user.GetRawRoles(), DeviceId: deviceId, IsOAuth: false}
maxAge := *utils.Cfg.ServiceSettings.SessionLengthWebInDays * 60 * 60 * 24
if len(deviceId) > 0 {
session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthMobileInDays)
// A special case where we logout of all other sessions with the same Id
if err := RevokeSessionsForDeviceId(user.Id, deviceId, ""); err != nil {
err.StatusCode = http.StatusInternalServerError
return nil, err
}
} else {
session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthWebInDays)
}
ua := user_agent.New(r.UserAgent())
plat := ua.Platform()
if plat == "" {
plat = "unknown"
}
os := ua.OS()
if os == "" {
os = "unknown"
}
bname, bversion := ua.Browser()
if bname == "" {
bname = "unknown"
}
if bversion == "" {
bversion = "0.0"
}
session.AddProp(model.SESSION_PROP_PLATFORM, plat)
session.AddProp(model.SESSION_PROP_OS, os)
session.AddProp(model.SESSION_PROP_BROWSER, fmt.Sprintf("%v/%v", bname, bversion))
var err *model.AppError
if session, err = CreateSession(session); err != nil {
err.StatusCode = http.StatusInternalServerError
return nil, err
}
w.Header().Set(model.HEADER_TOKEN, session.Token)
secure := false
if GetProtocol(r) == "https" {
secure = true
}
expiresAt := time.Unix(model.GetMillis()/1000+int64(maxAge), 0)
sessionCookie := &http.Cookie{
Name: model.SESSION_COOKIE_TOKEN,
Value: session.Token,
Path: "/",
MaxAge: maxAge,
Expires: expiresAt,
HttpOnly: true,
Secure: secure,
}
http.SetCookie(w, sessionCookie)
return session, nil
}
func GetProtocol(r *http.Request) string {
if r.Header.Get(model.HEADER_FORWARDED_PROTO) == "https" {
return "https"
} else {
return "http"
}
}

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

@@ -31,6 +31,10 @@ import (
)
func CreateUserWithHash(user *model.User, hash string, data string) (*model.User, *model.AppError) {
if err := IsUserSignUpAllowed(); err != nil {
return nil, err
}
props := model.MapFromJson(strings.NewReader(data))
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
@@ -69,6 +73,10 @@ func CreateUserWithHash(user *model.User, hash string, data string) (*model.User
}
func CreateUserWithInviteId(user *model.User, inviteId string, siteURL string) (*model.User, *model.AppError) {
if err := IsUserSignUpAllowed(); err != nil {
return nil, err
}
var team *model.Team
if result := <-Srv.Store.Team().GetByInviteId(inviteId); result.Err != nil {
return nil, result.Err
@@ -76,6 +84,8 @@ func CreateUserWithInviteId(user *model.User, inviteId string, siteURL string) (
team = result.Data.(*model.Team)
}
user.EmailVerified = false
var ruser *model.User
var err *model.AppError
if ruser, err = CreateUser(user); err != nil {
@@ -95,6 +105,40 @@ func CreateUserWithInviteId(user *model.User, inviteId string, siteURL string) (
return ruser, nil
}
func CreateUserFromSignup(user *model.User, siteURL string) (*model.User, *model.AppError) {
if err := IsUserSignUpAllowed(); err != nil {
return nil, err
}
if !IsFirstUserAccount() && !*utils.Cfg.TeamSettings.EnableOpenServer {
err := model.NewLocAppError("CreateUserFromSignup", "api.user.create_user.no_open_server", nil, "email="+user.Email)
err.StatusCode = http.StatusForbidden
return nil, err
}
user.EmailVerified = false
ruser, err := CreateUser(user)
if err != nil {
return nil, err
}
if err := SendWelcomeEmail(ruser.Id, ruser.Email, ruser.EmailVerified, ruser.Locale, siteURL); err != nil {
l4g.Error(err.Error())
}
return ruser, nil
}
func IsUserSignUpAllowed() *model.AppError {
if !utils.Cfg.EmailSettings.EnableSignUpWithEmail || !utils.Cfg.TeamSettings.EnableUserCreation {
err := model.NewLocAppError("IsUserSignUpAllowed", "api.user.create_user.signup_email_disabled.app_error", nil, "")
err.StatusCode = http.StatusNotImplemented
return err
}
return nil
}
func IsFirstUserAccount() bool {
if SessionCacheLength() == 0 {
if cr := <-Srv.Store.User().GetTotalUsersCount(); cr.Err != nil {
@@ -575,6 +619,43 @@ func SetProfileImage(userId string, imageData *multipart.FileHeader) *model.AppE
return nil
}
func UpdatePasswordAsUser(userId, currentPassword, newPassword, siteURL string) *model.AppError {
var user *model.User
var err *model.AppError
if user, err = GetUser(userId); err != nil {
return err
}
if user == nil {
err = model.NewLocAppError("updatePassword", "api.user.update_password.valid_account.app_error", nil, "")
err.StatusCode = http.StatusBadRequest
return err
}
if user.AuthData != nil && *user.AuthData != "" {
err = model.NewLocAppError("updatePassword", "api.user.update_password.oauth.app_error", nil, "auth_service="+user.AuthService)
err.StatusCode = http.StatusBadRequest
return err
}
if err := doubleCheckPassword(user, currentPassword); err != nil {
if err.Id == "api.user.check_user_password.invalid.app_error" {
err = model.NewLocAppError("updatePassword", "api.user.update_password.incorrect.app_error", nil, "")
}
err.StatusCode = http.StatusForbidden
return err
}
T := utils.GetUserTranslations(user.Locale)
if err := UpdatePasswordSendEmail(user, newPassword, T("api.user.update_password.menu"), siteURL); err != nil {
return err
}
return nil
}
func UpdateActiveNoLdap(userId string, active bool) (*model.User, *model.AppError) {
var user *model.User
var err *model.AppError
@@ -624,6 +705,33 @@ func UpdateActive(user *model.User, active bool) (*model.User, *model.AppError)
}
}
func SanitizeProfile(user *model.User, asAdmin bool) {
options := utils.Cfg.GetSanitizeOptions()
if asAdmin {
options["email"] = true
options["fullname"] = true
options["authservice"] = true
}
user.SanitizeProfile(options)
}
func UpdateUserAsUser(user *model.User, siteURL string, asAdmin bool) (*model.User, *model.AppError) {
updatedUser, err := UpdateUser(user, siteURL)
if err != nil {
return nil, err
}
SanitizeProfile(updatedUser, asAdmin)
omitUsers := make(map[string]bool, 1)
omitUsers[updatedUser.Id] = true
message := model.NewWebSocketEvent(model.WEBSOCKET_EVENT_USER_UPDATED, "", "", "", omitUsers)
message.Add("user", updatedUser)
go Publish(message)
return updatedUser, nil
}
func UpdateUser(user *model.User, siteURL string) (*model.User, *model.AppError) {
if result := <-Srv.Store.User().Update(user, false); result.Err != nil {
return nil, result.Err

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

@@ -16,6 +16,7 @@ import (
l4g "github.com/alecthomas/log4go"
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/api4"
"github.com/mattermost/platform/app"
"github.com/mattermost/platform/einterfaces"
"github.com/mattermost/platform/manualtesting"
@@ -73,6 +74,7 @@ func runServer(configFileLocation string) {
app.NewServer()
app.InitStores()
api.InitRouter()
api4.InitApi(false)
api.InitApi()
web.InitWeb()

2
glide.lock сгенерированный
Просмотреть файл

@@ -40,7 +40,7 @@ imports:
- name: github.com/gorilla/handlers
version: e1b2144f2167de0e1042d1d35e5cba5119d4fb5d
- name: github.com/gorilla/mux
version: 757bef944d0f21880861c2dd9c871ca543023cba
version: 34dda716af12ba79ed40ce1f66b102ff75dc3411
- name: github.com/gorilla/websocket
version: e8f0f8aaa98dfb6586cbdf2978d511e3199a960a
- name: github.com/hashicorp/golang-lru

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

@@ -24,7 +24,7 @@ import:
- package: github.com/gorilla/handlers
version: e1b2144f2167de0e1042d1d35e5cba5119d4fb5d
- package: github.com/gorilla/mux
version: 757bef944d0f21880861c2dd9c871ca543023cba
version: 34dda716af12ba79ed40ce1f66b102ff75dc3411
- package: github.com/gorilla/websocket
version: e8f0f8aaa98dfb6586cbdf2978d511e3199a960a
- package: github.com/lib/pq

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

@@ -799,6 +799,14 @@
"id": "api.context.invalid_param.app_error",
"translation": "Invalid {{.Name}} parameter"
},
{
"id": "api.context.invalid_body_param.app_error",
"translation": "Invalid or missing {{.Name}} in request body"
},
{
"id": "api.context.invalid_url_param.app_error",
"translation": "Invalid or missing {{.Name}} parameter in request URL"
},
{
"id": "api.context.invalid_session.error",
"translation": "Invalid session err=%v"
@@ -5245,7 +5253,7 @@
},
{
"id": "store.sql_user.missing_account.const",
"translation": "We couldn't find an existing account matching your email address for this team. This team may require an invite from the team owner to join."
"translation": "We couldn't find the user."
},
{
"id": "store.sql_user.permanent_delete.app_error",

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

@@ -40,7 +40,8 @@ const (
API_URL_SUFFIX_V1 = "/api/v1"
API_URL_SUFFIX_V3 = "/api/v3"
API_URL_SUFFIX = API_URL_SUFFIX_V3
API_URL_SUFFIX_V4 = "/api/v4"
API_URL_SUFFIX = API_URL_SUFFIX_V4
)
type Result struct {
@@ -71,7 +72,7 @@ type Client struct {
// NewClient constructs a new client with convienence methods for talking to
// the server.
func NewClient(url string) *Client {
return &Client{url, url + API_URL_SUFFIX, &http.Client{}, "", "", "", "", "", ""}
return &Client{url, url + API_URL_SUFFIX_V3, &http.Client{}, "", "", "", "", "", ""}
}
func closeBody(r *http.Response) {

222
model/client4.go Обычный файл
Просмотреть файл

@@ -0,0 +1,222 @@
// Copyright (c) 2017 Mattermost, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"fmt"
"net/http"
"strings"
)
type Response struct {
StatusCode int
Error *AppError
RequestId string
Etag string
ServerVersion string
}
type Client4 struct {
Url string // The location of the server, for example "http://localhost:8065"
ApiUrl string // The api location of the server, for example "http://localhost:8065/api/v4"
HttpClient *http.Client // The http client
AuthToken string
AuthType string
}
func NewAPIv4Client(url string) *Client4 {
return &Client4{url, url + API_URL_SUFFIX, &http.Client{}, "", ""}
}
func BuildResponse(r *http.Response) *Response {
return &Response{
StatusCode: r.StatusCode,
RequestId: r.Header.Get(HEADER_REQUEST_ID),
Etag: r.Header.Get(HEADER_ETAG_SERVER),
ServerVersion: r.Header.Get(HEADER_VERSION_ID),
}
}
func (c *Client4) SetOAuthToken(token string) {
c.AuthToken = token
c.AuthType = HEADER_TOKEN
}
func (c *Client4) ClearOAuthToken() {
c.AuthToken = ""
c.AuthType = HEADER_BEARER
}
func (c *Client4) GetUsersRoute() string {
return fmt.Sprintf("/users")
}
func (c *Client4) GetUserRoute(userId string) string {
return fmt.Sprintf(c.GetUsersRoute()+"/%v", userId)
}
func (c *Client4) GetTeamsRoute() string {
return fmt.Sprintf("/teams")
}
func (c *Client4) GetTeamRoute(teamId string) string {
return fmt.Sprintf(c.GetTeamsRoute()+"/%v", teamId)
}
func (c *Client4) DoApiGet(url string, etag string) (*http.Response, *AppError) {
return c.DoApiRequest(http.MethodGet, url, "", etag)
}
func (c *Client4) DoApiPost(url string, data string) (*http.Response, *AppError) {
return c.DoApiRequest(http.MethodPost, url, data, "")
}
func (c *Client4) DoApiPut(url string, data string) (*http.Response, *AppError) {
return c.DoApiRequest(http.MethodPut, url, data, "")
}
func (c *Client4) DoApiDelete(url string, data string) (*http.Response, *AppError) {
return c.DoApiRequest(http.MethodDelete, url, "", "")
}
func (c *Client4) DoApiRequest(method, url, data, etag string) (*http.Response, *AppError) {
rq, _ := http.NewRequest(method, c.ApiUrl+url, strings.NewReader(data))
rq.Close = true
if len(etag) > 0 {
rq.Header.Set(HEADER_ETAG_CLIENT, etag)
}
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
if rp, err := c.HttpClient.Do(rq); err != nil {
return nil, NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
} else if rp.StatusCode == 304 {
return rp, nil
} else if rp.StatusCode >= 300 {
defer closeBody(rp)
return rp, AppErrorFromJson(rp.Body)
} else {
return rp, nil
}
}
// CheckStatusOK is a convenience function for checking the standard OK response
// from the web service.
func CheckStatusOK(r *http.Response) bool {
m := MapFromJson(r.Body)
defer closeBody(r)
if m != nil && m[STATUS] == STATUS_OK {
return true
}
return false
}
// Authentication Section
// LoginById authenticates a user by user id and password.
func (c *Client4) LoginById(id string, password string) (*User, *Response) {
m := make(map[string]string)
m["id"] = id
m["password"] = password
return c.login(m)
}
// Login authenticates a user by login id, which can be username, email or some sort
// of SSO identifier based on server configuration, and a password.
func (c *Client4) Login(loginId string, password string) (*User, *Response) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
return c.login(m)
}
// LoginByLdap authenticates a user by LDAP id and password.
func (c *Client4) LoginByLdap(loginId string, password string) (*User, *Response) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
m["ldap_only"] = "true"
return c.login(m)
}
// LoginWithDevice authenticates a user by login id (username, email or some sort
// of SSO identifier based on configuration), password and attaches a device id to
// the session.
func (c *Client4) LoginWithDevice(loginId string, password string, deviceId string) (*User, *Response) {
m := make(map[string]string)
m["login_id"] = loginId
m["password"] = password
m["device_id"] = deviceId
return c.login(m)
}
func (c *Client4) login(m map[string]string) (*User, *Response) {
if r, err := c.DoApiPost("/users/login", MapToJson(m)); err != nil {
return nil, &Response{StatusCode: r.StatusCode, Error: err}
} else {
c.AuthToken = r.Header.Get(HEADER_TOKEN)
c.AuthType = HEADER_BEARER
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
}
// Logout terminates the current user's session.
func (c *Client4) Logout() (bool, *Response) {
if r, err := c.DoApiPost("/users/logout", ""); err != nil {
return false, &Response{StatusCode: r.StatusCode, Error: err}
} else {
c.AuthToken = ""
c.AuthType = HEADER_BEARER
defer closeBody(r)
return CheckStatusOK(r), BuildResponse(r)
}
}
// User Section
// CreateUser creates a user in the system based on the provided user struct.
func (c *Client4) CreateUser(user *User) (*User, *Response) {
if r, err := c.DoApiPost(c.GetUsersRoute(), user.ToJson()); err != nil {
return nil, &Response{StatusCode: r.StatusCode, Error: err}
} else {
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
}
// GetUser returns a user based on the provided user id string.
func (c *Client4) GetUser(userId, etag string) (*User, *Response) {
if r, err := c.DoApiGet(c.GetUserRoute(userId), etag); err != nil {
return nil, &Response{StatusCode: r.StatusCode, Error: err}
} else {
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
}
// UpdateUser updates a user in the system based on the provided user struct.
func (c *Client4) UpdateUser(user *User) (*User, *Response) {
if r, err := c.DoApiPut(c.GetUserRoute(user.Id), user.ToJson()); err != nil {
return nil, &Response{StatusCode: r.StatusCode, Error: err}
} else {
defer closeBody(r)
return UserFromJson(r.Body), BuildResponse(r)
}
}
// Team Section
// to be filled in..
// Channel Section
// to be filled in..
// Post Section
// to be filled in..

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

@@ -7,6 +7,7 @@ import (
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"strings"
"unicode/utf8"
@@ -56,51 +57,51 @@ type User struct {
func (u *User) IsValid() *AppError {
if len(u.Id) != 26 {
return NewLocAppError("User.IsValid", "model.user.is_valid.id.app_error", nil, "")
return NewAppError("User.IsValid", "model.user.is_valid.id.app_error", nil, "", http.StatusBadRequest)
}
if u.CreateAt == 0 {
return NewLocAppError("User.IsValid", "model.user.is_valid.create_at.app_error", nil, "user_id="+u.Id)
return NewAppError("User.IsValid", "model.user.is_valid.create_at.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if u.UpdateAt == 0 {
return NewLocAppError("User.IsValid", "model.user.is_valid.update_at.app_error", nil, "user_id="+u.Id)
return NewAppError("User.IsValid", "model.user.is_valid.update_at.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if !IsValidUsername(u.Username) {
return NewLocAppError("User.IsValid", "model.user.is_valid.username.app_error", nil, "user_id="+u.Id)
return NewAppError("User.IsValid", "model.user.is_valid.username.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if len(u.Email) > 128 || len(u.Email) == 0 {
return NewLocAppError("User.IsValid", "model.user.is_valid.email.app_error", nil, "user_id="+u.Id)
return NewAppError("User.IsValid", "model.user.is_valid.email.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(u.Nickname) > 64 {
return NewLocAppError("User.IsValid", "model.user.is_valid.nickname.app_error", nil, "user_id="+u.Id)
return NewAppError("User.IsValid", "model.user.is_valid.nickname.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(u.Position) > 35 {
return NewLocAppError("User.IsValid", "model.user.is_valid.position.app_error", nil, "user_id="+u.Id)
return NewAppError("User.IsValid", "model.user.is_valid.position.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(u.FirstName) > 64 {
return NewLocAppError("User.IsValid", "model.user.is_valid.first_name.app_error", nil, "user_id="+u.Id)
return NewAppError("User.IsValid", "model.user.is_valid.first_name.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if utf8.RuneCountInString(u.LastName) > 64 {
return NewLocAppError("User.IsValid", "model.user.is_valid.last_name.app_error", nil, "user_id="+u.Id)
return NewAppError("User.IsValid", "model.user.is_valid.last_name.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if u.AuthData != nil && len(*u.AuthData) > 128 {
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data.app_error", nil, "user_id="+u.Id)
return NewAppError("User.IsValid", "model.user.is_valid.auth_data.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if u.AuthData != nil && len(*u.AuthData) > 0 && len(u.AuthService) == 0 {
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_type.app_error", nil, "user_id="+u.Id)
return NewAppError("User.IsValid", "model.user.is_valid.auth_data_type.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
if len(u.Password) > 0 && u.AuthData != nil && len(*u.AuthData) > 0 {
return NewLocAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id)
return NewAppError("User.IsValid", "model.user.is_valid.auth_data_pwd.app_error", nil, "user_id="+u.Id, http.StatusBadRequest)
}
return nil

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

@@ -93,6 +93,18 @@ func AppErrorFromJson(data io.Reader) *AppError {
}
}
func NewAppError(where string, id string, params map[string]interface{}, details string, status int) *AppError {
ap := &AppError{}
ap.Id = id
ap.params = params
ap.Message = id
ap.Where = where
ap.DetailedError = details
ap.StatusCode = status
ap.IsOAuth = false
return ap
}
func NewLocAppError(where string, id string, params map[string]interface{}, details string) *AppError {
ap := &AppError{}
ap.Id = id

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

@@ -26,14 +26,14 @@ type WebSocketClient struct {
// NewWebSocketClient constructs a new WebSocket client with convienence
// methods for talking to the server.
func NewWebSocketClient(url, authToken string) (*WebSocketClient, *AppError) {
conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX+"/users/websocket", nil)
conn, _, err := websocket.DefaultDialer.Dial(url+API_URL_SUFFIX_V3+"/users/websocket", nil)
if err != nil {
return nil, NewLocAppError("NewWebSocketClient", "model.websocket_client.connect_fail.app_error", nil, err.Error())
}
client := &WebSocketClient{
url,
url + API_URL_SUFFIX,
url + API_URL_SUFFIX_V3,
conn,
authToken,
1,

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

@@ -7,6 +7,7 @@ import (
"crypto/md5"
"database/sql"
"fmt"
"net/http"
"strconv"
"strings"
@@ -108,9 +109,9 @@ func (us SqlUserStore) Save(user *model.User) StoreChannel {
if err := us.GetMaster().Insert(user); err != nil {
if IsUniqueConstraintError(err.Error(), []string{"Email", "users_email_key", "idx_users_email_unique"}) {
result.Err = model.NewLocAppError("SqlUserStore.Save", "store.sql_user.save.email_exists.app_error", nil, "user_id="+user.Id+", "+err.Error())
result.Err = model.NewAppError("SqlUserStore.Save", "store.sql_user.save.email_exists.app_error", nil, "user_id="+user.Id+", "+err.Error(), http.StatusBadRequest)
} else if IsUniqueConstraintError(err.Error(), []string{"Username", "users_username_key", "idx_users_username_unique"}) {
result.Err = model.NewLocAppError("SqlUserStore.Save", "store.sql_user.save.username_exists.app_error", nil, "user_id="+user.Id+", "+err.Error())
result.Err = model.NewAppError("SqlUserStore.Save", "store.sql_user.save.username_exists.app_error", nil, "user_id="+user.Id+", "+err.Error(), http.StatusBadRequest)
} else {
result.Err = model.NewLocAppError("SqlUserStore.Save", "store.sql_user.save.app_error", nil, "user_id="+user.Id+", "+err.Error())
}
@@ -389,6 +390,7 @@ func (us SqlUserStore) Get(id string) StoreChannel {
result.Err = model.NewLocAppError("SqlUserStore.Get", "store.sql_user.get.app_error", nil, "user_id="+id+", "+err.Error())
} else if obj == nil {
result.Err = model.NewLocAppError("SqlUserStore.Get", MISSING_ACCOUNT_ERROR, nil, "user_id="+id)
result.Err.StatusCode = http.StatusNotFound
} else {
result.Data = obj.(*model.User)
}

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

@@ -4,8 +4,10 @@
package utils
import (
"github.com/mattermost/platform/model"
"net/http"
"strings"
"github.com/mattermost/platform/model"
)
func IsPasswordValid(password string) *model.AppError {
@@ -57,7 +59,7 @@ func IsPasswordValid(password string) *model.AppError {
}
if isError {
return model.NewLocAppError("User.IsValid", id+".app_error", map[string]interface{}{"Min": min}, "")
return model.NewAppError("User.IsValid", id+".app_error", map[string]interface{}{"Min": min}, "", http.StatusBadRequest)
}
return nil

58
vendor/github.com/gorilla/mux/mux_test.go сгенерированный поставляемый
Просмотреть файл

@@ -1017,6 +1017,9 @@ func TestBuildVarsFunc(t *testing.T) {
func TestSubRouter(t *testing.T) {
subrouter1 := new(Route).Host("{v1:[a-z]+}.google.com").Subrouter()
subrouter2 := new(Route).PathPrefix("/foo/{v1}").Subrouter()
subrouter3 := new(Route).PathPrefix("/foo").Subrouter()
subrouter4 := new(Route).PathPrefix("/foo/bar").Subrouter()
subrouter5 := new(Route).PathPrefix("/{category}").Subrouter()
tests := []routeTest{
{
@@ -1057,6 +1060,61 @@ func TestSubRouter(t *testing.T) {
pathTemplate: `/foo/{v1}/baz/{v2}`,
shouldMatch: false,
},
{
route: subrouter3.Path("/"),
request: newRequest("GET", "http://localhost/foo/"),
vars: map[string]string{},
host: "",
path: "/foo/",
pathTemplate: `/foo/`,
shouldMatch: true,
},
{
route: subrouter3.Path(""),
request: newRequest("GET", "http://localhost/foo"),
vars: map[string]string{},
host: "",
path: "/foo",
pathTemplate: `/foo`,
shouldMatch: true,
},
{
route: subrouter4.Path("/"),
request: newRequest("GET", "http://localhost/foo/bar/"),
vars: map[string]string{},
host: "",
path: "/foo/bar/",
pathTemplate: `/foo/bar/`,
shouldMatch: true,
},
{
route: subrouter4.Path(""),
request: newRequest("GET", "http://localhost/foo/bar"),
vars: map[string]string{},
host: "",
path: "/foo/bar",
pathTemplate: `/foo/bar`,
shouldMatch: true,
},
{
route: subrouter5.Path("/"),
request: newRequest("GET", "http://localhost/baz/"),
vars: map[string]string{"category": "baz"},
host: "",
path: "/baz/",
pathTemplate: `/{category}/`,
shouldMatch: true,
},
{
route: subrouter5.Path(""),
request: newRequest("GET", "http://localhost/baz"),
vars: map[string]string{"category": "baz"},
host: "",
path: "/baz",
pathTemplate: `/{category}`,
shouldMatch: true,
},
}
for _, test := range tests {

2
vendor/github.com/gorilla/mux/route.go сгенерированный поставляемый
Просмотреть файл

@@ -153,7 +153,7 @@ func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery
}
r.regexp = r.getRegexpGroup()
if !matchHost && !matchQuery {
if len(tpl) == 0 || tpl[0] != '/' {
if tpl == "/" && (len(tpl) == 0 || tpl[0] != '/') {
return fmt.Errorf("mux: path must start with a slash, got %q", tpl)
}
if r.regexp.path != nil {