summaryrefslogtreecommitdiffstats
path: root/api4
diff options
context:
space:
mode:
Diffstat (limited to 'api4')
-rw-r--r--api4/api.go189
-rw-r--r--api4/apitestlib.go211
-rw-r--r--api4/context.go360
-rw-r--r--api4/params.go61
-rw-r--r--api4/user.go169
-rw-r--r--api4/user_test.go186
6 files changed, 1176 insertions, 0 deletions
diff --git a/api4/api.go b/api4/api.go
new file mode 100644
index 000000000..9314bb616
--- /dev/null
+++ b/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)))
+}
diff --git a/api4/apitestlib.go b/api4/apitestlib.go
new file mode 100644
index 000000000..05e8d76fe
--- /dev/null
+++ b/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")
+ }
+}
diff --git a/api4/context.go b/api4/context.go
new file mode 100644
index 000000000..1a3011795
--- /dev/null
+++ b/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
+}
diff --git a/api4/params.go b/api4/params.go
new file mode 100644
index 000000000..452b9ba21
--- /dev/null
+++ b/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
+}
diff --git a/api4/user.go b/api4/user.go
new file mode 100644
index 000000000..9d38df1a1
--- /dev/null
+++ b/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)
+}
diff --git a/api4/user_test.go b/api4/user_test.go
new file mode 100644
index 000000000..d643f4e3a
--- /dev/null
+++ b/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)
+}