Merge pull request #693 from mattermost/mm-1419

Implement OAuth2 service provider functionality.
This commit is contained in:
Christopher Speller
2015-09-16 16:26:56 -04:00
45 changed files with 2506 additions and 199 deletions

View File

@@ -43,6 +43,7 @@ func InitApi() {
InitFile(r)
InitCommand(r)
InitAdmin(r)
InitOAuth(r)
templatesDir := utils.FindDir("api/templates")
l4g.Debug("Parsing server templates at %v", templatesDir)

View File

@@ -17,7 +17,7 @@ func Setup() {
NewServer()
StartServer()
InitApi()
Client = model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port + "/api/v1")
Client = model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port)
}
}

View File

@@ -62,7 +62,7 @@ func TestCreateChannel(t *testing.T) {
}
}
if _, err := Client.DoPost("/channels/create", "garbage"); err == nil {
if _, err := Client.DoApiPost("/channels/create", "garbage"); err == nil {
t.Fatal("should have been an error")
}
@@ -627,7 +627,7 @@ func TestGetChannelExtraInfo(t *testing.T) {
currentEtag = cache_result.Etag
}
Client2 := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port + "/api/v1")
Client2 := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port)
user2 := &model.User{TeamId: team.Id, Email: model.NewId() + "tester2@test.com", Nickname: "Tester 2", Password: "pwd"}
user2 = Client2.Must(Client2.CreateUser(user2, "")).Data.(*model.User)

View File

@@ -315,7 +315,7 @@ func loadTestSetupCommand(c *Context, command *model.Command) bool {
numPosts, _ = strconv.Atoi(tokens[numArgs+2])
}
}
client := model.NewClient(c.GetSiteURL() + "/api/v1")
client := model.NewClient(c.GetSiteURL())
if doTeams {
if err := CreateBasicUser(client); err != nil {
@@ -375,7 +375,7 @@ func loadTestUsersCommand(c *Context, command *model.Command) bool {
if err == false {
usersr = utils.Range{10, 15}
}
client := model.NewClient(c.GetSiteURL() + "/api/v1")
client := model.NewClient(c.GetSiteURL())
userCreator := NewAutoUserCreator(client, c.Session.TeamId)
userCreator.Fuzzy = doFuzz
userCreator.CreateTestUsers(usersr)
@@ -405,7 +405,7 @@ func loadTestChannelsCommand(c *Context, command *model.Command) bool {
if err == false {
channelsr = utils.Range{20, 30}
}
client := model.NewClient(c.GetSiteURL() + "/api/v1")
client := model.NewClient(c.GetSiteURL())
client.MockSession(c.Session.Id)
channelCreator := NewAutoChannelCreator(client, c.Session.TeamId)
channelCreator.Fuzzy = doFuzz
@@ -457,7 +457,7 @@ func loadTestPostsCommand(c *Context, command *model.Command) bool {
}
}
client := model.NewClient(c.GetSiteURL() + "/api/v1")
client := model.NewClient(c.GetSiteURL())
client.MockSession(c.Session.Id)
testPoster := NewAutoPostCreator(client, command.ChannelId)
testPoster.Fuzzy = doFuzz

View File

@@ -80,9 +80,36 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.RequestId = model.NewId()
c.IpAddress = GetIpAddress(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_TOKEN); err == nil {
token = cookie.Value
}
}
// Attempt to parse token out of the query string
if len(token) == 0 {
token = r.URL.Query().Get("access_token")
isTokenFromQueryString = true
}
protocol := "http"
// if the request came from the ELB then assume this is produciton
// If the request came from the ELB then assume this is produciton
// and redirect all http requests to https
if utils.Cfg.ServiceSettings.UseSSL {
forwardProto := r.Header.Get(model.HEADER_FORWARDED_PROTO)
@@ -105,36 +132,19 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Content-Security-Policy", "frame-ancestors none")
} else {
// All api response bodies will be JSON formatted
// All api response bodies will be JSON formatted by default
w.Header().Set("Content-Type", "application/json")
}
sessionId := ""
// attempt to parse the session token from the header
if ah := r.Header.Get(model.HEADER_AUTH); ah != "" {
if len(ah) > 6 && strings.ToUpper(ah[0:6]) == "BEARER" {
sessionId = ah[7:]
}
}
// attempt to parse the session token from the cookie
if sessionId == "" {
if cookie, err := r.Cookie(model.SESSION_TOKEN); err == nil {
sessionId = cookie.Value
}
}
if sessionId != "" {
if len(token) != 0 {
var session *model.Session
if ts, ok := sessionCache.Get(sessionId); ok {
if ts, ok := sessionCache.Get(token); ok {
session = ts.(*model.Session)
}
if session == nil {
if sessionResult := <-Srv.Store.Session().Get(sessionId); sessionResult.Err != nil {
c.LogError(model.NewAppError("ServeHTTP", "Invalid session", "id="+sessionId+", err="+sessionResult.Err.DetailedError))
if sessionResult := <-Srv.Store.Session().Get(token); sessionResult.Err != nil {
c.LogError(model.NewAppError("ServeHTTP", "Invalid session", "token="+token+", err="+sessionResult.Err.DetailedError))
} else {
session = sessionResult.Data.(*model.Session)
}
@@ -142,7 +152,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if session == nil || session.IsExpired() {
c.RemoveSessionCookie(w)
c.Err = model.NewAppError("ServeHTTP", "Invalid or expired session, please login again.", "id="+sessionId)
c.Err = model.NewAppError("ServeHTTP", "Invalid or expired session, please login again.", "token="+token)
c.Err.StatusCode = http.StatusUnauthorized
} else if !session.IsOAuth && isTokenFromQueryString {
c.Err = model.NewAppError("ServeHTTP", "Session is not OAuth but token was provided in the query string", "token="+token)
c.Err.StatusCode = http.StatusUnauthorized
} else {
c.Session = *session
@@ -166,10 +179,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
c.SystemAdminRequired()
}
if c.Err == nil && h.isUserActivity && sessionId != "" && len(c.Session.UserId) > 0 {
if c.Err == nil && h.isUserActivity && token != "" && len(c.Session.UserId) > 0 {
go func() {
if err := (<-Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, sessionId, model.GetMillis())).Err; err != nil {
l4g.Error("Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v", c.Session.UserId, sessionId, err)
if err := (<-Srv.Store.User().UpdateUserAndSessionActivity(c.Session.UserId, c.Session.Id, model.GetMillis())).Err; err != nil {
l4g.Error("Failed to update LastActivityAt for user_id=%v and session_id=%v, err=%v", c.Session.UserId, c.Session.Id, err)
}
}()
}
@@ -197,7 +210,7 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
func (c *Context) LogAudit(extraInfo string) {
audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.AltId}
audit := &model.Audit{UserId: c.Session.UserId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
if r := <-Srv.Store.Audit().Save(audit); r.Err != nil {
c.LogError(r.Err)
}
@@ -209,7 +222,7 @@ func (c *Context) LogAuditWithUserId(userId, extraInfo string) {
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.AltId}
audit := &model.Audit{UserId: userId, IpAddress: c.IpAddress, Action: c.Path, ExtraInfo: extraInfo, SessionId: c.Session.Id}
if r := <-Srv.Store.Audit().Save(audit); r.Err != nil {
c.LogError(r.Err)
}
@@ -315,7 +328,7 @@ func (c *Context) IsTeamAdmin(userId string) bool {
func (c *Context) RemoveSessionCookie(w http.ResponseWriter) {
sessionCache.Remove(c.Session.Id)
sessionCache.Remove(c.Session.Token)
cookie := &http.Cookie{
Name: model.SESSION_TOKEN,
@@ -471,3 +484,7 @@ func Handle404(w http.ResponseWriter, r *http.Request) {
l4g.Error("%v: code=404 ip=%v", r.URL.Path, GetIpAddress(r))
RenderWebError(err, w, r)
}
func AddSessionToCache(session *model.Session) {
sessionCache.Add(session.Token, session)
}

165
api/oauth.go Normal file
View File

@@ -0,0 +1,165 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
l4g "code.google.com/p/log4go"
"fmt"
"github.com/gorilla/mux"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"net/http"
"net/url"
)
func InitOAuth(r *mux.Router) {
l4g.Debug("Initializing oauth api routes")
sr := r.PathPrefix("/oauth").Subrouter()
sr.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST")
sr.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET")
}
func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewAppError("registerOAuthApp", "The system admin has turned off OAuth service providing.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
app := model.OAuthAppFromJson(r.Body)
if app == nil {
c.SetInvalidParam("registerOAuthApp", "app")
return
}
secret := model.NewId()
app.ClientSecret = secret
app.CreatorId = c.Session.UserId
if result := <-Srv.Store.OAuth().SaveApp(app); result.Err != nil {
c.Err = result.Err
return
} else {
app = result.Data.(*model.OAuthApp)
app.ClientSecret = secret
c.LogAudit("client_id=" + app.Id)
w.Write([]byte(app.ToJson()))
return
}
}
func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewAppError("allowOAuth", "The system admin has turned off OAuth service providing.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
c.LogAudit("attempt")
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
responseData := map[string]string{}
responseType := r.URL.Query().Get("response_type")
if len(responseType) == 0 {
c.Err = model.NewAppError("allowOAuth", "invalid_request: Bad response_type", "")
return
}
clientId := r.URL.Query().Get("client_id")
if len(clientId) != 26 {
c.Err = model.NewAppError("allowOAuth", "invalid_request: Bad client_id", "")
return
}
redirectUri := r.URL.Query().Get("redirect_uri")
if len(redirectUri) == 0 {
c.Err = model.NewAppError("allowOAuth", "invalid_request: Missing or bad redirect_uri", "")
return
}
scope := r.URL.Query().Get("scope")
state := r.URL.Query().Get("state")
var app *model.OAuthApp
if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
c.Err = model.NewAppError("allowOAuth", "server_error: Error accessing the database", "")
return
} else {
app = result.Data.(*model.OAuthApp)
}
if !app.IsValidRedirectURL(redirectUri) {
c.LogAudit("fail - redirect_uri did not match registered callback")
c.Err = model.NewAppError("allowOAuth", "invalid_request: Supplied redirect_uri did not match registered callback_url", "")
return
}
if responseType != model.AUTHCODE_RESPONSE_TYPE {
responseData["redirect"] = redirectUri + "?error=unsupported_response_type&state=" + state
w.Write([]byte(model.MapToJson(responseData)))
return
}
authData := &model.AuthData{UserId: c.Session.UserId, ClientId: clientId, CreateAt: model.GetMillis(), RedirectUri: redirectUri, State: state, Scope: scope}
authData.Code = model.HashPassword(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, c.Session.UserId))
if result := <-Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil {
responseData["redirect"] = redirectUri + "?error=server_error&state=" + state
w.Write([]byte(model.MapToJson(responseData)))
return
}
c.LogAudit("success")
responseData["redirect"] = redirectUri + "?code=" + url.QueryEscape(authData.Code) + "&state=" + url.QueryEscape(authData.State)
w.Write([]byte(model.MapToJson(responseData)))
}
func RevokeAccessToken(token string) *model.AppError {
schan := Srv.Store.Session().Remove(token)
sessionCache.Remove(token)
var accessData *model.AccessData
if result := <-Srv.Store.OAuth().GetAccessData(token); result.Err != nil {
return model.NewAppError("RevokeAccessToken", "Error getting access token from DB before deletion", "")
} else {
accessData = result.Data.(*model.AccessData)
}
tchan := Srv.Store.OAuth().RemoveAccessData(token)
cchan := Srv.Store.OAuth().RemoveAuthData(accessData.AuthCode)
if result := <-tchan; result.Err != nil {
return model.NewAppError("RevokeAccessToken", "Error deleting access token from DB", "")
}
if result := <-cchan; result.Err != nil {
return model.NewAppError("RevokeAccessToken", "Error deleting authorization code from DB", "")
}
if result := <-schan; result.Err != nil {
return model.NewAppError("RevokeAccessToken", "Error deleting session from DB", "")
}
return nil
}
func GetAuthData(code string) *model.AuthData {
if result := <-Srv.Store.OAuth().GetAuthData(code); result.Err != nil {
l4g.Error("Couldn't find auth code for code=%s", code)
return nil
} else {
return result.Data.(*model.AuthData)
}
}

157
api/oauth_test.go Normal file
View File

@@ -0,0 +1,157 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package api
import (
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
"net/url"
"strings"
"testing"
)
func TestRegisterApp(t *testing.T) {
Setup()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Password: "pwd"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
if _, err := Client.RegisterApp(app); err == nil {
t.Fatal("should have failed - oauth providing turned off")
}
} else {
Client.Logout()
if _, err := Client.RegisterApp(app); err == nil {
t.Fatal("not logged in - should have failed")
}
Client.Must(Client.LoginById(ruser.Id, "pwd"))
if result, err := Client.RegisterApp(app); err != nil {
t.Fatal(err)
} else {
rapp := result.Data.(*model.OAuthApp)
if len(rapp.Id) != 26 {
t.Fatal("clientid didn't return properly")
}
if len(rapp.ClientSecret) != 26 {
t.Fatal("client secret didn't return properly")
}
}
app = &model.OAuthApp{Name: "", Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
if _, err := Client.RegisterApp(app); err == nil {
t.Fatal("missing name - should have failed")
}
app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
if _, err := Client.RegisterApp(app); err == nil {
t.Fatal("missing homepage - should have failed")
}
app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{}}
if _, err := Client.RegisterApp(app); err == nil {
t.Fatal("missing callback url - should have failed")
}
}
}
func TestAllowOAuth(t *testing.T) {
Setup()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := Client.CreateTeam(&team)
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Password: "pwd"}
ruser := Client.Must(Client.CreateUser(&user, "")).Data.(*model.User)
store.Must(Srv.Store.User().VerifyEmail(ruser.Id))
app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
Client.Must(Client.LoginById(ruser.Id, "pwd"))
state := "123"
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "12345678901234567890123456", app.CallbackUrls[0], "all", state); err == nil {
t.Fatal("should have failed - oauth service providing turned off")
}
} else {
app = Client.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp)
if result, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", state); err != nil {
t.Fatal(err)
} else {
redirect := result.Data.(map[string]string)["redirect"]
if len(redirect) == 0 {
t.Fatal("redirect url should be set")
}
ru, _ := url.Parse(redirect)
if ru == nil {
t.Fatal("redirect url unparseable")
} else {
if len(ru.Query().Get("code")) == 0 {
t.Fatal("authorization code not returned")
}
if ru.Query().Get("state") != state {
t.Fatal("returned state doesn't match")
}
}
}
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "all", state); err == nil {
t.Fatal("should have failed - no redirect_url given")
}
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "", state); err == nil {
t.Fatal("should have failed - no redirect_url given")
}
if result, err := Client.AllowOAuth("junk", app.Id, app.CallbackUrls[0], "all", state); err != nil {
t.Fatal(err)
} else {
redirect := result.Data.(map[string]string)["redirect"]
if len(redirect) == 0 {
t.Fatal("redirect url should be set")
}
ru, _ := url.Parse(redirect)
if ru == nil {
t.Fatal("redirect url unparseable")
} else {
if ru.Query().Get("error") != "unsupported_response_type" {
t.Fatal("wrong error returned")
}
if ru.Query().Get("state") != state {
t.Fatal("returned state doesn't match")
}
}
}
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "", app.CallbackUrls[0], "all", state); err == nil {
t.Fatal("should have failed - empty client id")
}
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "junk", app.CallbackUrls[0], "all", state); err == nil {
t.Fatal("should have failed - bad client id")
}
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "https://somewhereelse.com", "all", state); err == nil {
t.Fatal("should have failed - redirect uri host does not match app host")
}
}
}

