Merge pull request #237 from mattermost/mm-1570

fixes mm-1570 adds support for using mattermost as an OAuth client
Этот коммит содержится в:
Corey Hulen
2015-07-23 09:21:14 -08:00
родитель dc79707787 d42d0e3467
Коммит a9d48ff994
21 изменённых файлов: 792 добавлений и 134 удалений

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

@@ -20,6 +20,7 @@ import (
_ "image/gif" _ "image/gif"
_ "image/jpeg" _ "image/jpeg"
"image/png" "image/png"
"io"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
@@ -80,36 +81,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
hash := r.URL.Query().Get("h") hash := r.URL.Query().Get("h")
shouldVerifyHash := true if IsVerifyHashRequired(user, team, hash) {
if team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0 && len(hash) == 0 {
domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(team.AllowedDomains, "@", " ", -1), ",", " ", -1))))
matched := false
for _, d := range domains {
if strings.HasSuffix(user.Email, "@"+d) {
matched = true
break
}
}
if matched {
shouldVerifyHash = false
} else {
c.Err = model.NewAppError("createUser", "The signup link does not appear to be valid", "allowed domains failed")
return
}
}
if team.Type == model.TEAM_OPEN {
shouldVerifyHash = false
}
if len(hash) > 0 {
shouldVerifyHash = true
}
if shouldVerifyHash {
data := r.URL.Query().Get("d") data := r.URL.Query().Get("d")
props := model.MapFromJson(strings.NewReader(data)) props := model.MapFromJson(strings.NewReader(data))
@@ -133,6 +105,10 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
user.EmailVerified = true user.EmailVerified = true
} }
if len(user.AuthData) > 0 && len(user.AuthService) > 0 {
user.EmailVerified = true
}
ruser := CreateUser(c, team, user) ruser := CreateUser(c, team, user)
if c.Err != nil { if c.Err != nil {
return return
@@ -142,6 +118,38 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
} }
func IsVerifyHashRequired(user *model.User, team *model.Team, hash string) bool {
shouldVerifyHash := true
if team.Type == model.TEAM_INVITE && len(team.AllowedDomains) > 0 && len(hash) == 0 && user != nil {
domains := strings.Fields(strings.TrimSpace(strings.ToLower(strings.Replace(strings.Replace(team.AllowedDomains, "@", " ", -1), ",", " ", -1))))
matched := false
for _, d := range domains {
if strings.HasSuffix(user.Email, "@"+d) {
matched = true
break
}
}
if matched {
shouldVerifyHash = false
} else {
return true
}
}
if team.Type == model.TEAM_OPEN {
shouldVerifyHash = false
}
if len(hash) > 0 {
shouldVerifyHash = true
}
return shouldVerifyHash
}
func CreateValet(c *Context, team *model.Team) *model.User { func CreateValet(c *Context, team *model.Team) *model.User {
valet := &model.User{} valet := &model.User{}
valet.TeamId = team.Id valet.TeamId = team.Id
@@ -233,80 +241,77 @@ func FireAndForgetVerifyEmail(userId, name, email, teamDisplayName, teamURL stri
}() }()
} }
func login(c *Context, w http.ResponseWriter, r *http.Request) { func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, deviceId string) *model.User {
props := model.MapFromJson(r.Body) if result := <-Srv.Store.User().Get(userId); result.Err != nil {
c.Err = result.Err
extraInfo := "" return nil
var result store.StoreResult } else {
user := result.Data.(*model.User)
if len(props["id"]) != 0 { if checkUserPassword(c, user, password) {
extraInfo = props["id"] Login(c, w, r, user, deviceId)
if result = <-Srv.Store.User().Get(props["id"]); result.Err != nil { return user
c.Err = result.Err
return
} }
} }
return nil
}
func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, deviceId string) *model.User {
var team *model.Team var team *model.Team
if result.Data == nil && len(props["email"]) != 0 && len(props["name"]) != 0 {
extraInfo = props["email"] + " in " + props["name"]
if nr := <-Srv.Store.Team().GetByName(props["name"]); nr.Err != nil { if result := <-Srv.Store.Team().GetByName(name); result.Err != nil {
c.Err = nr.Err c.Err = result.Err
return return nil
} else { } else {
team = nr.Data.(*model.Team) team = result.Data.(*model.Team)
}
if result = <-Srv.Store.User().GetByEmail(team.Id, props["email"]); result.Err != nil { if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil {
c.Err = result.Err c.Err = result.Err
return return nil
} } else {
user := result.Data.(*model.User)
if checkUserPassword(c, user, password) {
Login(c, w, r, user, deviceId)
return user
} }
} }
if result.Data == nil { return nil
c.Err = model.NewAppError("login", "Login failed because we couldn't find a valid account", extraInfo) }
c.Err.StatusCode = http.StatusBadRequest
return func checkUserPassword(c *Context, user *model.User, password string) bool {
} if !model.ComparePassword(user.Password, password) {
c.LogAuditWithUserId(user.Id, "fail")
user := result.Data.(*model.User) c.Err = model.NewAppError("checkUserPassword", "Login failed because of invalid password", "user_id="+user.Id)
c.Err.StatusCode = http.StatusForbidden
if team == nil { return false
if tResult := <-Srv.Store.Team().Get(user.TeamId); tResult.Err != nil {
c.Err = tResult.Err
return
} else {
team = tResult.Data.(*model.Team)
}
} }
return true
}
// User MUST be validated before calling Login
func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, deviceId string) {
c.LogAuditWithUserId(user.Id, "attempt") c.LogAuditWithUserId(user.Id, "attempt")
if !model.ComparePassword(user.Password, props["password"]) {
c.LogAuditWithUserId(user.Id, "fail")
c.Err = model.NewAppError("login", "Login failed because of invalid password", extraInfo)
c.Err.StatusCode = http.StatusForbidden
return
}
if !user.EmailVerified && !utils.Cfg.EmailSettings.ByPassEmail { if !user.EmailVerified && !utils.Cfg.EmailSettings.ByPassEmail {
c.Err = model.NewAppError("login", "Login failed because email address has not been verified", extraInfo) c.Err = model.NewAppError("Login", "Login failed because email address has not been verified", "user_id="+user.Id)
c.Err.StatusCode = http.StatusForbidden c.Err.StatusCode = http.StatusForbidden
return return
} }
if user.DeleteAt > 0 { if user.DeleteAt > 0 {
c.Err = model.NewAppError("login", "Login failed because your account has been set to inactive. Please contact an administrator.", extraInfo) c.Err = model.NewAppError("Login", "Login failed because your account has been set to inactive. Please contact an administrator.", "user_id="+user.Id)
c.Err.StatusCode = http.StatusForbidden c.Err.StatusCode = http.StatusForbidden
return return
} }
session := &model.Session{UserId: user.Id, TeamId: team.Id, Roles: user.Roles, DeviceId: props["device_id"]} session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, DeviceId: deviceId}
maxAge := model.SESSION_TIME_WEB_IN_SECS maxAge := model.SESSION_TIME_WEB_IN_SECS
if len(props["device_id"]) > 0 { if len(deviceId) > 0 {
session.SetExpireInDays(model.SESSION_TIME_MOBILE_IN_DAYS) session.SetExpireInDays(model.SESSION_TIME_MOBILE_IN_DAYS)
maxAge = model.SESSION_TIME_MOBILE_IN_SECS maxAge = model.SESSION_TIME_MOBILE_IN_SECS
} else { } else {
@@ -357,12 +362,41 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) {
} }
http.SetCookie(w, sessionCookie) http.SetCookie(w, sessionCookie)
user.Sanitize(map[string]bool{})
c.Session = *session c.Session = *session
c.LogAuditWithUserId(user.Id, "success") c.LogAuditWithUserId(user.Id, "success")
}
w.Write([]byte(result.Data.(*model.User).ToJson())) func login(c *Context, w http.ResponseWriter, r *http.Request) {
props := model.MapFromJson(r.Body)
if len(props["password"]) == 0 {
c.Err = model.NewAppError("login", "Password field must not be blank", "")
c.Err.StatusCode = http.StatusForbidden
return
}
var user *model.User
if len(props["id"]) != 0 {
user = LoginById(c, w, r, props["id"], props["password"], props["device_id"])
} else if len(props["email"]) != 0 && len(props["name"]) != 0 {
user = LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"])
} else {
c.Err = model.NewAppError("login", "Either user id or team name and user email must be provided", "")
c.Err.StatusCode = http.StatusForbidden
return
}
if c.Err != nil {
return
}
if user != nil {
user.Sanitize(map[string]bool{})
} else {
user = &model.User{}
}
w.Write([]byte(user.ToJson()))
} }
func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) { func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -793,6 +827,13 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) {
tchan := Srv.Store.Team().Get(user.TeamId) tchan := Srv.Store.Team().Get(user.TeamId)
if user.AuthData != "" {
c.LogAudit("failed - tried to update user password who was logged in through oauth")
c.Err = model.NewAppError("updatePassword", "Update password failed because the user is logged in through an OAuth service", "auth_service="+user.AuthService)
c.Err.StatusCode = http.StatusForbidden
return
}
if !model.ComparePassword(user.Password, currentPassword) { if !model.ComparePassword(user.Password, currentPassword) {
c.Err = model.NewAppError("updatePassword", "Update password failed because of invalid password", "") c.Err = model.NewAppError("updatePassword", "Update password failed because of invalid password", "")
c.Err.StatusCode = http.StatusForbidden c.Err.StatusCode = http.StatusForbidden
@@ -1212,3 +1253,72 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) {
return return
} }
} }
func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, teamName, service, redirectUri string) {
if s, ok := utils.Cfg.SSOSettings[service]; !ok || !s.Allow {
c.Err = model.NewAppError("GetAuthorizationCode", "Unsupported OAuth service provider", "service="+service)
c.Err.StatusCode = http.StatusBadRequest
return
}
clientId := utils.Cfg.SSOSettings[service].Id
endpoint := utils.Cfg.SSOSettings[service].AuthEndpoint
state := model.HashPassword(clientId)
authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri+"?team="+teamName) + "&state=" + url.QueryEscape(state)
http.Redirect(w, r, authUrl, http.StatusFound)
}
func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser, *model.AppError) {
if s, ok := utils.Cfg.SSOSettings[service]; !ok || !s.Allow {
return nil, model.NewAppError("AuthorizeOAuthUser", "Unsupported OAuth service provider", "service="+service)
}
if !model.ComparePassword(state, utils.Cfg.SSOSettings[service].Id) {
return nil, model.NewAppError("AuthorizeOAuthUser", "Invalid state", "")
}
p := url.Values{}
p.Set("client_id", utils.Cfg.SSOSettings[service].Id)
p.Set("client_secret", utils.Cfg.SSOSettings[service].Secret)
p.Set("code", code)
p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
p.Set("redirect_uri", redirectUri)
client := &http.Client{}
req, _ := http.NewRequest("POST", utils.Cfg.SSOSettings[service].TokenEndpoint, strings.NewReader(p.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
var ar *model.AccessResponse
if resp, err := client.Do(req); err != nil {
return nil, model.NewAppError("AuthorizeOAuthUser", "Token request failed", err.Error())
} else {
ar = model.AccessResponseFromJson(resp.Body)
}
if ar.TokenType != model.ACCESS_TOKEN_TYPE {
return nil, model.NewAppError("AuthorizeOAuthUser", "Bad token type", "token_type="+ar.TokenType)
}
if len(ar.AccessToken) == 0 {
return nil, model.NewAppError("AuthorizeOAuthUser", "Missing access token", "")
}
p = url.Values{}
p.Set("access_token", ar.AccessToken)
req, _ = http.NewRequest("GET", utils.Cfg.SSOSettings[service].UserApiEndpoint, strings.NewReader(""))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+ar.AccessToken)
if resp, err := client.Do(req); err != nil {
return nil, model.NewAppError("AuthorizeOAuthUser", "Token request to "+service+" failed", err.Error())
} else {
return resp.Body, nil
}
}

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

@@ -23,6 +23,16 @@
"UseLocalStorage": true, "UseLocalStorage": true,
"StorageDirectory": "./data/" "StorageDirectory": "./data/"
}, },
"SSOSettings": {
"gitlab": {
"Allow": false,
"Secret" : "",
"Id": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
},
"SqlSettings": { "SqlSettings": {
"DriverName": "mysql", "DriverName": "mysql",
"DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test", "DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test",

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

@@ -23,6 +23,16 @@
"UseLocalStorage": true, "UseLocalStorage": true,
"StorageDirectory": "/mattermost/data/" "StorageDirectory": "/mattermost/data/"
}, },
"SSOSettings": {
"gitlab": {
"Allow": false,
"Secret" : "",
"Id": "",
"AuthEndpoint": "",
"TokenEndpoint": "",
"UserApiEndpoint": ""
}
},
"SqlSettings": { "SqlSettings": {
"DriverName": "mysql", "DriverName": "mysql",
"DataSource": "mmuser:mostest@tcp(localhost:3306)/mattermost_test", "DataSource": "mmuser:mostest@tcp(localhost:3306)/mattermost_test",

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

@@ -0,0 +1,41 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
package model
import (
"encoding/json"
"io"
)
const (
ACCESS_TOKEN_GRANT_TYPE = "authorization_code"
ACCESS_TOKEN_TYPE = "bearer"
)
type AccessResponse struct {
AccessToken string `json:"access_token"`
TokenType string `json:"token_type"`
ExpiresIn int32 `json:"expires_in"`
RefreshToken string `json:"refresh_token"`
}
func (ar *AccessResponse) ToJson() string {
b, err := json.Marshal(ar)
if err != nil {
return ""
} else {
return string(b)
}
}
func AccessResponseFromJson(data io.Reader) *AccessResponse {
decoder := json.NewDecoder(data)
var ar AccessResponse
err := decoder.Decode(&ar)
if err == nil {
return &ar
} else {
return nil
}
}

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

@@ -8,22 +8,24 @@ import (
"encoding/json" "encoding/json"
"io" "io"
"regexp" "regexp"
"strconv"
"strings" "strings"
) )
const ( const (
ROLE_ADMIN = "admin" ROLE_ADMIN = "admin"
ROLE_SYSTEM_ADMIN = "system_admin" ROLE_SYSTEM_ADMIN = "system_admin"
ROLE_SYSTEM_SUPPORT = "system_support" ROLE_SYSTEM_SUPPORT = "system_support"
USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes
USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute
USER_OFFLINE = "offline" USER_OFFLINE = "offline"
USER_AWAY = "away" USER_AWAY = "away"
USER_ONLINE = "online" USER_ONLINE = "online"
USER_NOTIFY_ALL = "all" USER_NOTIFY_ALL = "all"
USER_NOTIFY_MENTION = "mention" USER_NOTIFY_MENTION = "mention"
USER_NOTIFY_NONE = "none" USER_NOTIFY_NONE = "none"
BOT_USERNAME = "valet" BOT_USERNAME = "valet"
USER_AUTH_SERVICE_GITLAB = "gitlab"
) )
type User struct { type User struct {
@@ -35,6 +37,7 @@ type User struct {
Username string `json:"username"` Username string `json:"username"`
Password string `json:"password"` Password string `json:"password"`
AuthData string `json:"auth_data"` AuthData string `json:"auth_data"`
AuthService string `json:"auth_service"`
Email string `json:"email"` Email string `json:"email"`
EmailVerified bool `json:"email_verified"` EmailVerified bool `json:"email_verified"`
Nickname string `json:"nickname"` Nickname string `json:"nickname"`
@@ -50,6 +53,13 @@ type User struct {
LastPictureUpdate int64 `json:"last_picture_update"` LastPictureUpdate int64 `json:"last_picture_update"`
} }
type GitLabUser struct {
Id int64 `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Name string `json:"name"`
}
// IsValid validates the user and returns an error if it isn't configured // IsValid validates the user and returns an error if it isn't configured
// correctly. // correctly.
func (u *User) IsValid() *AppError { func (u *User) IsValid() *AppError {
@@ -96,6 +106,22 @@ func (u *User) IsValid() *AppError {
return NewAppError("User.IsValid", "Invalid last name", "user_id="+u.Id) return NewAppError("User.IsValid", "Invalid last name", "user_id="+u.Id)
} }
if len(u.Password) > 128 {
return NewAppError("User.IsValid", "Invalid password", "user_id="+u.Id)
}
if len(u.AuthData) > 128 {
return NewAppError("User.IsValid", "Invalid auth data", "user_id="+u.Id)
}
if len(u.AuthData) > 0 && len(u.AuthService) == 0 {
return NewAppError("User.IsValid", "Invalid user, auth data must be set with auth type", "user_id="+u.Id)
}
if len(u.Password) > 0 && len(u.AuthData) > 0 {
return NewAppError("User.IsValid", "Invalid user, password and auth data cannot both be set", "user_id="+u.Id)
}
return nil return nil
} }
@@ -328,3 +354,38 @@ func IsUsernameValid(username string) bool {
return true return true
} }
func UserFromGitLabUser(glu *GitLabUser) *User {
user := &User{}
user.Username = glu.Username
splitName := strings.Split(glu.Name, " ")
if len(splitName) == 2 {
user.FirstName = splitName[0]
user.LastName = splitName[1]
} else if len(splitName) >= 2 {
user.FirstName = splitName[0]
user.LastName = strings.Join(splitName[1:], " ")
} else {
user.FirstName = glu.Name
}
user.Email = glu.Email
user.AuthData = strconv.FormatInt(glu.Id, 10)
user.AuthService = USER_AUTH_SERVICE_GITLAB
return user
}
func GitLabUserFromJson(data io.Reader) *GitLabUser {
decoder := json.NewDecoder(data)
var glu GitLabUser
err := decoder.Decode(&glu)
if err == nil {
return &glu
} else {
return nil
}
}
func (glu *GitLabUser) GetAuthData() string {
return strconv.FormatInt(glu.Id, 10)
}

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

@@ -24,6 +24,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore {
table.ColMap("Username").SetMaxSize(64) table.ColMap("Username").SetMaxSize(64)
table.ColMap("Password").SetMaxSize(128) table.ColMap("Password").SetMaxSize(128)
table.ColMap("AuthData").SetMaxSize(128) table.ColMap("AuthData").SetMaxSize(128)
table.ColMap("AuthService").SetMaxSize(32)
table.ColMap("Email").SetMaxSize(128) table.ColMap("Email").SetMaxSize(128)
table.ColMap("Nickname").SetMaxSize(64) table.ColMap("Nickname").SetMaxSize(64)
table.ColMap("FirstName").SetMaxSize(64) table.ColMap("FirstName").SetMaxSize(64)
@@ -57,6 +58,8 @@ func (us SqlUserStore) UpgradeSchemaIfNeeded() {
panic("Failed to set last name from nickname " + err.Error()) panic("Failed to set last name from nickname " + err.Error())
} }
} }
us.CreateColumnIfNotExists("Users", "AuthService", "AuthData", "varchar(32)", "") // for OAuth Client
} }
//func (ss SqlStore) CreateColumnIfNotExists(tableName string, columnName string, afterName string, colType string, defaultValue string) bool { //func (ss SqlStore) CreateColumnIfNotExists(tableName string, columnName string, afterName string, colType string, defaultValue string) bool {
@@ -369,6 +372,28 @@ func (us SqlUserStore) GetByEmail(teamId string, email string) StoreChannel {
return storeChannel return storeChannel
} }
func (us SqlUserStore) GetByAuth(teamId string, authData string, authService string) StoreChannel {
storeChannel := make(StoreChannel)
go func() {
result := StoreResult{}
user := model.User{}
if err := us.GetReplica().SelectOne(&user, "SELECT * FROM Users WHERE TeamId=? AND AuthData=? AND AuthService=?", teamId, authData, authService); err != nil {
result.Err = model.NewAppError("SqlUserStore.GetByAuth", "We couldn't find the existing account", "teamId="+teamId+", authData="+authData+", authService="+authService+", "+err.Error())
}
result.Data = &user
storeChannel <- result
close(storeChannel)
}()
return storeChannel
}
func (us SqlUserStore) GetByUsername(teamId string, username string) StoreChannel { func (us SqlUserStore) GetByUsername(teamId string, username string) StoreChannel {
storeChannel := make(StoreChannel) storeChannel := make(StoreChannel)

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

@@ -236,6 +236,25 @@ func TestUserStoreGetByEmail(t *testing.T) {
} }
} }
func TestUserStoreGetByAuthData(t *testing.T) {
Setup()
u1 := model.User{}
u1.TeamId = model.NewId()
u1.Email = model.NewId()
u1.AuthData = "123"
u1.AuthService = "service"
Must(store.User().Save(&u1))
if err := (<-store.User().GetByAuth(u1.TeamId, u1.AuthData, u1.AuthService)).Err; err != nil {
t.Fatal(err)
}
if err := (<-store.User().GetByAuth("", "", "")).Err; err == nil {
t.Fatal("Should have failed because of missing auth data")
}
}
func TestUserStoreGetByUsername(t *testing.T) { func TestUserStoreGetByUsername(t *testing.T) {
Setup() Setup()

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

@@ -85,6 +85,7 @@ type UserStore interface {
Get(id string) StoreChannel Get(id string) StoreChannel
GetProfiles(teamId string) StoreChannel GetProfiles(teamId string) StoreChannel
GetByEmail(teamId string, email string) StoreChannel GetByEmail(teamId string, email string) StoreChannel
GetByAuth(teamId string, authData string, authService string) StoreChannel
GetByUsername(teamId string, username string) StoreChannel GetByUsername(teamId string, username string) StoreChannel
VerifyEmail(userId string) StoreChannel VerifyEmail(userId string) StoreChannel
GetEtagForProfiles(teamId string) StoreChannel GetEtagForProfiles(teamId string) StoreChannel

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

@@ -32,6 +32,15 @@ type ServiceSettings struct {
StorageDirectory string StorageDirectory string
} }
type SSOSetting struct {
Allow bool
Secret string
Id string
AuthEndpoint string
TokenEndpoint string
UserApiEndpoint string
}
type SqlSettings struct { type SqlSettings struct {
DriverName string DriverName string
DataSource string DataSource string
@@ -109,6 +118,7 @@ type Config struct {
EmailSettings EmailSettings EmailSettings EmailSettings
PrivacySettings PrivacySettings PrivacySettings PrivacySettings
TeamSettings TeamSettings TeamSettings TeamSettings
SSOSettings map[string]SSOSetting
} }
func (o *Config) ToJson() string { func (o *Config) ToJson() string {
@@ -243,3 +253,14 @@ func IsS3Configured() bool {
return true return true
} }
func GetAllowedAuthServices() []string {
authServices := []string{}
for name, service := range Cfg.SSOSettings {
if service.Allow {
authServices = append(authServices, name)
}
}
return authServices
}

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

@@ -90,6 +90,17 @@ module.exports = React.createClass({
focusEmail = true; focusEmail = true;
} }
var auth_services = JSON.parse(this.props.authServices);
var login_message;
if (auth_services.indexOf("gitlab") >= 0) {
login_message = (
<div className="form-group form-group--small">
<span><a href={"/"+teamName+"/login/gitlab"}>{"Log in with GitLab"}</a></span>
</div>
);
}
return ( return (
<div className="signup-team__container"> <div className="signup-team__container">
<div> <div>
@@ -112,6 +123,7 @@ module.exports = React.createClass({
<div className="form-group"> <div className="form-group">
<button type="submit" className="btn btn-primary">Sign in</button> <button type="submit" className="btn btn-primary">Sign in</button>
</div> </div>
{ login_message }
<div className="form-group form-group--small"> <div className="form-group form-group--small">
<span><a href="/find_team">{"Find other " + strings.TeamPlural}</a></span> <span><a href="/find_team">{"Find other " + strings.TeamPlural}</a></span>
</div> </div>

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

@@ -20,7 +20,7 @@ module.exports = React.createClass({
<hr /> <hr />
{ server_error } { server_error }
{ client_error } { client_error }
<a className="btn btn-sm btn-primary" onClick={this.props.submit}>Submit</a> { this.props.submit ? <a className="btn btn-sm btn-primary" onClick={this.props.submit}>Submit</a> : "" }
<a className="btn btn-sm theme" href="#" onClick={this.props.updateSection}>Cancel</a> <a className="btn btn-sm theme" href="#" onClick={this.props.updateSection}>Cancel</a>
</li> </li>
</ul> </ul>

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

@@ -46,7 +46,7 @@ module.exports = React.createClass({
function(data) { function(data) {
client.track('signup', 'signup_user_02_complete'); client.track('signup', 'signup_user_02_complete');
client.loginByEmail(this.props.domain, this.state.user.email, this.state.user.password, client.loginByEmail(this.props.teamName, this.state.user.email, this.state.user.password,
function(data) { function(data) {
UserStore.setLastEmail(this.state.user.email); UserStore.setLastEmail(this.state.user.email);
UserStore.setCurrentUser(data); UserStore.setCurrentUser(data);
@@ -58,7 +58,7 @@ module.exports = React.createClass({
}.bind(this), }.bind(this),
function(err) { function(err) {
if (err.message == "Login failed because email address has not been verified") { if (err.message == "Login failed because email address has not been verified") {
window.location.href = "/verify_email?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.domain); window.location.href = "/verify_email?email="+ encodeURIComponent(this.state.user.email) + "&domain=" + encodeURIComponent(this.props.teamName);
} else { } else {
this.state.server_error = err.message; this.state.server_error = err.message;
this.setState(this.state); this.setState(this.state);
@@ -79,7 +79,7 @@ module.exports = React.createClass({
props = {}; props = {};
props.wizard = "welcome"; props.wizard = "welcome";
props.user = {}; props.user = {};
props.user.team_id = this.props.team_id; props.user.team_id = this.props.teamId;
props.user.email = this.props.email; props.user.email = this.props.email;
props.hash = this.props.hash; props.hash = this.props.hash;
props.data = this.props.data; props.data = this.props.data;
@@ -103,7 +103,7 @@ module.exports = React.createClass({
var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is { this.state.user.email }. </span> var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is { this.state.user.email }. </span>
var email = var email = (
<div className={ this.state.original_email == "" ? "" : "hidden"} > <div className={ this.state.original_email == "" ? "" : "hidden"} >
<label className="control-label">Email</label> <label className="control-label">Email</label>
<div className={ email_error ? "form-group has-error" : "form-group" }> <div className={ email_error ? "form-group has-error" : "form-group" }>
@@ -111,12 +111,25 @@ module.exports = React.createClass({
{ email_error } { email_error }
</div> </div>
</div> </div>
);
var auth_services = JSON.parse(this.props.authServices);
var signup_message;
if (auth_services.indexOf("gitlab") >= 0) {
signup_message = <p>{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team} <a href={"/"+this.props.teamName+"/signup/gitlab"+window.location.search}>{"or sign up with GitLab."}</a></p>;
} else {
signup_message = <p>{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team + "."}</p>;
}
return ( return (
<div> <div>
<img className="signup-team-logo" src="/static/images/logo.png" /> <img className="signup-team-logo" src="/static/images/logo.png" />
<h4>Welcome to { config.SiteName }</h4> <h4>Welcome to { config.SiteName }</h4>
<p>{"Choose your username and password for the " + this.props.team_name + " " + strings.Team +"."}</p> <div className="form-group form-group--small">
<span></span>
</div>
{ signup_message }
<p>Your username can be made of lowercase letters and numbers.</p> <p>Your username can be made of lowercase letters and numbers.</p>
<label className="control-label">Username</label> <label className="control-label">Username</label>
<div className={ name_error ? "form-group has-error" : "form-group" }> <div className={ name_error ? "form-group has-error" : "form-group" }>

84
web/react/components/signup_user_oauth.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,84 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var utils = require('../utils/utils.jsx');
var client = require('../utils/client.jsx');
var UserStore = require('../stores/user_store.jsx');
var BrowserStore = require('../stores/browser_store.jsx');
module.exports = React.createClass({
handleSubmit: function(e) {
e.preventDefault();
if (!this.state.user.username) {
this.setState({name_error: "This field is required", email_error: "", password_error: "", server_error: ""});
return;
}
var username_error = utils.isValidUsername(this.state.user.username);
if (username_error === "Cannot use a reserved word as a username.") {
this.setState({name_error: "This username is reserved, please choose a new one.", email_error: "", password_error: "", server_error: ""});
return;
} else if (username_error) {
this.setState({name_error: "Username must begin with a letter, and contain between 3 to 15 lowercase characters made up of numbers, letters, and the symbols '.', '-' and '_'.", email_error: "", password_error: "", server_error: ""});
return;
}
this.setState({name_error: "", server_error: ""});
this.state.user.allow_marketing = this.refs.email_service.getDOMNode().checked;
var user = this.state.user;
client.createUser(user, "", "",
function(data) {
client.track('signup', 'signup_user_oauth_02');
window.location.href = '/' + this.props.teamName + '/login/'+user.auth_service;
}.bind(this),
function(err) {
this.state.server_error = err.message;
this.setState(this.state);
}.bind(this)
);
},
handleChange: function() {
var user = this.state.user;
user.username = this.refs.name.getDOMNode().value;
this.setState({ user: user });
},
getInitialState: function() {
var user = JSON.parse(this.props.user);
return { user: user };
},
render: function() {
client.track('signup', 'signup_user_oauth_01');
var name_error = this.state.name_error ? <label className='control-label'>{ this.state.name_error }</label> : null;
var server_error = this.state.server_error ? <div className={ "form-group has-error" }><label className='control-label'>{ this.state.server_error }</label></div> : null;
var yourEmailIs = this.state.user.email == "" ? "" : <span>Your email address is <b>{ this.state.user.email }.</b></span>;
return (
<div>
<img className="signup-team-logo" src="/static/images/logo.png" />
<h4>Welcome to { config.SiteName }</h4>
<p>{"To continue signing up with " + this.state.user.auth_service + ", please register a username."}</p>
<p>Your username can be made of lowercase letters and numbers.</p>
<label className="control-label">Username</label>
<div className={ name_error ? "form-group has-error" : "form-group" }>
<input type="text" ref="name" className="form-control" placeholder="" maxLength="128" value={this.state.user.username} onChange={this.handleChange} />
{ name_error }
</div>
<p>{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others."}</p>
<p>{ yourEmailIs } Youll use this address to sign in to {config.SiteName}.</p>
<div className="checkbox"><label><input type="checkbox" ref="email_service" /> It's ok to send me occassional email with updates about the {config.SiteName} service. </label></div>
<p><button onClick={this.handleSubmit} className="btn-primary btn">Create Account</button></p>
{ server_error }
<p>By proceeding to create your account and use { config.SiteName }, you agree to our <a href={ config.TermsLink }>Terms of Service</a> and <a href={ config.PrivacyLink }>Privacy Policy</a>. If you do not agree, you cannot use {config.SiteName}.</p>
</div>
);
}
});

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

@@ -449,7 +449,7 @@ var SecurityTab = React.createClass({
submitPassword: function(e) { submitPassword: function(e) {
e.preventDefault(); e.preventDefault();
var user = UserStore.getCurrentUser(); var user = this.props.user;
var currentPassword = this.state.current_password; var currentPassword = this.state.current_password;
var newPassword = this.state.new_password; var newPassword = this.state.new_password;
var confirmPassword = this.state.confirm_password; var confirmPassword = this.state.confirm_password;
@@ -513,53 +513,69 @@ var SecurityTab = React.createClass({
var self = this; var self = this;
if (this.props.activeSection === 'password') { if (this.props.activeSection === 'password') {
var inputs = []; var inputs = [];
var submit = null;
inputs.push( if (this.props.user.auth_service === "") {
<div className="form-group"> inputs.push(
<label className="col-sm-5 control-label">Current Password</label> <div className="form-group">
<div className="col-sm-7"> <label className="col-sm-5 control-label">Current Password</label>
<input className="form-control" type="password" onChange={this.updateCurrentPassword} value={this.state.current_password}/> <div className="col-sm-7">
<input className="form-control" type="password" onChange={this.updateCurrentPassword} value={this.state.current_password}/>
</div>
</div> </div>
</div> );
); inputs.push(
inputs.push( <div className="form-group">
<div className="form-group"> <label className="col-sm-5 control-label">New Password</label>
<label className="col-sm-5 control-label">New Password</label> <div className="col-sm-7">
<div className="col-sm-7"> <input className="form-control" type="password" onChange={this.updateNewPassword} value={this.state.new_password}/>
<input className="form-control" type="password" onChange={this.updateNewPassword} value={this.state.new_password}/> </div>
</div> </div>
</div> );
); inputs.push(
inputs.push( <div className="form-group">
<div className="form-group"> <label className="col-sm-5 control-label">Retype New Password</label>
<label className="col-sm-5 control-label">Retype New Password</label> <div className="col-sm-7">
<div className="col-sm-7"> <input className="form-control" type="password" onChange={this.updateConfirmPassword} value={this.state.confirm_password}/>
<input className="form-control" type="password" onChange={this.updateConfirmPassword} value={this.state.confirm_password}/> </div>
</div> </div>
</div> );
);
submit = this.submitPassword;
} else {
inputs.push(
<div className="form-group">
<label className="col-sm-12">Log in occurs through GitLab. Please see your GitLab account settings page to update your password.</label>
</div>
);
}
passwordSection = ( passwordSection = (
<SettingItemMax <SettingItemMax
title="Password" title="Password"
inputs={inputs} inputs={inputs}
submit={this.submitPassword} submit={submit}
server_error={server_error} server_error={server_error}
client_error={password_error} client_error={password_error}
updateSection={function(e){self.props.updateSection("");e.preventDefault();}} updateSection={function(e){self.props.updateSection("");e.preventDefault();}}
/> />
); );
} else { } else {
var d = new Date(this.props.user.last_password_update); var describe;
var hour = d.getHours() % 12 ? String(d.getHours() % 12) : "12"; if (this.props.user.auth_service === "") {
var min = d.getMinutes() < 10 ? "0" + d.getMinutes() : String(d.getMinutes()); var d = new Date(this.props.user.last_password_update);
var timeOfDay = d.getHours() >= 12 ? " pm" : " am"; var hour = d.getHours() % 12 ? String(d.getHours() % 12) : "12";
var dateStr = "Last updated " + Constants.MONTHS[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear() + " at " + hour + ":" + min + timeOfDay; var min = d.getMinutes() < 10 ? "0" + d.getMinutes() : String(d.getMinutes());
var timeOfDay = d.getHours() >= 12 ? " pm" : " am";
describe = "Last updated " + Constants.MONTHS[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear() + " at " + hour + ":" + min + timeOfDay;
} else {
describe = "Log in done through GitLab"
}
passwordSection = ( passwordSection = (
<SettingItemMin <SettingItemMin
title="Password" title="Password"
describe={dateStr} describe={describe}
updateSection={function(){self.props.updateSection("password");}} updateSection={function(){self.props.updateSection("password");}}
/> />
); );

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

@@ -3,9 +3,9 @@
var Login = require('../components/login.jsx'); var Login = require('../components/login.jsx');
global.window.setup_login_page = function(teamDisplayName, teamName) { global.window.setup_login_page = function(team_display_name, team_name, auth_services) {
React.render( React.render(
<Login teamDisplayName={teamDisplayName} teamName={teamName}/>, <Login teamDisplayName={team_display_name} teamName={team_name} authServices={auth_services} />,
document.getElementById('login') document.getElementById('login')
); );
}; };

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

@@ -3,9 +3,9 @@
var SignupUserComplete =require('../components/signup_user_complete.jsx'); var SignupUserComplete =require('../components/signup_user_complete.jsx');
global.window.setup_signup_user_complete_page = function(email, domain, name, id, data, hash) { global.window.setup_signup_user_complete_page = function(email, name, ui_name, id, data, hash, auth_services) {
React.render( React.render(
<SignupUserComplete team_id={id} domain={domain} team_name={name} email={email} hash={hash} data={data} />, <SignupUserComplete teamId={id} teamName={name} teamDisplayName={ui_name} email={email} hash={hash} data={data} authServices={auth_services} />,
document.getElementById('signup-user-complete') document.getElementById('signup-user-complete')
); );
}; };

11
web/react/pages/signup_user_oauth.jsx Обычный файл
Просмотреть файл

@@ -0,0 +1,11 @@
// Copyright (c) 2015 Spinpunch, Inc. All Rights Reserved.
// See License.txt for license information.
var SignupUserOAuth = require('../components/signup_user_oauth.jsx');
global.window.setup_signup_user_oauth_page = function(user, team_name, team_display_name) {
React.render(
<SignupUserOAuth user={user} teamName={team_name} teamDisplayName={team_display_name} />,
document.getElementById('signup-user-complete')
);
};

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

@@ -20,7 +20,7 @@
</div> </div>
</div> </div>
<script> <script>
window.setup_login_page({{.Props.TeamDisplayName}}, {{.Props.TeamName}}); window.setup_login_page('{{.Props.TeamDisplayName}}', '{{.Props.TeamName}}', '{{.Props.AuthServices}}');
</script> </script>
</body> </body>
</html> </html>

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

@@ -19,7 +19,7 @@
</div> </div>
</div> </div>
<script> <script>
window.setup_signup_user_complete_page('{{.Props.Email}}', '{{.Props.TeamName}}', '{{.Props.TeamDisplayName}}', '{{.Props.TeamId}}', '{{.Props.Data}}', '{{.Props.Hash}}'); window.setup_signup_user_complete_page('{{.Props.Email}}', '{{.Props.TeamName}}', '{{.Props.TeamDisplayName}}', '{{.Props.TeamId}}', '{{.Props.Data}}', '{{.Props.Hash}}', '{{.Props.AuthServices}}');
</script> </script>
</body> </body>
</html> </html>

26
web/templates/signup_user_oauth.html Обычный файл
Просмотреть файл

@@ -0,0 +1,26 @@
{{define "signup_user_oauth"}}
<!DOCTYPE html>
<html>
{{template "head" . }}
<body class="white">
<div class="container-fluid">
<div class="inner__wrap">
<div class="row content">
<div class="col-sm-12">
<div class="signup-team__container">
<div id="signup-user-complete"></div>
</div>
</div>
<div class="footer-push"></div>
</div>
<div class="row footer">
{{template "footer" . }}
</div>
</div>
</div>
<script>
window.setup_signup_user_oauth_page('{{.Props.User}}', '{{.Props.TeamName}}', '{{.Props.TeamDisplayName}}');
</script>
</body>
</html>
{{end}}

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

@@ -52,6 +52,11 @@ func InitWeb() {
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET")
// Bug in gorilla.mux pervents us from using regex here.
mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET")
mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(loginCompleteOAuth)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET")
mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET")
// Bug in gorilla.mux pervents us from using regex here. // Bug in gorilla.mux pervents us from using regex here.
@@ -61,6 +66,11 @@ func InitWeb() {
mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET") mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET")
mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET")
mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET")
// Bug in gorilla.mux pervents us from using regex here.
mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET")
mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(signupCompleteOAuth)).Methods("GET")
mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET")
mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET")
mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET")
@@ -178,6 +188,7 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) {
page := NewHtmlTemplatePage("login", "Login") page := NewHtmlTemplatePage("login", "Login")
page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamDisplayName"] = team.DisplayName
page.Props["TeamName"] = teamName page.Props["TeamName"] = teamName
page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w) page.Render(c, w)
} }
@@ -264,6 +275,7 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request)
page.Props["TeamId"] = props["id"] page.Props["TeamId"] = props["id"]
page.Props["Data"] = data page.Props["Data"] = data
page.Props["Hash"] = hash page.Props["Hash"] = hash
page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices())
page.Render(c, w) page.Render(c, w)
} }
@@ -439,3 +451,189 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) {
page.Props["IsReset"] = strconv.FormatBool(isResetLink) page.Props["IsReset"] = strconv.FormatBool(isResetLink)
page.Render(c, w) page.Render(c, w)
} }
func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
teamName := params["team"]
if len(teamName) == 0 {
c.Err = model.NewAppError("signupWithOAuth", "Invalid team name", "team_name="+teamName)
c.Err.StatusCode = http.StatusBadRequest
return
}
hash := r.URL.Query().Get("h")
var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
if api.IsVerifyHashRequired(nil, team, hash) {
data := r.URL.Query().Get("d")
props := model.MapFromJson(strings.NewReader(data))
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) {
c.Err = model.NewAppError("signupWithOAuth", "The signup link does not appear to be valid", "")
return
}
t, err := strconv.ParseInt(props["time"], 10, 64)
if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
c.Err = model.NewAppError("signupWithOAuth", "The signup link has expired", "")
return
}
if team.Id != props["id"] {
c.Err = model.NewAppError("signupWithOAuth", "Invalid team name", data)
return
}
}
redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete"
api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri)
}
func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
teamName := r.FormValue("team")
uri := c.GetSiteURL() + "/signup/" + service + "/complete?team=" + teamName
if len(teamName) == 0 {
c.Err = model.NewAppError("signupCompleteOAuth", "Invalid team name", "team_name="+teamName)
c.Err.StatusCode = http.StatusBadRequest
return
}
// Make sure team exists
var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
if body, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil {
c.Err = err
return
} else {
var user *model.User
if service == model.USER_AUTH_SERVICE_GITLAB {
glu := model.GitLabUserFromJson(body)
user = model.UserFromGitLabUser(glu)
}
if user == nil {
c.Err = model.NewAppError("signupCompleteOAuth", "Could not create user out of "+service+" user object", "")
return
}
if result := <-api.Srv.Store.User().GetByAuth(team.Id, user.AuthData, service); result.Err == nil {
c.Err = model.NewAppError("signupCompleteOAuth", "This "+service+" account has already been used to sign up for team "+team.DisplayName, "email="+user.Email)
return
}
if result := <-api.Srv.Store.User().GetByEmail(team.Id, user.Email); result.Err == nil {
c.Err = model.NewAppError("signupCompleteOAuth", "Team "+team.DisplayName+" already has a user with the email address attached to your "+service+" account", "email="+user.Email)
return
}
user.TeamId = team.Id
page := NewHtmlTemplatePage("signup_user_oauth", "Complete User Sign Up")
page.Props["User"] = user.ToJson()
page.Props["TeamName"] = team.Name
page.Props["TeamDisplayName"] = team.DisplayName
page.Render(c, w)
}
}
func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
teamName := params["team"]
if len(teamName) == 0 {
c.Err = model.NewAppError("loginWithOAuth", "Invalid team name", "team_name="+teamName)
c.Err.StatusCode = http.StatusBadRequest
return
}
// Make sure team exists
if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
}
redirectUri := c.GetSiteURL() + "/login/" + service + "/complete"
api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri)
}
func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) {
params := mux.Vars(r)
service := params["service"]
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
teamName := r.FormValue("team")
uri := c.GetSiteURL() + "/login/" + service + "/complete?team=" + teamName
if len(teamName) == 0 {
c.Err = model.NewAppError("loginCompleteOAuth", "Invalid team name", "team_name="+teamName)
c.Err.StatusCode = http.StatusBadRequest
return
}
// Make sure team exists
var team *model.Team
if result := <-api.Srv.Store.Team().GetByName(teamName); result.Err != nil {
c.Err = result.Err
return
} else {
team = result.Data.(*model.Team)
}
if body, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil {
c.Err = err
return
} else {
authData := ""
if service == model.USER_AUTH_SERVICE_GITLAB {
glu := model.GitLabUserFromJson(body)
authData = glu.GetAuthData()
}
if len(authData) == 0 {
c.Err = model.NewAppError("loginCompleteOAuth", "Could not parse auth data out of "+service+" user object", "")
return
}
var user *model.User
if result := <-api.Srv.Store.User().GetByAuth(team.Id, authData, service); result.Err != nil {
c.Err = result.Err
return
} else {
user = result.Data.(*model.User)
api.Login(c, w, r, user, "")
if c.Err != nil {
return
}
root(c, w, r)
}
}
}