View File

@@ -118,7 +118,7 @@ func TestCreatePost(t *testing.T) {
t.Fatal("Should have been forbidden")
}
if _, err = Client.DoPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil {
if _, err = Client.DoApiPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil {
t.Fatal("should have been an error")
}
}
@@ -203,7 +203,7 @@ func TestCreateValetPost(t *testing.T) {
t.Fatal("Should have been forbidden")
}
if _, err = Client.DoPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil {
if _, err = Client.DoApiPost("/channels/"+channel3.Id+"/create", "garbage"); err == nil {
t.Fatal("should have been an error")
}
} else {

View File

@@ -103,7 +103,7 @@ func TestCreateTeam(t *testing.T) {
}
}
if _, err := Client.DoPost("/teams/create", "garbage"); err == nil {
if _, err := Client.DoApiPost("/teams/create", "garbage"); err == nil {
t.Fatal("should have been an error")
}
}

View File

@@ -336,7 +336,7 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User,
return
}
session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, DeviceId: deviceId}
session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, DeviceId: deviceId, IsOAuth: false}
maxAge := model.SESSION_TIME_WEB_IN_SECS
@@ -378,13 +378,13 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User,
return
} else {
session = result.Data.(*model.Session)
sessionCache.Add(session.Id, session)
AddSessionToCache(session)
}
w.Header().Set(model.HEADER_TOKEN, session.Id)
w.Header().Set(model.HEADER_TOKEN, session.Token)
sessionCookie := &http.Cookie{
Name: model.SESSION_TOKEN,
Value: session.Id,
Value: session.Token,
Path: "/",
MaxAge: maxAge,
HttpOnly: true,
@@ -430,25 +430,27 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
altId := props["id"]
id := props["id"]
if result := <-Srv.Store.Session().GetSessions(c.Session.UserId); result.Err != nil {
if result := <-Srv.Store.Session().Get(id); result.Err != nil {
c.Err = result.Err
return
} else {
sessions := result.Data.([]*model.Session)
session := result.Data.(*model.Session)
for _, session := range sessions {
if session.AltId == altId {
c.LogAudit("session_id=" + session.AltId)
sessionCache.Remove(session.Id)
if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
c.Err = result.Err
return
} else {
w.Write([]byte(model.MapToJson(props)))
return
}
c.LogAudit("session_id=" + session.Id)
if session.IsOAuth {
RevokeAccessToken(session.Token)
} else {
sessionCache.Remove(session.Token)
if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
c.Err = result.Err
return
} else {
w.Write([]byte(model.MapToJson(props)))
return
}
}
}
@@ -462,8 +464,8 @@ func RevokeAllSession(c *Context, userId string) {
sessions := result.Data.([]*model.Session)
for _, session := range sessions {
c.LogAuditWithUserId(userId, "session_id="+session.AltId)
sessionCache.Remove(session.Id)
c.LogAuditWithUserId(userId, "session_id="+session.Id)
sessionCache.Remove(session.Token)
if result := <-Srv.Store.Session().Remove(session.Id); result.Err != nil {
c.Err = result.Err
return

View File

@@ -68,7 +68,7 @@ func TestCreateUser(t *testing.T) {
}
}
if _, err := Client.DoPost("/users/create", "garbage"); err == nil {
if _, err := Client.DoApiPost("/users/create", "garbage"); err == nil {
t.Fatal("should have been an error")
}
}
@@ -190,11 +190,11 @@ func TestSessions(t *testing.T) {
for _, session := range sessions {
if session.DeviceId == deviceId {
otherSession = session.AltId
otherSession = session.Id
}
if len(session.Id) != 0 {
t.Fatal("shouldn't return sessions")
if len(session.Token) != 0 {
t.Fatal("shouldn't return session tokens")
}
}
@@ -212,11 +212,6 @@ func TestSessions(t *testing.T) {
if len(sessions2) != 1 {
t.Fatal("invalid number of sessions")
}
if _, err := Client.RevokeSession(otherSession); err != nil {
t.Fatal(err)
}
}
func TestGetUser(t *testing.T) {
@@ -355,7 +350,7 @@ func TestUserCreateImage(t *testing.T) {
Client.LoginByEmail(team.Name, user.Email, "pwd")
Client.DoGet("/users/"+user.Id+"/image", "", "")
Client.DoApiGet("/users/"+user.Id+"/image", "", "")
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
var auth aws.Auth
@@ -453,7 +448,7 @@ func TestUserUploadProfileImage(t *testing.T) {
t.Fatal(upErr)
}
Client.DoGet("/users/"+user.Id+"/image", "", "")
Client.DoApiGet("/users/"+user.Id+"/image", "", "")
if utils.IsS3Configured() && !utils.Cfg.ServiceSettings.UseLocalStorage {
var auth aws.Auth

View File

@@ -23,7 +23,8 @@
"UseLocalStorage": true,
"StorageDirectory": "./data/",
"AllowedLoginAttempts": 10,
"DisableEmailSignUp": false
"DisableEmailSignUp": false,
"EnableOAuthServiceProvider": false
},
"SSOSettings": {
"gitlab": {

View File

@@ -23,7 +23,8 @@
"UseLocalStorage": true,
"StorageDirectory": "/mattermost/data/",
"AllowedLoginAttempts": 10,
"DisableEmailSignUp": false
"DisableEmailSignUp": false,
"EnableOAuthServiceProvider": false
},
"SSOSettings": {
"gitlab": {

View File

@@ -23,7 +23,8 @@
"UseLocalStorage": true,
"StorageDirectory": "/mattermost/data/",
"AllowedLoginAttempts": 10,
"DisableEmailSignUp": false
"DisableEmailSignUp": false,
"EnableOAuthServiceProvider": false
},
"SSOSettings": {
"gitlab": {

View File

@@ -53,7 +53,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
}
// Create a client for tests to use
client := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port + "/api/v1")
client := model.NewClient("http://localhost:" + utils.Cfg.ServiceSettings.Port)
// Check for username parameter and create a user if present
username, ok1 := params["username"]
@@ -65,7 +65,7 @@ func manualTest(c *api.Context, w http.ResponseWriter, r *http.Request) {
// Create team for testing
team := &model.Team{
DisplayName: teamDisplayName[0],
Name: utils.RandomName(utils.Range{20, 20}, utils.LOWERCASE),
Name: utils.RandomName(utils.Range{20, 20}, utils.LOWERCASE),
Email: utils.RandomEmail(utils.Range{20, 20}, utils.LOWERCASE),
Type: model.TEAM_OPEN,
}

View File

@@ -9,17 +9,69 @@ import (
)
const (
ACCESS_TOKEN_GRANT_TYPE = "authorization_code"
ACCESS_TOKEN_TYPE = "bearer"
ACCESS_TOKEN_GRANT_TYPE = "authorization_code"
ACCESS_TOKEN_TYPE = "bearer"
REFRESH_TOKEN_GRANT_TYPE = "refresh_token"
)
type AccessData struct {
AuthCode string `json:"auth_code"`
Token string `json"token"`
RefreshToken string `json:"refresh_token"`
RedirectUri string `json:"redirect_uri"`
}
type AccessResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int32 `json:"expires_in"`
Scope string `json:"scope"`
RefreshToken string `json:"refresh_token"`
}
// IsValid validates the AccessData and returns an error if it isn't configured
// correctly.
func (ad *AccessData) IsValid() *AppError {
if len(ad.AuthCode) == 0 || len(ad.AuthCode) > 128 {
return NewAppError("AccessData.IsValid", "Invalid auth code", "")
}
if len(ad.Token) != 26 {
return NewAppError("AccessData.IsValid", "Invalid access token", "")
}
if len(ad.RefreshToken) > 26 {
return NewAppError("AccessData.IsValid", "Invalid refresh token", "")
}
if len(ad.RedirectUri) > 256 {
return NewAppError("AccessData.IsValid", "Invalid redirect uri", "")
}
return nil
}
func (ad *AccessData) ToJson() string {
b, err := json.Marshal(ad)
if err != nil {
return ""
} else {
return string(b)
}
}
func AccessDataFromJson(data io.Reader) *AccessData {
decoder := json.NewDecoder(data)
var ad AccessData
err := decoder.Decode(&ad)
if err == nil {
return &ad
} else {
return nil
}
}
func (ar *AccessResponse) ToJson() string {
b, err := json.Marshal(ar)
if err != nil {

41
model/access_test.go Normal file
View File

@@ -0,0 +1,41 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"strings"
"testing"
)
func TestAccessJson(t *testing.T) {
a1 := AccessData{}
a1.AuthCode = NewId()
a1.Token = NewId()
a1.RefreshToken = NewId()
json := a1.ToJson()
ra1 := AccessDataFromJson(strings.NewReader(json))
if a1.Token != ra1.Token {
t.Fatal("tokens didn't match")
}
}
func TestAccessIsValid(t *testing.T) {
ad := AccessData{}
if err := ad.IsValid(); err == nil {
t.Fatal("should have failed")
}
ad.AuthCode = NewId()
if err := ad.IsValid(); err == nil {
t.Fatal("should have failed")
}
ad.Token = NewId()
if err := ad.IsValid(); err != nil {
t.Fatal(err)
}
}

103
model/authorize.go Normal file
View File

@@ -0,0 +1,103 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
AUTHCODE_EXPIRE_TIME = 60 * 10 // 10 minutes
AUTHCODE_RESPONSE_TYPE = "code"
)
type AuthData struct {
ClientId string `json:"client_id"`
UserId string `json:"user_id"`
Code string `json:"code"`
ExpiresIn int32 `json:"expires_in"`
CreateAt int64 `json:"create_at"`
RedirectUri string `json:"redirect_uri"`
State string `json:"state"`
Scope string `json:"scope"`
}
// IsValid validates the AuthData and returns an error if it isn't configured
// correctly.
func (ad *AuthData) IsValid() *AppError {
if len(ad.ClientId) != 26 {
return NewAppError("AuthData.IsValid", "Invalid client id", "")
}
if len(ad.UserId) != 26 {
return NewAppError("AuthData.IsValid", "Invalid user id", "")
}
if len(ad.Code) == 0 || len(ad.Code) > 128 {
return NewAppError("AuthData.IsValid", "Invalid authorization code", "client_id="+ad.ClientId)
}
if ad.ExpiresIn == 0 {
return NewAppError("AuthData.IsValid", "Expires in must be set", "")
}
if ad.CreateAt <= 0 {
return NewAppError("AuthData.IsValid", "Create at must be a valid time", "client_id="+ad.ClientId)
}
if len(ad.RedirectUri) > 256 {
return NewAppError("AuthData.IsValid", "Invalid redirect uri", "client_id="+ad.ClientId)
}
if len(ad.State) > 128 {
return NewAppError("AuthData.IsValid", "Invalid state", "client_id="+ad.ClientId)
}
if len(ad.Scope) > 128 {
return NewAppError("AuthData.IsValid", "Invalid scope", "client_id="+ad.ClientId)
}
return nil
}
func (ad *AuthData) PreSave() {
if ad.ExpiresIn == 0 {
ad.ExpiresIn = AUTHCODE_EXPIRE_TIME
}
if ad.CreateAt == 0 {
ad.CreateAt = GetMillis()
}
}
func (ad *AuthData) ToJson() string {
b, err := json.Marshal(ad)
if err != nil {
return ""
} else {
return string(b)
}
}
func AuthDataFromJson(data io.Reader) *AuthData {
decoder := json.NewDecoder(data)
var ad AuthData
err := decoder.Decode(&ad)
if err == nil {
return &ad
} else {
return nil
}
}
func (ad *AuthData) IsExpired() bool {
if GetMillis() > ad.CreateAt+int64(ad.ExpiresIn*1000) {
return true
}
return false
}

66
model/authorize_test.go Normal file
View File

@@ -0,0 +1,66 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"strings"
"testing"
)
func TestAuthJson(t *testing.T) {
a1 := AuthData{}
a1.ClientId = NewId()
a1.UserId = NewId()
a1.Code = NewId()
json := a1.ToJson()
ra1 := AuthDataFromJson(strings.NewReader(json))
if a1.Code != ra1.Code {
t.Fatal("codes didn't match")
}
}
func TestAuthPreSave(t *testing.T) {
a1 := AuthData{}
a1.ClientId = NewId()
a1.UserId = NewId()
a1.Code = NewId()
a1.PreSave()
a1.IsExpired()
}
func TestAuthIsValid(t *testing.T) {
ad := AuthData{}
if err := ad.IsValid(); err == nil {
t.Fatal()
}
ad.ClientId = NewId()
if err := ad.IsValid(); err == nil {
t.Fatal()
}
ad.UserId = NewId()
if err := ad.IsValid(); err == nil {
t.Fatal()
}
ad.Code = NewId()
if err := ad.IsValid(); err == nil {
t.Fatal()
}
ad.ExpiresIn = 1
if err := ad.IsValid(); err == nil {
t.Fatal()
}
ad.CreateAt = 1
if err := ad.IsValid(); err != nil {
t.Fatal()
}
}

View File

@@ -23,7 +23,9 @@ const (
HEADER_FORWARDED = "X-Forwarded-For"
HEADER_FORWARDED_PROTO = "X-Forwarded-Proto"
HEADER_TOKEN = "token"
HEADER_BEARER = "BEARER"
HEADER_AUTH = "Authorization"
API_URL_SUFFIX = "/api/v1"
)
type Result struct {
@@ -33,22 +35,37 @@ type Result struct {
}
type Client struct {
Url string // The location of the server like "http://localhost/api/v1"
Url string // The location of the server like "http://localhost:8065"
ApiUrl string // The api location of the server like "http://localhost:8065/api/v1"
HttpClient *http.Client // The http client
AuthToken string
AuthType string
}
// NewClient constructs a new client with convienence methods for talking to
// the server.
func NewClient(url string) *Client {
return &Client{url, &http.Client{}, ""}
return &Client{url, url + API_URL_SUFFIX, &http.Client{}, "", ""}
}
func (c *Client) DoPost(url string, data string) (*http.Response, *AppError) {
func (c *Client) DoPost(url string, data, contentType string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("POST", c.Url+url, strings.NewReader(data))
rq.Header.Set("Content-Type", contentType)
if rp, err := c.HttpClient.Do(rq); err != nil {
return nil, NewAppError(url, "We encountered an error while connecting to the server", err.Error())
} else if rp.StatusCode >= 300 {
return nil, AppErrorFromJson(rp.Body)
} else {
return rp, nil
}
}
func (c *Client) DoApiPost(url string, data string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("POST", c.ApiUrl+url, strings.NewReader(data))
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
if rp, err := c.HttpClient.Do(rq); err != nil {
@@ -60,15 +77,15 @@ func (c *Client) DoPost(url string, data string) (*http.Response, *AppError) {
}
}
func (c *Client) DoGet(url string, data string, etag string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("GET", c.Url+url, strings.NewReader(data))
func (c *Client) DoApiGet(url string, data string, etag string) (*http.Response, *AppError) {
rq, _ := http.NewRequest("GET", c.ApiUrl+url, strings.NewReader(data))
if len(etag) > 0 {
rq.Header.Set(HEADER_ETAG_CLIENT, etag)
}
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
rq.Header.Set(HEADER_AUTH, c.AuthType+" "+c.AuthToken)
}
if rp, err := c.HttpClient.Do(rq); err != nil {
@@ -106,7 +123,7 @@ func (c *Client) SignupTeam(email string, displayName string) (*Result, *AppErro
m := make(map[string]string)
m["email"] = email
m["display_name"] = displayName
if r, err := c.DoPost("/teams/signup", MapToJson(m)); err != nil {
if r, err := c.DoApiPost("/teams/signup", MapToJson(m)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -115,7 +132,7 @@ func (c *Client) SignupTeam(email string, displayName string) (*Result, *AppErro
}
func (c *Client) CreateTeamFromSignup(teamSignup *TeamSignup) (*Result, *AppError) {
if r, err := c.DoPost("/teams/create_from_signup", teamSignup.ToJson()); err != nil {
if r, err := c.DoApiPost("/teams/create_from_signup", teamSignup.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -124,7 +141,7 @@ func (c *Client) CreateTeamFromSignup(teamSignup *TeamSignup) (*Result, *AppErro
}
func (c *Client) CreateTeam(team *Team) (*Result, *AppError) {
if r, err := c.DoPost("/teams/create", team.ToJson()); err != nil {
if r, err := c.DoApiPost("/teams/create", team.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -136,7 +153,7 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro
m := make(map[string]string)
m["name"] = name
m["all"] = fmt.Sprintf("%v", allServers)
if r, err := c.DoPost("/teams/find_team_by_name", MapToJson(m)); err != nil {
if r, err := c.DoApiPost("/teams/find_team_by_name", MapToJson(m)); err != nil {
return nil, err
} else {
val := false
@@ -152,7 +169,7 @@ func (c *Client) FindTeamByName(name string, allServers bool) (*Result, *AppErro
func (c *Client) FindTeams(email string) (*Result, *AppError) {
m := make(map[string]string)
m["email"] = email
if r, err := c.DoPost("/teams/find_teams", MapToJson(m)); err != nil {
if r, err := c.DoApiPost("/teams/find_teams", MapToJson(m)); err != nil {
return nil, err
} else {
@@ -164,7 +181,7 @@ func (c *Client) FindTeams(email string) (*Result, *AppError) {
func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) {
m := make(map[string]string)
m["email"] = email
if r, err := c.DoPost("/teams/email_teams", MapToJson(m)); err != nil {
if r, err := c.DoApiPost("/teams/email_teams", MapToJson(m)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -173,7 +190,7 @@ func (c *Client) FindTeamsSendEmail(email string) (*Result, *AppError) {
}
func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) {
if r, err := c.DoPost("/teams/invite_members", invites.ToJson()); err != nil {
if r, err := c.DoApiPost("/teams/invite_members", invites.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -182,7 +199,7 @@ func (c *Client) InviteMembers(invites *Invites) (*Result, *AppError) {
}
func (c *Client) UpdateTeamDisplayName(data map[string]string) (*Result, *AppError) {
if r, err := c.DoPost("/teams/update_name", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/teams/update_name", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -191,7 +208,7 @@ func (c *Client) UpdateTeamDisplayName(data map[string]string) (*Result, *AppErr
}
func (c *Client) UpdateValetFeature(data map[string]string) (*Result, *AppError) {
if r, err := c.DoPost("/teams/update_valet_feature", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/teams/update_valet_feature", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -200,7 +217,7 @@ func (c *Client) UpdateValetFeature(data map[string]string) (*Result, *AppError)
}
func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) {
if r, err := c.DoPost("/users/create", user.ToJson()); err != nil {
if r, err := c.DoApiPost("/users/create", user.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -209,7 +226,7 @@ func (c *Client) CreateUser(user *User, hash string) (*Result, *AppError) {
}
func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Result, *AppError) {
if r, err := c.DoPost("/users/create?d="+data+"&h="+hash, user.ToJson()); err != nil {
if r, err := c.DoApiPost("/users/create?d="+data+"&h="+hash, user.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -218,7 +235,7 @@ func (c *Client) CreateUserFromSignup(user *User, data string, hash string) (*Re
}
func (c *Client) GetUser(id string, etag string) (*Result, *AppError) {
if r, err := c.DoGet("/users/"+id, "", etag); err != nil {
if r, err := c.DoApiGet("/users/"+id, "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -227,7 +244,7 @@ func (c *Client) GetUser(id string, etag string) (*Result, *AppError) {
}
func (c *Client) GetMe(etag string) (*Result, *AppError) {
if r, err := c.DoGet("/users/me", "", etag); err != nil {
if r, err := c.DoApiGet("/users/me", "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -236,7 +253,7 @@ func (c *Client) GetMe(etag string) (*Result, *AppError) {
}
func (c *Client) GetProfiles(teamId string, etag string) (*Result, *AppError) {
if r, err := c.DoGet("/users/profiles", "", etag); err != nil {
if r, err := c.DoApiGet("/users/profiles", "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -269,13 +286,14 @@ func (c *Client) LoginByEmailWithDevice(name string, email string, password stri
}
func (c *Client) login(m map[string]string) (*Result, *AppError) {
if r, err := c.DoPost("/users/login", MapToJson(m)); err != nil {
if r, err := c.DoApiPost("/users/login", MapToJson(m)); err != nil {
return nil, err
} else {
c.AuthToken = r.Header.Get(HEADER_TOKEN)
sessionId := getCookie(SESSION_TOKEN, r)
c.AuthType = HEADER_BEARER
sessionToken := getCookie(SESSION_TOKEN, r)
if c.AuthToken != sessionId.Value {
if c.AuthToken != sessionToken.Value {
NewAppError("/users/login", "Authentication tokens didn't match", "")
}
@@ -285,21 +303,32 @@ func (c *Client) login(m map[string]string) (*Result, *AppError) {
}
func (c *Client) Logout() (*Result, *AppError) {
if r, err := c.DoPost("/users/logout", ""); err != nil {
if r, err := c.DoApiPost("/users/logout", ""); err != nil {
return nil, err
} else {
c.AuthToken = ""
c.AuthType = HEADER_BEARER
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
func (c *Client) SetOAuthToken(token string) {
c.AuthToken = token
c.AuthType = HEADER_TOKEN
}
func (c *Client) ClearOAuthToken() {
c.AuthToken = ""
c.AuthType = HEADER_BEARER
}
func (c *Client) RevokeSession(sessionAltId string) (*Result, *AppError) {
m := make(map[string]string)
m["id"] = sessionAltId
if r, err := c.DoPost("/users/revoke_session", MapToJson(m)); err != nil {
if r, err := c.DoApiPost("/users/revoke_session", MapToJson(m)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -308,7 +337,7 @@ func (c *Client) RevokeSession(sessionAltId string) (*Result, *AppError) {
}
func (c *Client) GetSessions(id string) (*Result, *AppError) {
if r, err := c.DoGet("/users/"+id+"/sessions", "", ""); err != nil {
if r, err := c.DoApiGet("/users/"+id+"/sessions", "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -321,7 +350,7 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul
m["command"] = command
m["channelId"] = channelId
m["suggest"] = strconv.FormatBool(suggest)
if r, err := c.DoPost("/command", MapToJson(m)); err != nil {
if r, err := c.DoApiPost("/command", MapToJson(m)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -330,7 +359,7 @@ func (c *Client) Command(channelId string, command string, suggest bool) (*Resul
}
func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) {
if r, err := c.DoGet("/users/"+id+"/audits", "", etag); err != nil {
if r, err := c.DoApiGet("/users/"+id+"/audits", "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -339,7 +368,7 @@ func (c *Client) GetAudits(id string, etag string) (*Result, *AppError) {
}
func (c *Client) GetLogs() (*Result, *AppError) {
if r, err := c.DoGet("/admin/logs", "", ""); err != nil {
if r, err := c.DoApiGet("/admin/logs", "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -348,7 +377,7 @@ func (c *Client) GetLogs() (*Result, *AppError) {
}
func (c *Client) GetClientProperties() (*Result, *AppError) {
if r, err := c.DoGet("/admin/client_props", "", ""); err != nil {
if r, err := c.DoApiGet("/admin/client_props", "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -357,7 +386,7 @@ func (c *Client) GetClientProperties() (*Result, *AppError) {
}
func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
if r, err := c.DoPost("/channels/create", channel.ToJson()); err != nil {
if r, err := c.DoApiPost("/channels/create", channel.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -366,7 +395,7 @@ func (c *Client) CreateChannel(channel *Channel) (*Result, *AppError) {
}
func (c *Client) CreateDirectChannel(data map[string]string) (*Result, *AppError) {
if r, err := c.DoPost("/channels/create_direct", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/channels/create_direct", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -375,7 +404,7 @@ func (c *Client) CreateDirectChannel(data map[string]string) (*Result, *AppError
}
func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) {
if r, err := c.DoPost("/channels/update", channel.ToJson()); err != nil {
if r, err := c.DoApiPost("/channels/update", channel.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -384,7 +413,7 @@ func (c *Client) UpdateChannel(channel *Channel) (*Result, *AppError) {
}
func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError) {
if r, err := c.DoPost("/channels/update_desc", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/channels/update_desc", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -393,7 +422,7 @@ func (c *Client) UpdateChannelDesc(data map[string]string) (*Result, *AppError)
}
func (c *Client) UpdateNotifyLevel(data map[string]string) (*Result, *AppError) {
if r, err := c.DoPost("/channels/update_notify_level", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/channels/update_notify_level", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -402,7 +431,7 @@ func (c *Client) UpdateNotifyLevel(data map[string]string) (*Result, *AppError)
}
func (c *Client) GetChannels(etag string) (*Result, *AppError) {
if r, err := c.DoGet("/channels/", "", etag); err != nil {
if r, err := c.DoApiGet("/channels/", "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -411,7 +440,7 @@ func (c *Client) GetChannels(etag string) (*Result, *AppError) {
}
func (c *Client) GetChannel(id, etag string) (*Result, *AppError) {
if r, err := c.DoGet("/channels/"+id+"/", "", etag); err != nil {
if r, err := c.DoApiGet("/channels/"+id+"/", "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -420,7 +449,7 @@ func (c *Client) GetChannel(id, etag string) (*Result, *AppError) {
}
func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) {
if r, err := c.DoGet("/channels/more", "", etag); err != nil {
if r, err := c.DoApiGet("/channels/more", "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -429,7 +458,7 @@ func (c *Client) GetMoreChannels(etag string) (*Result, *AppError) {
}
func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) {
if r, err := c.DoGet("/channels/counts", "", etag); err != nil {
if r, err := c.DoApiGet("/channels/counts", "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -438,7 +467,7 @@ func (c *Client) GetChannelCounts(etag string) (*Result, *AppError) {
}
func (c *Client) JoinChannel(id string) (*Result, *AppError) {
if r, err := c.DoPost("/channels/"+id+"/join", ""); err != nil {
if r, err := c.DoApiPost("/channels/"+id+"/join", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -447,7 +476,7 @@ func (c *Client) JoinChannel(id string) (*Result, *AppError) {
}
func (c *Client) LeaveChannel(id string) (*Result, *AppError) {
if r, err := c.DoPost("/channels/"+id+"/leave", ""); err != nil {
if r, err := c.DoApiPost("/channels/"+id+"/leave", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -456,7 +485,7 @@ func (c *Client) LeaveChannel(id string) (*Result, *AppError) {
}
func (c *Client) DeleteChannel(id string) (*Result, *AppError) {
if r, err := c.DoPost("/channels/"+id+"/delete", ""); err != nil {
if r, err := c.DoApiPost("/channels/"+id+"/delete", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -467,7 +496,7 @@ func (c *Client) DeleteChannel(id string) (*Result, *AppError) {
func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) {
data := make(map[string]string)
data["user_id"] = user_id
if r, err := c.DoPost("/channels/"+id+"/add", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/channels/"+id+"/add", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -478,7 +507,7 @@ func (c *Client) AddChannelMember(id, user_id string) (*Result, *AppError) {
func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) {
data := make(map[string]string)
data["user_id"] = user_id
if r, err := c.DoPost("/channels/"+id+"/remove", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/channels/"+id+"/remove", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -487,7 +516,7 @@ func (c *Client) RemoveChannelMember(id, user_id string) (*Result, *AppError) {
}
func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) {
if r, err := c.DoPost("/channels/"+channelId+"/update_last_viewed_at", ""); err != nil {
if r, err := c.DoApiPost("/channels/"+channelId+"/update_last_viewed_at", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -496,7 +525,7 @@ func (c *Client) UpdateLastViewedAt(channelId string) (*Result, *AppError) {
}
func (c *Client) GetChannelExtraInfo(id string, etag string) (*Result, *AppError) {
if r, err := c.DoGet("/channels/"+id+"/extra_info", "", etag); err != nil {
if r, err := c.DoApiGet("/channels/"+id+"/extra_info", "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -505,7 +534,7 @@ func (c *Client) GetChannelExtraInfo(id string, etag string) (*Result, *AppError
}
func (c *Client) CreatePost(post *Post) (*Result, *AppError) {
if r, err := c.DoPost("/channels/"+post.ChannelId+"/create", post.ToJson()); err != nil {
if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/create", post.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -514,7 +543,7 @@ func (c *Client) CreatePost(post *Post) (*Result, *AppError) {
}
func (c *Client) CreateValetPost(post *Post) (*Result, *AppError) {
if r, err := c.DoPost("/channels/"+post.ChannelId+"/valet_create", post.ToJson()); err != nil {
if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/valet_create", post.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -523,7 +552,7 @@ func (c *Client) CreateValetPost(post *Post) (*Result, *AppError) {
}
func (c *Client) UpdatePost(post *Post) (*Result, *AppError) {
if r, err := c.DoPost("/channels/"+post.ChannelId+"/update", post.ToJson()); err != nil {
if r, err := c.DoApiPost("/channels/"+post.ChannelId+"/update", post.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -532,7 +561,7 @@ func (c *Client) UpdatePost(post *Post) (*Result, *AppError) {
}
func (c *Client) GetPosts(channelId string, offset int, limit int, etag string) (*Result, *AppError) {
if r, err := c.DoGet(fmt.Sprintf("/channels/%v/posts/%v/%v", channelId, offset, limit), "", etag); err != nil {
if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/posts/%v/%v", channelId, offset, limit), "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -541,7 +570,7 @@ func (c *Client) GetPosts(channelId string, offset int, limit int, etag string)
}
func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError) {
if r, err := c.DoGet(fmt.Sprintf("/channels/%v/posts/%v", channelId, time), "", ""); err != nil {
if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/posts/%v", channelId, time), "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -550,7 +579,7 @@ func (c *Client) GetPostsSince(channelId string, time int64) (*Result, *AppError
}
func (c *Client) GetPost(channelId string, postId string, etag string) (*Result, *AppError) {
if r, err := c.DoGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil {
if r, err := c.DoApiGet(fmt.Sprintf("/channels/%v/post/%v", channelId, postId), "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -559,7 +588,7 @@ func (c *Client) GetPost(channelId string, postId string, etag string) (*Result,
}
func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError) {
if r, err := c.DoPost(fmt.Sprintf("/channels/%v/post/%v/delete", channelId, postId), ""); err != nil {
if r, err := c.DoApiPost(fmt.Sprintf("/channels/%v/post/%v/delete", channelId, postId), ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -568,7 +597,7 @@ func (c *Client) DeletePost(channelId string, postId string) (*Result, *AppError
}
func (c *Client) SearchPosts(terms string) (*Result, *AppError) {
if r, err := c.DoGet("/posts/search?terms="+url.QueryEscape(terms), "", ""); err != nil {
if r, err := c.DoApiGet("/posts/search?terms="+url.QueryEscape(terms), "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -577,7 +606,7 @@ func (c *Client) SearchPosts(terms string) (*Result, *AppError) {
}
func (c *Client) UploadFile(url string, data []byte, contentType string) (*Result, *AppError) {
rq, _ := http.NewRequest("POST", c.Url+url, bytes.NewReader(data))
rq, _ := http.NewRequest("POST", c.ApiUrl+url, bytes.NewReader(data))
rq.Header.Set("Content-Type", contentType)
if len(c.AuthToken) > 0 {
@@ -599,7 +628,7 @@ func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) {
if isFullUrl {
rq, _ = http.NewRequest("GET", url, nil)
} else {
rq, _ = http.NewRequest("GET", c.Url+"/files/get"+url, nil)
rq, _ = http.NewRequest("GET", c.ApiUrl+"/files/get"+url, nil)
}
if len(c.AuthToken) > 0 {
@@ -618,7 +647,7 @@ func (c *Client) GetFile(url string, isFullUrl bool) (*Result, *AppError) {
func (c *Client) GetFileInfo(url string) (*Result, *AppError) {
var rq *http.Request
rq, _ = http.NewRequest("GET", c.Url+"/files/get_info"+url, nil)
rq, _ = http.NewRequest("GET", c.ApiUrl+"/files/get_info"+url, nil)
if len(c.AuthToken) > 0 {
rq.Header.Set(HEADER_AUTH, "BEARER "+c.AuthToken)
@@ -635,7 +664,7 @@ func (c *Client) GetFileInfo(url string) (*Result, *AppError) {
}
func (c *Client) GetPublicLink(data map[string]string) (*Result, *AppError) {
if r, err := c.DoPost("/files/get_public_link", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/files/get_public_link", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -644,7 +673,7 @@ func (c *Client) GetPublicLink(data map[string]string) (*Result, *AppError) {
}
func (c *Client) UpdateUser(user *User) (*Result, *AppError) {
if r, err := c.DoPost("/users/update", user.ToJson()); err != nil {
if r, err := c.DoApiPost("/users/update", user.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -653,7 +682,7 @@ func (c *Client) UpdateUser(user *User) (*Result, *AppError) {
}
func (c *Client) UpdateUserRoles(data map[string]string) (*Result, *AppError) {
if r, err := c.DoPost("/users/update_roles", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/users/update_roles", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -665,7 +694,7 @@ func (c *Client) UpdateActive(userId string, active bool) (*Result, *AppError) {
data := make(map[string]string)
data["user_id"] = userId
data["active"] = strconv.FormatBool(active)
if r, err := c.DoPost("/users/update_active", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/users/update_active", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -674,7 +703,7 @@ func (c *Client) UpdateActive(userId string, active bool) (*Result, *AppError) {
}
func (c *Client) UpdateUserNotify(data map[string]string) (*Result, *AppError) {
if r, err := c.DoPost("/users/update_notify", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/users/update_notify", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -688,7 +717,7 @@ func (c *Client) UpdateUserPassword(userId, currentPassword, newPassword string)
data["new_password"] = newPassword
data["user_id"] = userId
if r, err := c.DoPost("/users/newpassword", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/users/newpassword", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -697,7 +726,7 @@ func (c *Client) UpdateUserPassword(userId, currentPassword, newPassword string)
}
func (c *Client) SendPasswordReset(data map[string]string) (*Result, *AppError) {
if r, err := c.DoPost("/users/send_password_reset", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/users/send_password_reset", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -706,7 +735,7 @@ func (c *Client) SendPasswordReset(data map[string]string) (*Result, *AppError)
}
func (c *Client) ResetPassword(data map[string]string) (*Result, *AppError) {
if r, err := c.DoPost("/users/reset_password", MapToJson(data)); err != nil {
if r, err := c.DoApiPost("/users/reset_password", MapToJson(data)); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -715,7 +744,7 @@ func (c *Client) ResetPassword(data map[string]string) (*Result, *AppError) {
}
func (c *Client) GetStatuses() (*Result, *AppError) {
if r, err := c.DoGet("/users/status", "", ""); err != nil {
if r, err := c.DoApiGet("/users/status", "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -724,7 +753,7 @@ func (c *Client) GetStatuses() (*Result, *AppError) {
}
func (c *Client) GetMyTeam(etag string) (*Result, *AppError) {
if r, err := c.DoGet("/teams/me", "", etag); err != nil {
if r, err := c.DoApiGet("/teams/me", "", etag); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
@@ -732,6 +761,33 @@ func (c *Client) GetMyTeam(etag string) (*Result, *AppError) {
}
}
func (c *Client) RegisterApp(app *OAuthApp) (*Result, *AppError) {
if r, err := c.DoApiPost("/oauth/register", app.ToJson()); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), OAuthAppFromJson(r.Body)}, nil
}
}
func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (*Result, *AppError) {
if r, err := c.DoApiGet("/oauth/allow?response_type="+rspType+"&client_id="+clientId+"&redirect_uri="+url.QueryEscape(redirect)+"&scope="+scope+"&state="+url.QueryEscape(state), "", ""); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
}
}
func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) {
if r, err := c.DoPost("/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil {
return nil, err
} else {
return &Result{r.Header.Get(HEADER_REQUEST_ID),
r.Header.Get(HEADER_ETAG_SERVER), AccessResponseFromJson(r.Body)}, nil
}
}
func (c *Client) MockSession(sessionToken string) {
c.AuthToken = sessionToken
}

151
model/oauth.go Normal file
View File

@@ -0,0 +1,151 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"fmt"
"io"
)
type OAuthApp struct {
Id string `json:"id"`
CreatorId string `json:"creator_id"`
CreateAt int64 `json:"update_at"`
UpdateAt int64 `json:"update_at"`
ClientSecret string `json:"client_secret"`
Name string `json:"name"`
Description string `json:"description"`
CallbackUrls StringArray `json:"callback_urls"`
Homepage string `json:"homepage"`
}
// IsValid validates the app and returns an error if it isn't configured
// correctly.
func (a *OAuthApp) IsValid() *AppError {
if len(a.Id) != 26 {
return NewAppError("OAuthApp.IsValid", "Invalid app id", "")
}
if a.CreateAt == 0 {
return NewAppError("OAuthApp.IsValid", "Create at must be a valid time", "app_id="+a.Id)
}
if a.UpdateAt == 0 {
return NewAppError("OAuthApp.IsValid", "Update at must be a valid time", "app_id="+a.Id)
}
if len(a.CreatorId) != 26 {
return NewAppError("OAuthApp.IsValid", "Invalid creator id", "app_id="+a.Id)
}
if len(a.ClientSecret) == 0 || len(a.ClientSecret) > 128 {
return NewAppError("OAuthApp.IsValid", "Invalid client secret", "app_id="+a.Id)
}
if len(a.Name) == 0 || len(a.Name) > 64 {
return NewAppError("OAuthApp.IsValid", "Invalid name", "app_id="+a.Id)
}
if len(a.CallbackUrls) == 0 || len(fmt.Sprintf("%s", a.CallbackUrls)) > 1024 {
return NewAppError("OAuthApp.IsValid", "Invalid callback urls", "app_id="+a.Id)
}
if len(a.Homepage) == 0 || len(a.Homepage) > 256 {
return NewAppError("OAuthApp.IsValid", "Invalid homepage", "app_id="+a.Id)
}
if len(a.Description) > 512 {
return NewAppError("OAuthApp.IsValid", "Invalid description", "app_id="+a.Id)
}
return nil
}
// PreSave will set the Id and ClientSecret if missing. It will also fill
// in the CreateAt, UpdateAt times. It should be run before saving the app to the db.
func (a *OAuthApp) PreSave() {
if a.Id == "" {
a.Id = NewId()
}
if a.ClientSecret == "" {
a.ClientSecret = NewId()
}
a.CreateAt = GetMillis()
a.UpdateAt = a.CreateAt
if len(a.ClientSecret) > 0 {
a.ClientSecret = HashPassword(a.ClientSecret)
}
}
// PreUpdate should be run before updating the app in the db.
func (a *OAuthApp) PreUpdate() {
a.UpdateAt = GetMillis()
}
// ToJson convert a User to a json string
func (a *OAuthApp) ToJson() string {
b, err := json.Marshal(a)
if err != nil {
return ""
} else {
return string(b)
}
}
// Generate a valid strong etag so the browser can cache the results
func (a *OAuthApp) Etag() string {
return Etag(a.Id, a.UpdateAt)
}
// Remove any private data from the app object
func (a *OAuthApp) Sanitize() {
a.ClientSecret = ""
}
func (a *OAuthApp) IsValidRedirectURL(url string) bool {
for _, u := range a.CallbackUrls {
if u == url {
return true
}
}
return false
}
// OAuthAppFromJson will decode the input and return a User
func OAuthAppFromJson(data io.Reader) *OAuthApp {
decoder := json.NewDecoder(data)
var app OAuthApp
err := decoder.Decode(&app)
if err == nil {
return &app
} else {
return nil
}
}
func OAuthAppMapToJson(a map[string]*OAuthApp) string {
b, err := json.Marshal(a)
if err != nil {
return ""
} else {
return string(b)
}
}
func OAuthAppMapFromJson(data io.Reader) map[string]*OAuthApp {
decoder := json.NewDecoder(data)
var apps map[string]*OAuthApp
err := decoder.Decode(&apps)
if err == nil {
return apps
} else {
return nil
}
}

95
model/oauth_test.go Normal file
View File

@@ -0,0 +1,95 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"strings"
"testing"
)
func TestOAuthAppJson(t *testing.T) {
a1 := OAuthApp{}
a1.Id = NewId()
a1.Name = "TestOAuthApp" + NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
a1.ClientSecret = NewId()
json := a1.ToJson()
ra1 := OAuthAppFromJson(strings.NewReader(json))
if a1.Id != ra1.Id {
t.Fatal("ids did not match")
}
}
func TestOAuthAppPreSave(t *testing.T) {
a1 := OAuthApp{}
a1.Id = NewId()
a1.Name = "TestOAuthApp" + NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
a1.ClientSecret = NewId()
a1.PreSave()
a1.Etag()
a1.Sanitize()
}
func TestOAuthAppPreUpdate(t *testing.T) {
a1 := OAuthApp{}
a1.Id = NewId()
a1.Name = "TestOAuthApp" + NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
a1.ClientSecret = NewId()
a1.PreUpdate()
}
func TestOAuthAppIsValid(t *testing.T) {
app := OAuthApp{}
if err := app.IsValid(); err == nil {
t.Fatal()
}
app.Id = NewId()
if err := app.IsValid(); err == nil {
t.Fatal()
}
app.CreateAt = 1
if err := app.IsValid(); err == nil {
t.Fatal()
}
app.UpdateAt = 1
if err := app.IsValid(); err == nil {
t.Fatal()
}
app.CreatorId = NewId()
if err := app.IsValid(); err == nil {
t.Fatal()
}
app.ClientSecret = NewId()
if err := app.IsValid(); err == nil {
t.Fatal()
}
app.Name = "TestOAuthApp"
if err := app.IsValid(); err == nil {
t.Fatal()
}
app.CallbackUrls = []string{"https://nowhere.com"}
if err := app.IsValid(); err == nil {
t.Fatal()
}
app.Homepage = "https://nowhere.com"
if err := app.IsValid(); err != nil {
t.Fatal()
}
}

View File

@@ -14,6 +14,8 @@ const (
SESSION_TIME_WEB_IN_SECS = 60 * 60 * 24 * SESSION_TIME_WEB_IN_DAYS
SESSION_TIME_MOBILE_IN_DAYS = 30
SESSION_TIME_MOBILE_IN_SECS = 60 * 60 * 24 * SESSION_TIME_MOBILE_IN_DAYS
SESSION_TIME_OAUTH_IN_DAYS = 365
SESSION_TIME_OAUTH_IN_SECS = 60 * 60 * 24 * SESSION_TIME_OAUTH_IN_DAYS
SESSION_CACHE_IN_SECS = 60 * 10
SESSION_CACHE_SIZE = 10000
SESSION_PROP_PLATFORM = "platform"
@@ -23,7 +25,7 @@ const (
type Session struct {
Id string `json:"id"`
AltId string `json:"alt_id"`
Token string `json:"token"`
CreateAt int64 `json:"create_at"`
ExpiresAt int64 `json:"expires_at"`
LastActivityAt int64 `json:"last_activity_at"`
@@ -31,6 +33,7 @@ type Session struct {
TeamId string `json:"team_id"`
DeviceId string `json:"device_id"`
Roles string `json:"roles"`
IsOAuth bool `json:"is_oauth"`
Props StringMap `json:"props"`
}
@@ -59,7 +62,7 @@ func (me *Session) PreSave() {
me.Id = NewId()
}
me.AltId = NewId()
me.Token = NewId()
me.CreateAt = GetMillis()
me.LastActivityAt = me.CreateAt
@@ -70,7 +73,7 @@ func (me *Session) PreSave() {
}
func (me *Session) Sanitize() {
me.Id = ""
me.Token = ""
}
func (me *Session) IsExpired() bool {

View File

@@ -32,6 +32,7 @@ type AppError struct {
RequestId string `json:"request_id"` // The RequestId that's also set in the header
StatusCode int `json:"status_code"` // The http status code
Where string `json:"-"` // The function where it happened in the form of Struct.Func
IsOAuth bool `json:"is_oauth"` // Whether the error is OAuth specific
}
func (er *AppError) Error() string {
@@ -65,6 +66,7 @@ func NewAppError(where string, message string, details string) *AppError {
ap.Where = where
ap.DetailedError = details
ap.StatusCode = 500
ap.IsOAuth = false
return ap
}

334
store/sql_oauth_store.go Normal file
View File

@@ -0,0 +1,334 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package store
import (
"github.com/mattermost/platform/model"
"strings"
)
type SqlOAuthStore struct {
*SqlStore
}
func NewSqlOAuthStore(sqlStore *SqlStore) OAuthStore {
as := &SqlOAuthStore{sqlStore}
for _, db := range sqlStore.GetAllConns() {
table := db.AddTableWithName(model.OAuthApp{}, "OAuthApps").SetKeys(false, "Id")
table.ColMap("Id").SetMaxSize(26)
table.ColMap("CreatorId").SetMaxSize(26)
table.ColMap("ClientSecret").SetMaxSize(128)
table.ColMap("Name").SetMaxSize(64)
table.ColMap("Description").SetMaxSize(512)
table.ColMap("CallbackUrls").SetMaxSize(1024)
table.ColMap("Homepage").SetMaxSize(256)
tableAuth := db.AddTableWithName(model.AuthData{}, "OAuthAuthData").SetKeys(false, "Code")
tableAuth.ColMap("UserId").SetMaxSize(26)
tableAuth.ColMap("ClientId").SetMaxSize(26)
tableAuth.ColMap("Code").SetMaxSize(128)
tableAuth.ColMap("RedirectUri").SetMaxSize(256)
tableAuth.ColMap("State").SetMaxSize(128)
tableAuth.ColMap("Scope").SetMaxSize(128)
tableAccess := db.AddTableWithName(model.AccessData{}, "OAuthAccessData").SetKeys(false, "Token")
tableAccess.ColMap("AuthCode").SetMaxSize(128)
tableAccess.ColMap("Token").SetMaxSize(26)
tableAccess.ColMap("RefreshToken").SetMaxSize(26)
tableAccess.ColMap("RedirectUri").SetMaxSize(256)
}
return as
}
func (as SqlOAuthStore) UpgradeSchemaIfNeeded() {
}
func (as SqlOAuthStore) CreateIndexesIfNotExists() {
as.CreateIndexIfNotExists("idx_oauthapps_creator_id", "OAuthApps", "CreatorId")
as.CreateIndexIfNotExists("idx_oauthaccessdata_auth_code", "OAuthAccessData", "AuthCode")
as.CreateIndexIfNotExists("idx_oauthauthdata_client_id", "OAuthAuthData", "Code")
}
func (as SqlOAuthStore) SaveApp(app *model.OAuthApp) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if len(app.Id) > 0 {
result.Err = model.NewAppError("SqlOAuthStore.SaveApp", "Must call update for exisiting app", "app_id="+app.Id)
storeChannel <- result
close(storeChannel)
return
}
app.PreSave()
if result.Err = app.IsValid(); result.Err != nil {
storeChannel <- result
close(storeChannel)
return
}
if err := as.GetMaster().Insert(app); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.SaveApp", "We couldn't save the app.", "app_id="+app.Id+", "+err.Error())
} else {
result.Data = app
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) UpdateApp(app *model.OAuthApp) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
app.PreUpdate()
if result.Err = app.IsValid(); result.Err != nil {
storeChannel <- result
close(storeChannel)
return
}
if oldAppResult, err := as.GetMaster().Get(model.OAuthApp{}, app.Id); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We encounted an error finding the app", "app_id="+app.Id+", "+err.Error())
} else if oldAppResult == nil {
result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We couldn't find the existing app to update", "app_id="+app.Id)
} else {
oldApp := oldAppResult.(*model.OAuthApp)
app.CreateAt = oldApp.CreateAt
app.ClientSecret = oldApp.ClientSecret
app.CreatorId = oldApp.CreatorId
if count, err := as.GetMaster().Update(app); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We encounted an error updating the app", "app_id="+app.Id+", "+err.Error())
} else if count != 1 {
result.Err = model.NewAppError("SqlOAuthStore.UpdateApp", "We couldn't update the app", "app_id="+app.Id)
} else {
result.Data = [2]*model.OAuthApp{app, oldApp}
}
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) GetApp(id string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if obj, err := as.GetReplica().Get(model.OAuthApp{}, id); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.GetApp", "We encounted an error finding the app", "app_id="+id+", "+err.Error())
} else if obj == nil {
result.Err = model.NewAppError("SqlOAuthStore.GetApp", "We couldn't find the existing app", "app_id="+id)
} else {
result.Data = obj.(*model.OAuthApp)
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) GetAppByUser(userId string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
var apps []*model.OAuthApp
if _, err := as.GetReplica().Select(&apps, "SELECT * FROM OAuthApps WHERE CreatorId = :UserId", map[string]interface{}{"UserId": userId}); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.GetAppByUser", "We couldn't find any existing apps", "user_id="+userId+", "+err.Error())
}
result.Data = apps
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) SaveAccessData(accessData *model.AccessData) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if result.Err = accessData.IsValid(); result.Err != nil {
storeChannel <- result
close(storeChannel)
return
}
if err := as.GetMaster().Insert(accessData); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.SaveAccessData", "We couldn't save the access token.", err.Error())
} else {
result.Data = accessData
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) GetAccessData(token string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
accessData := model.AccessData{}
if err := as.GetReplica().SelectOne(&accessData, "SELECT * FROM OAuthAccessData WHERE Token = :Token", map[string]interface{}{"Token": token}); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.GetAccessData", "We encounted an error finding the access token", err.Error())
} else {
result.Data = &accessData
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) GetAccessDataByAuthCode(authCode string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
accessData := model.AccessData{}
if err := as.GetReplica().SelectOne(&accessData, "SELECT * FROM OAuthAccessData WHERE AuthCode = :AuthCode", map[string]interface{}{"AuthCode": authCode}); err != nil {
if strings.Contains(err.Error(), "no rows") {
result.Data = nil
} else {
result.Err = model.NewAppError("SqlOAuthStore.GetAccessDataByAuthCode", "We encountered an error finding the access token", err.Error())
}
} else {
result.Data = &accessData
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) RemoveAccessData(token string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if _, err := as.GetMaster().Exec("DELETE FROM OAuthAccessData WHERE Token = :Token", map[string]interface{}{"Token": token}); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.RemoveAccessData", "We couldn't remove the access token", "err="+err.Error())
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) SaveAuthData(authData *model.AuthData) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
authData.PreSave()
if result.Err = authData.IsValid(); result.Err != nil {
storeChannel <- result
close(storeChannel)
return
}
if err := as.GetMaster().Insert(authData); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.SaveAuthData", "We couldn't save the authorization code.", err.Error())
} else {
result.Data = authData
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) GetAuthData(code string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if obj, err := as.GetReplica().Get(model.AuthData{}, code); err != nil {
result.Err = model.NewAppError("SqlOAuthStore.GetAuthData", "We encounted an error finding the authorization code", err.Error())
} else if obj == nil {
result.Err = model.NewAppError("SqlOAuthStore.GetAuthData", "We couldn't find the existing authorization code", "")
} else {
result.Data = obj.(*model.AuthData)
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (as SqlOAuthStore) RemoveAuthData(code string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
_, err := as.GetMaster().Exec("DELETE FROM OAuthAuthData WHERE Code = :Code", map[string]interface{}{"Code": code})
if err != nil {
result.Err = model.NewAppError("SqlOAuthStore.RemoveAuthData", "We couldn't remove the authorization code", "err="+err.Error())
}
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}

View File

@@ -0,0 +1,182 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package store
import (
"github.com/mattermost/platform/model"
"testing"
)
func TestOAuthStoreSaveApp(t *testing.T) {
Setup()
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
if err := (<-store.OAuth().SaveApp(&a1)).Err; err != nil {
t.Fatal(err)
}
}
func TestOAuthStoreGetApp(t *testing.T) {
Setup()
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
Must(store.OAuth().SaveApp(&a1))
if err := (<-store.OAuth().GetApp(a1.Id)).Err; err != nil {
t.Fatal(err)
}
if err := (<-store.OAuth().GetAppByUser(a1.CreatorId)).Err; err != nil {
t.Fatal(err)
}
}
func TestOAuthStoreUpdateApp(t *testing.T) {
Setup()
a1 := model.OAuthApp{}
a1.CreatorId = model.NewId()
a1.Name = "TestApp" + model.NewId()
a1.CallbackUrls = []string{"https://nowhere.com"}
a1.Homepage = "https://nowhere.com"
Must(store.OAuth().SaveApp(&a1))
a1.CreateAt = 1
a1.ClientSecret = "pwd"
a1.CreatorId = "12345678901234567890123456"
a1.Name = "NewName"
if result := <-store.OAuth().UpdateApp(&a1); result.Err != nil {
t.Fatal(result.Err)
} else {
ua1 := (result.Data.([2]*model.OAuthApp)[0])
if ua1.Name != "NewName" {
t.Fatal("name did not update")
}
if ua1.CreateAt == 1 {
t.Fatal("create at should not have updated")
}
if ua1.ClientSecret == "pwd" {
t.Fatal("client secret should not have updated")
}
if ua1.CreatorId == "12345678901234567890123456" {
t.Fatal("creator id should not have updated")
}
}
}
func TestOAuthStoreSaveAccessData(t *testing.T) {
Setup()
a1 := model.AccessData{}
a1.AuthCode = model.NewId()
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
if err := (<-store.OAuth().SaveAccessData(&a1)).Err; err != nil {
t.Fatal(err)
}
}
func TestOAuthStoreGetAccessData(t *testing.T) {
Setup()
a1 := model.AccessData{}
a1.AuthCode = model.NewId()
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
Must(store.OAuth().SaveAccessData(&a1))
if result := <-store.OAuth().GetAccessData(a1.Token); result.Err != nil {
t.Fatal(result.Err)
} else {
ra1 := result.Data.(*model.AccessData)
if a1.Token != ra1.Token {
t.Fatal("tokens didn't match")
}
}
if err := (<-store.OAuth().GetAccessDataByAuthCode(a1.AuthCode)).Err; err != nil {
t.Fatal(err)
}
if err := (<-store.OAuth().GetAccessDataByAuthCode("junk")).Err; err != nil {
t.Fatal(err)
}
}
func TestOAuthStoreRemoveAccessData(t *testing.T) {
Setup()
a1 := model.AccessData{}
a1.AuthCode = model.NewId()
a1.Token = model.NewId()
a1.RefreshToken = model.NewId()
Must(store.OAuth().SaveAccessData(&a1))
if err := (<-store.OAuth().RemoveAccessData(a1.Token)).Err; err != nil {
t.Fatal(err)
}
if result := <-store.OAuth().GetAccessDataByAuthCode(a1.AuthCode); result.Err != nil {
t.Fatal(result.Err)
} else {
if result.Data != nil {
t.Fatal("did not delete access token")
}
}
}
func TestOAuthStoreSaveAuthData(t *testing.T) {
Setup()
a1 := model.AuthData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Code = model.NewId()
if err := (<-store.OAuth().SaveAuthData(&a1)).Err; err != nil {
t.Fatal(err)
}
}
func TestOAuthStoreGetAuthData(t *testing.T) {
Setup()
a1 := model.AuthData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Code = model.NewId()
Must(store.OAuth().SaveAuthData(&a1))
if err := (<-store.OAuth().GetAuthData(a1.Code)).Err; err != nil {
t.Fatal(err)
}
}
func TestOAuthStoreRemoveAuthData(t *testing.T) {
Setup()
a1 := model.AuthData{}
a1.ClientId = model.NewId()
a1.UserId = model.NewId()
a1.Code = model.NewId()
Must(store.OAuth().SaveAuthData(&a1))
if err := (<-store.OAuth().RemoveAuthData(a1.Code)).Err; err != nil {
t.Fatal(err)
}
if err := (<-store.OAuth().GetAuthData(a1.Code)).Err; err == nil {
t.Fatal("should have errored - auth code removed")
}
}

View File

@@ -18,7 +18,7 @@ func NewSqlSessionStore(sqlStore *SqlStore) SessionStore {
for _, db := range sqlStore.GetAllConns() {
table := db.AddTableWithName(model.Session{}, "Sessions").SetKeys(false, "Id")
table.ColMap("Id").SetMaxSize(26)
table.ColMap("AltId").SetMaxSize(26)
table.ColMap("Token").SetMaxSize(26)
table.ColMap("UserId").SetMaxSize(26)
table.ColMap("TeamId").SetMaxSize(26)
table.ColMap("DeviceId").SetMaxSize(128)
@@ -34,7 +34,7 @@ func (me SqlSessionStore) UpgradeSchemaIfNeeded() {
func (me SqlSessionStore) CreateIndexesIfNotExists() {
me.CreateIndexIfNotExists("idx_sessions_user_id", "Sessions", "UserId")
me.CreateIndexIfNotExists("idx_sessions_alt_id", "Sessions", "AltId")
me.CreateIndexIfNotExists("idx_sessions_token", "Sessions", "Token")
}
func (me SqlSessionStore) Save(session *model.Session) StoreChannel {
@@ -70,19 +70,21 @@ func (me SqlSessionStore) Save(session *model.Session) StoreChannel {
return storeChannel
}
func (me SqlSessionStore) Get(id string) StoreChannel {
func (me SqlSessionStore) Get(sessionIdOrToken string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
if obj, err := me.GetReplica().Get(model.Session{}, id); err != nil {
result.Err = model.NewAppError("SqlSessionStore.Get", "We encounted an error finding the session", "id="+id+", "+err.Error())
} else if obj == nil {
result.Err = model.NewAppError("SqlSessionStore.Get", "We couldn't find the existing session", "id="+id)
var sessions []*model.Session
if _, err := me.GetReplica().Select(&sessions, "SELECT * FROM Sessions WHERE Token = :Token OR Id = :Id LIMIT 1", map[string]interface{}{"Token": sessionIdOrToken, "Id": sessionIdOrToken}); err != nil {
result.Err = model.NewAppError("SqlSessionStore.Get", "We encounted an error finding the session", "sessionIdOrToken="+sessionIdOrToken+", "+err.Error())
} else if sessions == nil || len(sessions) == 0 {
result.Err = model.NewAppError("SqlSessionStore.Get", "We encounted an error finding the session", "sessionIdOrToken="+sessionIdOrToken)
} else {
result.Data = obj.(*model.Session)
result.Data = sessions[0]
}
storeChannel <- result
@@ -120,15 +122,15 @@ func (me SqlSessionStore) GetSessions(userId string) StoreChannel {
return storeChannel
}
func (me SqlSessionStore) Remove(sessionIdOrAlt string) StoreChannel {
func (me SqlSessionStore) Remove(sessionIdOrToken string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
_, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE Id = :Id Or AltId = :AltId", map[string]interface{}{"Id": sessionIdOrAlt, "AltId": sessionIdOrAlt})
_, err := me.GetMaster().Exec("DELETE FROM Sessions WHERE Id = :Id Or Token = :Token", map[string]interface{}{"Id": sessionIdOrToken, "Token": sessionIdOrToken})
if err != nil {
result.Err = model.NewAppError("SqlSessionStore.RemoveSession", "We couldn't remove the session", "id="+sessionIdOrAlt+", err="+err.Error())
result.Err = model.NewAppError("SqlSessionStore.RemoveSession", "We couldn't remove the session", "id="+sessionIdOrToken+", err="+err.Error())
}
storeChannel <- result
@@ -181,7 +183,6 @@ func (me SqlSessionStore) UpdateRoles(userId, roles string) StoreChannel {
go func() {
result := StoreResult{}
if _, err := me.GetMaster().Exec("UPDATE Sessions SET Roles = :Roles WHERE UserId = :UserId", map[string]interface{}{"Roles": roles, "UserId": userId}); err != nil {
result.Err = model.NewAppError("SqlSessionStore.UpdateRoles", "We couldn't update the roles", "userId="+userId)
} else {

View File

@@ -80,7 +80,7 @@ func TestSessionRemove(t *testing.T) {
}
}
func TestSessionRemoveAlt(t *testing.T) {
func TestSessionRemoveToken(t *testing.T) {
Setup()
s1 := model.Session{}
@@ -96,7 +96,7 @@ func TestSessionRemoveAlt(t *testing.T) {
}
}
Must(store.Session().Remove(s1.AltId))
Must(store.Session().Remove(s1.Token))
if rs2 := (<-store.Session().Get(s1.Id)); rs2.Err == nil {
t.Fatal("should have been removed")

View File

@@ -38,6 +38,7 @@ type SqlStore struct {
user UserStore
audit AuditStore
session SessionStore
oauth OAuthStore
}
func NewSqlStore() Store {
@@ -55,28 +56,36 @@ func NewSqlStore() Store {
utils.Cfg.SqlSettings.Trace)
}
// Temporary upgrade code, remove after 0.8.0 release
if sqlStore.DoesColumnExist("Sessions", "AltId") {
sqlStore.GetMaster().Exec("DROP TABLE IF EXISTS Sessions")
}
sqlStore.team = NewSqlTeamStore(sqlStore)
sqlStore.channel = NewSqlChannelStore(sqlStore)
sqlStore.post = NewSqlPostStore(sqlStore)
sqlStore.user = NewSqlUserStore(sqlStore)
sqlStore.audit = NewSqlAuditStore(sqlStore)
sqlStore.session = NewSqlSessionStore(sqlStore)
sqlStore.oauth = NewSqlOAuthStore(sqlStore)
sqlStore.master.CreateTablesIfNotExists()
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
sqlStore.post.(*SqlPostStore).CreateIndexesIfNotExists()
sqlStore.user.(*SqlUserStore).CreateIndexesIfNotExists()
sqlStore.audit.(*SqlAuditStore).CreateIndexesIfNotExists()
sqlStore.session.(*SqlSessionStore).CreateIndexesIfNotExists()
sqlStore.team.(*SqlTeamStore).UpgradeSchemaIfNeeded()
sqlStore.channel.(*SqlChannelStore).UpgradeSchemaIfNeeded()
sqlStore.post.(*SqlPostStore).UpgradeSchemaIfNeeded()
sqlStore.user.(*SqlUserStore).UpgradeSchemaIfNeeded()
sqlStore.audit.(*SqlAuditStore).UpgradeSchemaIfNeeded()
sqlStore.session.(*SqlSessionStore).UpgradeSchemaIfNeeded()
sqlStore.oauth.(*SqlOAuthStore).UpgradeSchemaIfNeeded()
sqlStore.team.(*SqlTeamStore).CreateIndexesIfNotExists()
sqlStore.channel.(*SqlChannelStore).CreateIndexesIfNotExists()
sqlStore.post.(*SqlPostStore).CreateIndexesIfNotExists()
sqlStore.user.(*SqlUserStore).CreateIndexesIfNotExists()
sqlStore.audit.(*SqlAuditStore).CreateIndexesIfNotExists()
sqlStore.session.(*SqlSessionStore).CreateIndexesIfNotExists()
sqlStore.oauth.(*SqlOAuthStore).CreateIndexesIfNotExists()
return sqlStore
}
@@ -363,6 +372,10 @@ func (ss SqlStore) Audit() AuditStore {
return ss.audit
}
func (ss SqlStore) OAuth() OAuthStore {
return ss.oauth
}
type mattermConverter struct{}
func (me mattermConverter) ToDb(val interface{}) (interface{}, error) {

View File

@@ -34,6 +34,7 @@ type Store interface {
User() UserStore
Audit() AuditStore
Session() SessionStore
OAuth() OAuthStore
Close()
}
@@ -104,9 +105,9 @@ type UserStore interface {
type SessionStore interface {
Save(session *model.Session) StoreChannel
Get(id string) StoreChannel
Get(sessionIdOrToken string) StoreChannel
GetSessions(userId string) StoreChannel
Remove(sessionIdOrAlt string) StoreChannel
Remove(sessionIdOrToken string) StoreChannel
UpdateLastActivityAt(sessionId string, time int64) StoreChannel
UpdateRoles(userId string, roles string) StoreChannel
}
@@ -115,3 +116,17 @@ type AuditStore interface {
Save(audit *model.Audit) StoreChannel
Get(user_id string, limit int) StoreChannel
}
type OAuthStore interface {
SaveApp(app *model.OAuthApp) StoreChannel
UpdateApp(app *model.OAuthApp) StoreChannel
GetApp(id string) StoreChannel
GetAppByUser(userId string) StoreChannel
SaveAuthData(authData *model.AuthData) StoreChannel
GetAuthData(code string) StoreChannel
RemoveAuthData(code string) StoreChannel
SaveAccessData(accessData *model.AccessData) StoreChannel
GetAccessData(token string) StoreChannel
GetAccessDataByAuthCode(authCode string) StoreChannel
RemoveAccessData(token string) StoreChannel
}

View File

@@ -21,20 +21,21 @@ const (
)
type ServiceSettings struct {
SiteName string
Mode string
AllowTesting bool
UseSSL bool
Port string
Version string
InviteSalt string
PublicLinkSalt string
ResetSalt string
AnalyticsUrl string
UseLocalStorage bool
StorageDirectory string
AllowedLoginAttempts int
DisableEmailSignUp bool
SiteName string
Mode string
AllowTesting bool
UseSSL bool
Port string
Version string
InviteSalt string
PublicLinkSalt string
ResetSalt string
AnalyticsUrl string
UseLocalStorage bool
StorageDirectory string
AllowedLoginAttempts int
DisableEmailSignUp bool
EnableOAuthServiceProvider bool
}
type SSOSetting struct {
@@ -286,6 +287,7 @@ func getClientProperties(c *Config) map[string]string {
props["ProfileHeight"] = fmt.Sprintf("%v", c.ImageSettings.ProfileHeight)
props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth)
props["ProfileWidth"] = fmt.Sprintf("%v", c.ImageSettings.ProfileWidth)
props["EnableOAuthServiceProvider"] = strconv.FormatBool(c.ServiceSettings.EnableOAuthServiceProvider)
return props
}

View File

@@ -0,0 +1,72 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../utils/client.jsx');
export default class Authorize extends React.Component {
constructor(props) {
super(props);
this.handleAllow = this.handleAllow.bind(this);
this.handleDeny = this.handleDeny.bind(this);
this.state = {};
}
handleAllow() {
const responseType = this.props.responseType;
const clientId = this.props.clientId;
const redirectUri = this.props.redirectUri;
const state = this.props.state;
const scope = this.props.scope;
Client.allowOAuth2(responseType, clientId, redirectUri, state, scope,
(data) => {
if (data.redirect) {
window.location.replace(data.redirect);
}
},
() => {}
);
}
handleDeny() {
window.location.replace(this.props.redirectUri + '?error=access_denied');
}
render() {
return (
<div className='authorize-box'>
<div className='authorize-inner'>
<h3>{'An application would like to connect to your '}{this.props.teamName}{' account'}</h3>
<label>{'The app '}{this.props.appName}{' would like the ability to access and modify your basic information.'}</label>
<br/>
<br/>
<label>{'Allow '}{this.props.appName}{' access?'}</label>
<br/>
<button
type='submit'
className='btn authorize-btn'
onClick={this.handleDeny}
>
{'Deny'}
</button>
<button
type='submit'
className='btn btn-primary authorize-btn'
onClick={this.handleAllow}
>
{'Allow'}
</button>
</div>
</div>
);
}
}
Authorize.propTypes = {
appName: React.PropTypes.string,
teamName: React.PropTypes.string,
responseType: React.PropTypes.string,
clientId: React.PropTypes.string,
redirectUri: React.PropTypes.string,
state: React.PropTypes.string,
scope: React.PropTypes.string
};

View File

@@ -25,7 +25,7 @@ export default class PopoverListMembers extends React.Component {
$('#member_popover').popover({placement: 'bottom', trigger: 'click', html: true});
$('body').on('click', function onClick(e) {
if ($(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) {
if (e.target.parentNode && $(e.target.parentNode.parentNode)[0] !== $('#member_popover')[0] && $(e.target).parents('.popover.in').length === 0) {
$('#member_popover').popover('hide');
}
});

View File

@@ -0,0 +1,249 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Client = require('../utils/client.jsx');
export default class RegisterAppModal extends React.Component {
constructor() {
super();
this.register = this.register.bind(this);
this.onHide = this.onHide.bind(this);
this.save = this.save.bind(this);
this.state = {clientId: '', clientSecret: '', saved: false};
}
componentDidMount() {
$(React.findDOMNode(this)).on('hide.bs.modal', this.onHide);
}
register() {
var state = this.state;
state.serverError = null;
var app = {};
var name = this.refs.name.getDOMNode().value;
if (!name || name.length === 0) {
state.nameError = 'Application name must be filled in.';
this.setState(state);
return;
}
state.nameError = null;
app.name = name;
var homepage = this.refs.homepage.getDOMNode().value;
if (!homepage || homepage.length === 0) {
state.homepageError = 'Homepage must be filled in.';
this.setState(state);
return;
}
state.homepageError = null;
app.homepage = homepage;
var desc = this.refs.desc.getDOMNode().value;
app.description = desc;
var rawCallbacks = this.refs.callback.getDOMNode().value.trim();
if (!rawCallbacks || rawCallbacks.length === 0) {
state.callbackError = 'At least one callback URL must be filled in.';
this.setState(state);
return;
}
state.callbackError = null;
app.callback_urls = rawCallbacks.split('\n');
Client.registerOAuthApp(app,
(data) => {
state.clientId = data.id;
state.clientSecret = data.client_secret;
this.setState(state);
},
(err) => {
state.serverError = err.message;
this.setState(state);
}
);
}
onHide(e) {
if (!this.state.saved && this.state.clientId !== '') {
e.preventDefault();
return;
}
this.setState({clientId: '', clientSecret: '', saved: false});
}
save() {
this.setState({saved: this.refs.save.getDOMNode().checked});
}
render() {
var nameError;
if (this.state.nameError) {
nameError = <div className='form-group has-error'><label className='control-label'>{this.state.nameError}</label></div>;
}
var homepageError;
if (this.state.homepageError) {
homepageError = <div className='form-group has-error'><label className='control-label'>{this.state.homepageError}</label></div>;
}
var callbackError;
if (this.state.callbackError) {
callbackError = <div className='form-group has-error'><label className='control-label'>{this.state.callbackError}</label></div>;
}
var serverError;
if (this.state.serverError) {
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
}
var body = '';
if (this.state.clientId === '') {
body = (
<div className='form-group user-settings'>
<h3>{'Register a New Application'}</h3>
<br/>
<label className='col-sm-4 control-label'>{'Application Name'}</label>
<div className='col-sm-7'>
<input
ref='name'
className='form-control'
type='text'
placeholder='Required'
/>
{nameError}
</div>
<br/>
<br/>
<label className='col-sm-4 control-label'>{'Homepage URL'}</label>
<div className='col-sm-7'>
<input
ref='homepage'
className='form-control'
type='text'
placeholder='Required'
/>
{homepageError}
</div>
<br/>
<br/>
<label className='col-sm-4 control-label'>{'Description'}</label>
<div className='col-sm-7'>
<input
ref='desc'
className='form-control'
type='text'
placeholder='Optional'
/>
</div>
<br/>
<br/>
<label className='col-sm-4 control-label'>{'Callback URL'}</label>
<div className='col-sm-7'>
<textarea
ref='callback'
className='form-control'
type='text'
placeholder='Required'
rows='5'
/>
{callbackError}
</div>
<br/>
<br/>
<br/>
<br/>
<br/>
{serverError}
<a
className='btn btn-sm theme pull-right'
href='#'
data-dismiss='modal'
aria-label='Close'
>
{'Cancel'}
</a>
<a
className='btn btn-sm btn-primary pull-right'
onClick={this.register}
>
{'Register'}
</a>
</div>
);
} else {
var btnClass = ' disabled';
if (this.state.saved) {
btnClass = '';
}
body = (
<div className='form-group user-settings'>
<h3>{'Your Application Credentials'}</h3>
<br/>
<br/>
<label className='col-sm-12 control-label'>{'Client ID: '}{this.state.clientId}</label>
<label className='col-sm-12 control-label'>{'Client Secret: '}{this.state.clientSecret}</label>
<br/>
<br/>
<br/>
<br/>
<strong>{'Save these somewhere SAFE and SECURE. We can retrieve your Client Id if you lose it, but your Client Secret will be lost forever if you were to lose it.'}</strong>
<br/>
<br/>
<div className='checkbox'>
<label>
<input
ref='save'
type='checkbox'
checked={this.state.saved}
onClick={this.save}
>
{'I have saved both my Client Id and Client Secret somewhere safe'}
</input>
</label>
</div>
<a
className={'btn btn-sm btn-primary pull-right' + btnClass}
href='#'
data-dismiss='modal'
aria-label='Close'
>
{'Close'}
</a>
</div>
);
}
return (
<div
className='modal fade'
ref='modal'
id='register_app'
role='dialog'
aria-hidden='true'
>
<div className='modal-dialog'>
<div className='modal-content'>
<div className='modal-header'>
<button
type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>{'x'}</span>
</button>
<h4
className='modal-title'
ref='title'
>
{'Developer Applications'}
</h4>
</div>
<div className='modal-body'>
{body}
</div>
</div>
</div>
</div>
);
}
}

View File

@@ -7,6 +7,7 @@ var NotificationsTab = require('./user_settings_notifications.jsx');
var SecurityTab = require('./user_settings_security.jsx');
var GeneralTab = require('./user_settings_general.jsx');
var AppearanceTab = require('./user_settings_appearance.jsx');
var DeveloperTab = require('./user_settings_developer.jsx');
export default class UserSettings extends React.Component {
constructor(props) {
@@ -76,6 +77,15 @@ export default class UserSettings extends React.Component {
/>
</div>
);
} else if (this.props.activeTab === 'developer') {
return (
<div>
<DeveloperTab
activeSection={this.props.activeSection}
updateSection={this.props.updateSection}
/>
</div>
);
}
return <div/>;

View File

@@ -0,0 +1,93 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var SettingItemMin = require('./setting_item_min.jsx');
var SettingItemMax = require('./setting_item_max.jsx');
export default class DeveloperTab extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
register() {
$('#user_settings1').modal('hide');
$('#register_app').modal('show');
}
render() {
var appSection;
var self = this;
if (this.props.activeSection === 'app') {
var inputs = [];
inputs.push(
<div className='form-group'>
<div className='col-sm-7'>
<a
className='btn btn-sm btn-primary'
onClick={this.register}
>
{'Register New Application'}
</a>
</div>
</div>
);
appSection = (
<SettingItemMax
title='Applications (Preview)'
inputs={inputs}
updateSection={function updateSection(e) {
self.props.updateSection('');
e.preventDefault();
}}
/>
);
} else {
appSection = (
<SettingItemMin
title='Applications (Preview)'
describe='Open to register a new third-party application'
updateSection={function updateSection() {
self.props.updateSection('app');
}}
/>
);
}
return (
<div>
<div className='modal-header'>
<button
type='button'
className='close'
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>{'x'}</span>
</button>
<h4
className='modal-title'
ref='title'
>
<i className='modal-back'></i>{'Developer Settings'}
</h4>
</div>
<div className='user-settings'>
<h3 className='tab-header'>{'Developer Settings'}</h3>
<div className='divider-dark first'/>
{appSection}
<div className='divider-dark'/>
</div>
</div>
);
}
}
DeveloperTab.defaultProps = {
activeSection: ''
};
DeveloperTab.propTypes = {
activeSection: React.PropTypes.string,
updateSection: React.PropTypes.func
};

View File

@@ -17,8 +17,8 @@ export default class UserSettingsModal extends React.Component {
$('body').on('click', '.modal-back', function changeDisplay() {
$(this).closest('.modal-dialog').removeClass('display--content');
});
$('body').on('click', '.modal-header .close', function closeModal() {
setTimeout(function finishClose() {
$('body').on('click', '.modal-header .close', () => {
setTimeout(() => {
$('.modal-dialog.display--content').removeClass('display--content');
}, 500);
});
@@ -35,6 +35,9 @@ export default class UserSettingsModal extends React.Component {
tabs.push({name: 'security', uiName: 'Security', icon: 'glyphicon glyphicon-lock'});
tabs.push({name: 'notifications', uiName: 'Notifications', icon: 'glyphicon glyphicon-exclamation-sign'});
tabs.push({name: 'appearance', uiName: 'Appearance', icon: 'glyphicon glyphicon-wrench'});
if (global.window.config.EnableOAuthServiceProvider) {
tabs.push({name: 'developer', uiName: 'Developer', icon: 'glyphicon glyphicon-th'});
}
return (
<div
@@ -54,13 +57,13 @@ export default class UserSettingsModal extends React.Component {
data-dismiss='modal'
aria-label='Close'
>
<span aria-hidden='true'>&times;</span>
<span aria-hidden='true'>{'x'}</span>
</button>
<h4
className='modal-title'
ref='title'
>
Account Settings
{'Account Settings'}
</h4>
</div>
<div className='modal-body'>

View File

@@ -0,0 +1,21 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var Authorize = require('../components/authorize.jsx');
function setupAuthorizePage(teamName, appName, responseType, clientId, redirectUri, scope, state) {
React.render(
<Authorize
teamName={teamName}
appName={appName}
responseType={responseType}
clientId={clientId}
redirectUri={redirectUri}
scope={scope}
state={state}
/>,
document.getElementById('authorize')
);
}
global.window.setup_authorize_page = setupAuthorizePage;

View File

@@ -33,6 +33,7 @@ var AccessHistoryModal = require('../components/access_history_modal.jsx');
var ActivityLogModal = require('../components/activity_log_modal.jsx');
var RemovedFromChannelModal = require('../components/removed_from_channel_modal.jsx');
var FileUploadOverlay = require('../components/file_upload_overlay.jsx');
var RegisterAppModal = require('../components/register_app_modal.jsx');
var Constants = require('../utils/constants.jsx');
var ActionTypes = Constants.ActionTypes;
@@ -222,6 +223,11 @@ function setupChannelPage(props) {
/>,
document.getElementById('file_upload_overlay')
);
React.render(
<RegisterAppModal />,
document.getElementById('register_app_modal')
);
}
global.window.setup_channel_page = setupChannelPage;

View File

@@ -987,3 +987,36 @@ export function updateValetFeature(data, success, error) {
track('api', 'api_teams_update_valet_feature');
}
export function registerOAuthApp(app, success, error) {
$.ajax({
url: '/api/v1/oauth/register',
dataType: 'json',
contentType: 'application/json',
type: 'POST',
data: JSON.stringify(app),
success: success,
error: (xhr, status, err) => {
const e = handleError('registerApp', xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_apps_register');
}
export function allowOAuth2(responseType, clientId, redirectUri, state, scope, success, error) {
$.ajax({
url: '/api/v1/oauth/allow?response_type=' + responseType + '&client_id=' + clientId + '&redirect_uri=' + redirectUri + '&scope=' + scope + '&state=' + state,
dataType: 'json',
contentType: 'application/json',
type: 'GET',
success: success,
error: (xhr, status, err) => {
const e = handleError('allowOAuth2', xhr, status, err);
error(e);
}
});
module.exports.track('api', 'api_users_allow_oauth2');
}

View File

@@ -315,3 +315,18 @@
}
}
.authorize-box {
margin: 100px auto;
width:500px;
height:280px;
border: 1px solid black;
}
.authorize-inner {
padding: 20px;
}
.authorize-btn {
margin-right: 6px;
}

View File

@@ -0,0 +1,26 @@
{{define "authorize"}}
<html>
{{template "head" . }}
<body class="white">
<div class="container-fluid">
<div class="inner__wrap">
<div class="row content">
<div class="signup-header">
{{.Props.TeamName}}
</div>
<div class="col-sm-12">
<div id="authorize"></div>
</div>
<div class="footer-push"></div>
</div>
<div class="row footer">
{{template "footer" . }}
</div>
</div>
</div>
<script>
window.setup_authorize_page('{{ .Props.TeamName }}', '{{ .Props.AppName }}', '{{ .Props.ResponseType }}', '{{ .Props.ClientId }}', '{{ .Props.RedirectUri }}', '{{ .Props.Scope }}', '{{ .Props.State }}' );
</script>
</body>
</html>
{{end}}

View File

@@ -49,6 +49,7 @@
<div id="access_history_modal"></div>
<div id="activity_log_modal"></div>
<div id="removed_from_channel_modal"></div>
<div id="register_app_modal"></div>
<script>
window.setup_channel_page({{ .Props }});
$('body').tooltip( {selector: '[data-toggle=tooltip]'} );

View File

@@ -4,19 +4,18 @@
package web
import (
"fmt"
"html/template"
"net/http"
"strconv"
"strings"
l4g "code.google.com/p/log4go"
"fmt"
"github.com/gorilla/mux"
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/utils"
"github.com/mssola/user_agent"
"gopkg.in/fsnotify.v1"
"html/template"
"net/http"
"strconv"
"strings"
)
var Templates *template.Template
@@ -50,6 +49,8 @@ func InitWeb() {
mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir))))
mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET")
mainrouter.Handle("/oauth/authorize", api.UserRequired(authorizeOAuth)).Methods("GET")
mainrouter.Handle("/oauth/access_token", api.ApiAppHandler(getAccessToken)).Methods("POST")
mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET")
mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET")
@@ -63,7 +64,7 @@ func InitWeb() {
mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET")
// ----------------------------------------------------------------------------------------------
// *ANYTHING* team spefic should go below this line
// *ANYTHING* team specific should go below this line
// ----------------------------------------------------------------------------------------------
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET")
@@ -648,3 +649,192 @@ func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) {
page := NewHtmlTemplatePage("admin_console", "Admin Console")
page.Render(c, w)
}
func authorizeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewAppError("authorizeOAuth", "The system admin has turned off OAuth service providing.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
if !CheckBrowserCompatability(c, r) {
return
}
responseType := r.URL.Query().Get("response_type")
clientId := r.URL.Query().Get("client_id")
redirect := r.URL.Query().Get("redirect_uri")
scope := r.URL.Query().Get("scope")
state := r.URL.Query().Get("state")
if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 {
c.Err = model.NewAppError("authorizeOAuth", "Missing one or more of response_type, client_id, or redirect_uri", "")
return
}
var app *model.OAuthApp
if result := <-api.Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
c.Err = result.Err
return
} else {
app = result.Data.(*model.OAuthApp)
}
var team *model.Team
if result := <-api.Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
page := NewHtmlTemplatePage("authorize", "Authorize Application")
page.Props["TeamName"] = team.Name
page.Props["AppName"] = app.Name
page.Props["ResponseType"] = responseType
page.Props["ClientId"] = clientId
page.Props["RedirectUri"] = redirect
page.Props["Scope"] = scope
page.Props["State"] = state
page.Render(c, w)
}
func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) {
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
c.Err = model.NewAppError("getAccessToken", "The system admin has turned off OAuth service providing.", "")
c.Err.StatusCode = http.StatusNotImplemented
return
}
c.LogAudit("attempt")
r.ParseForm()
grantType := r.FormValue("grant_type")
if grantType != model.ACCESS_TOKEN_GRANT_TYPE {
c.Err = model.NewAppError("getAccessToken", "invalid_request: Bad grant_type", "")
return
}
clientId := r.FormValue("client_id")
if len(clientId) != 26 {
c.Err = model.NewAppError("getAccessToken", "invalid_request: Bad client_id", "")
return
}
secret := r.FormValue("client_secret")
if len(secret) == 0 {
c.Err = model.NewAppError("getAccessToken", "invalid_request: Missing client_secret", "")
return
}
code := r.FormValue("code")
if len(code) == 0 {
c.Err = model.NewAppError("getAccessToken", "invalid_request: Missing code", "")
return
}
redirectUri := r.FormValue("redirect_uri")
achan := api.Srv.Store.OAuth().GetApp(clientId)
tchan := api.Srv.Store.OAuth().GetAccessDataByAuthCode(code)
authData := api.GetAuthData(code)
if authData == nil {
c.LogAudit("fail - invalid auth code")
c.Err = model.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "")
return
}
uchan := api.Srv.Store.User().Get(authData.UserId)
if authData.IsExpired() {
c.LogAudit("fail - auth code expired")
c.Err = model.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "")
return
}
if authData.RedirectUri != redirectUri {
c.LogAudit("fail - redirect uri provided did not match previous redirect uri")
c.Err = model.NewAppError("getAccessToken", "invalid_request: Supplied redirect_uri does not match authorization code redirect_uri", "")
return
}
if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) {
c.LogAudit("fail - auth code is invalid")
c.Err = model.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "")
return
}
var app *model.OAuthApp
if result := <-achan; result.Err != nil {
c.Err = model.NewAppError("getAccessToken", "invalid_client: Invalid client credentials", "")
return
} else {
app = result.Data.(*model.OAuthApp)
}
if !model.ComparePassword(app.ClientSecret, secret) {
c.LogAudit("fail - invalid client credentials")
c.Err = model.NewAppError("getAccessToken", "invalid_client: Invalid client credentials", "")
return
}
callback := redirectUri
if len(callback) == 0 {
callback = app.CallbackUrls[0]
}
if result := <-tchan; result.Err != nil {
c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while accessing database", "")
return
} else if result.Data != nil {
c.LogAudit("fail - auth code has been used previously")
accessData := result.Data.(*model.AccessData)
// Revoke access token, related auth code, and session from DB as well as from cache
if err := api.RevokeAccessToken(accessData.Token); err != nil {
l4g.Error("Encountered an error revoking an access token, err=" + err.Message)
}
c.Err = model.NewAppError("getAccessToken", "invalid_grant: Authorization code already exchanged for an access token", "")
return
}
var user *model.User
if result := <-uchan; result.Err != nil {
c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while pulling user from database", "")
return
} else {
user = result.Data.(*model.User)
}
session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true}
if result := <-api.Srv.Store.Session().Save(session); result.Err != nil {
c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while saving session to database", "")
return
} else {
session = result.Data.(*model.Session)
api.AddSessionToCache(session)
}
accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback}
if result := <-api.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
l4g.Error(result.Err)
c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while saving access token to database", "")
return
}
accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: model.SESSION_TIME_OAUTH_IN_SECS}
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Pragma", "no-cache")
c.LogAuditWithUserId(user.Id, "success")
w.Write([]byte(accessRsp.ToJson()))
}

View File

@@ -6,8 +6,11 @@ package web
import (
"github.com/mattermost/platform/api"
"github.com/mattermost/platform/model"
"github.com/mattermost/platform/store"
"github.com/mattermost/platform/utils"
"net/http"
"net/url"
"strings"
"testing"
"time"
)
@@ -23,7 +26,7 @@ func Setup() {
api.InitApi()
InitWeb()
URL = "http://localhost:" + utils.Cfg.ServiceSettings.Port
ApiClient = model.NewClient(URL + "/api/v1")
ApiClient = model.NewClient(URL)
}
}
@@ -48,6 +51,135 @@ func TestStatic(t *testing.T) {
}
}
func TestGetAccessToken(t *testing.T) {
Setup()
team := model.Team{DisplayName: "Name", Name: "z-z-" + model.NewId() + "a", Email: "test@nowhere.com", Type: model.TEAM_OPEN}
rteam, _ := ApiClient.CreateTeam(&team)
user := model.User{TeamId: rteam.Data.(*model.Team).Id, Email: strings.ToLower(model.NewId()) + "corey@test.com", Password: "pwd"}
ruser := ApiClient.Must(ApiClient.CreateUser(&user, "")).Data.(*model.User)
store.Must(api.Srv.Store.User().VerifyEmail(ruser.Id))
app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{app.CallbackUrls[0]}}
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - oauth providing turned off")
}
} else {
ApiClient.Must(ApiClient.LoginById(ruser.Id, "pwd"))
app = ApiClient.Must(ApiClient.RegisterApp(app)).Data.(*model.OAuthApp)
redirect := ApiClient.Must(ApiClient.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", "123")).Data.(map[string]string)["redirect"]
rurl, _ := url.Parse(redirect)
ApiClient.Logout()
data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{app.Id}, "client_secret": []string{app.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{app.CallbackUrls[0]}}
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - bad grant type")
}
data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
data.Set("client_id", "")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - missing client id")
}
data.Set("client_id", "junk")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - bad client id")
}
data.Set("client_id", app.Id)
data.Set("client_secret", "")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - missing client secret")
}
data.Set("client_secret", "junk")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - bad client secret")
}
data.Set("client_secret", app.ClientSecret)
data.Set("code", "")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - missing code")
}
data.Set("code", "junk")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - bad code")
}
data.Set("code", rurl.Query().Get("code"))
data.Set("redirect_uri", "junk")
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - non-matching redirect uri")
}
// reset data for successful request
data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
data.Set("client_id", app.Id)
data.Set("client_secret", app.ClientSecret)
data.Set("code", rurl.Query().Get("code"))
data.Set("redirect_uri", app.CallbackUrls[0])
token := ""
if result, err := ApiClient.GetAccessToken(data); err != nil {
t.Fatal(err)
} else {
rsp := result.Data.(*model.AccessResponse)
if len(rsp.AccessToken) == 0 {
t.Fatal("access token not returned")
} else {
token = rsp.AccessToken
}
if rsp.TokenType != model.ACCESS_TOKEN_TYPE {
t.Fatal("access token type incorrect")
}
}
if result, err := ApiClient.DoApiGet("/users/profiles?access_token="+token, "", ""); err != nil {
t.Fatal(err)
} else {
userMap := model.UserMapFromJson(result.Body)
if len(userMap) == 0 {
t.Fatal("user map empty - did not get results correctly")
}
}
if _, err := ApiClient.DoApiGet("/users/profiles", "", ""); err == nil {
t.Fatal("should have failed - no access token provided")
}
if _, err := ApiClient.DoApiGet("/users/profiles?access_token=junk", "", ""); err == nil {
t.Fatal("should have failed - bad access token provided")
}
ApiClient.SetOAuthToken(token)
if result, err := ApiClient.DoApiGet("/users/profiles", "", ""); err != nil {
t.Fatal(err)
} else {
userMap := model.UserMapFromJson(result.Body)
if len(userMap) == 0 {
t.Fatal("user map empty - did not get results correctly")
}
}
if _, err := ApiClient.GetAccessToken(data); err == nil {
t.Fatal("should have failed - tried to reuse auth code")
}
ApiClient.ClearOAuthToken()
}
}
func TestZZWebTearDown(t *testing.T) {
// *IMPORTANT*
// This should be the last function in any test file