From c39e95c7cb1ad6e812aa3ce4000b4dfdf214e77e Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 15 Jul 2015 12:48:50 -0400 Subject: inital implementation of using GitLab OAuth2 provider for signup/login --- api/user.go | 150 ++++++++++++++++++++--------- config/config.json | 6 ++ model/access.go | 41 ++++++++ model/user.go | 81 +++++++++++++--- store/sql_user_store.go | 26 +++++ store/store.go | 1 + utils/config.go | 8 ++ web/react/components/signup_user_oauth.jsx | 84 ++++++++++++++++ web/react/pages/signup_user_oauth.jsx | 11 +++ web/templates/signup_user_oauth.html | 26 +++++ web/web.go | 87 +++++++++++++++++ 11 files changed, 463 insertions(+), 58 deletions(-) create mode 100644 model/access.go create mode 100644 web/react/components/signup_user_oauth.jsx create mode 100644 web/react/pages/signup_user_oauth.jsx create mode 100644 web/templates/signup_user_oauth.html diff --git a/api/user.go b/api/user.go index 7035613ea..4765a5611 100644 --- a/api/user.go +++ b/api/user.go @@ -133,6 +133,10 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { user.EmailVerified = true } + if len(user.AuthData) > 0 && len(user.AuthService) > 0 { + user.EmailVerified = true + } + ruser := CreateUser(c, team, user) if c.Err != nil { return @@ -233,80 +237,71 @@ func FireAndForgetVerifyEmail(userId, name, email, teamDisplayName, teamURL stri }() } -func login(c *Context, w http.ResponseWriter, r *http.Request) { - props := model.MapFromJson(r.Body) - - extraInfo := "" - var result store.StoreResult - - if len(props["id"]) != 0 { - extraInfo = props["id"] - if result = <-Srv.Store.User().Get(props["id"]); result.Err != nil { - c.Err = result.Err - return +func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, deviceId string) { + if result := <-Srv.Store.User().Get(userId); result.Err != nil { + c.Err = result.Err + return + } else { + user := result.Data.(*model.User) + if checkUserPassword(c, user, password) { + Login(c, w, r, user, deviceId) } } +} +func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, deviceId string) { 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 { - c.Err = nr.Err - return - } else { - team = nr.Data.(*model.Team) - if result = <-Srv.Store.User().GetByEmail(team.Id, props["email"]); result.Err != nil { - c.Err = result.Err - return - } - } - } - - if result.Data == nil { - c.Err = model.NewAppError("login", "Login failed because we couldn't find a valid account", extraInfo) - c.Err.StatusCode = http.StatusBadRequest + if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { + c.Err = result.Err return + } else { + team = result.Data.(*model.Team) } - user := result.Data.(*model.User) + if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { + c.Err = result.Err + return + } else { + user := result.Data.(*model.User) - if team == nil { - if tResult := <-Srv.Store.Team().Get(user.TeamId); tResult.Err != nil { - c.Err = tResult.Err - return - } else { - team = tResult.Data.(*model.Team) + if checkUserPassword(c, user, password) { + Login(c, w, r, user, deviceId) } } +} - c.LogAuditWithUserId(user.Id, "attempt") - - if !model.ComparePassword(user.Password, props["password"]) { +func checkUserPassword(c *Context, user *model.User, password string) bool { + if !model.ComparePassword(user.Password, password) { c.LogAuditWithUserId(user.Id, "fail") - c.Err = model.NewAppError("login", "Login failed because of invalid password", extraInfo) + c.Err = model.NewAppError("checkUserPassword", "Login failed because of invalid password", "user_id="+user.Id) c.Err.StatusCode = http.StatusForbidden - return + return false } + 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") 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 return } 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 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 - if len(props["device_id"]) > 0 { + if len(deviceId) > 0 { session.SetExpireInDays(model.SESSION_TIME_MOBILE_IN_DAYS) maxAge = model.SESSION_TIME_MOBILE_IN_SECS } else { @@ -361,8 +356,22 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { c.Session = *session 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["id"]) != 0 { + LoginById(c, w, r, props["id"], props["password"], props["device_id"]) + } else if len(props["email"]) != 0 && len(props["name"]) != 0 { + LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"]) + } + + if c.Err != nil { + return + } + + w.Write([]byte("{}")) } func revokeSession(c *Context, w http.ResponseWriter, r *http.Request) { @@ -1212,3 +1221,52 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) { return } } + +func AuthorizeGitLabUser(code, state, uri string) (*model.GitLabUser, *model.AppError) { + if !model.ComparePassword(state, utils.Cfg.SSOSettings.GitLabId) { + return nil, model.NewAppError("AuthorizeGitLabUser", "Invalid state", "") + } + + p := url.Values{} + p.Set("client_id", utils.Cfg.SSOSettings.GitLabId) + p.Set("client_secret", utils.Cfg.SSOSettings.GitLabSecret) + p.Set("code", code) + p.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE) + p.Set("redirect_uri", uri) + + client := &http.Client{} + req, _ := http.NewRequest("POST", utils.Cfg.SSOSettings.GitLabUrl+"/oauth/token", 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("AuthorizeGitLabUser", "Token request to GitLab failed", err.Error()) + } else { + ar = model.AccessResponseFromJson(resp.Body) + } + + if ar.TokenType != model.ACCESS_TOKEN_TYPE { + return nil, model.NewAppError("AuthorizeGitLabUser", "Bad token type", "token_type="+ar.TokenType) + } + + if len(ar.AccessToken) == 0 { + return nil, model.NewAppError("AuthorizeGitLabUser", "Missing access token", "") + } + + p = url.Values{} + p.Set("access_token", ar.AccessToken) + req, _ = http.NewRequest("GET", utils.Cfg.SSOSettings.GitLabUrl+"/api/v3/user", 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("AuthorizeGitLabUser", "Token request to GitLab failed", err.Error()) + } else { + return model.GitLabUserFromJson(resp.Body), nil + } + +} diff --git a/config/config.json b/config/config.json index 085dd6de6..61183948f 100644 --- a/config/config.json +++ b/config/config.json @@ -23,6 +23,12 @@ "UseLocalStorage": true, "StorageDirectory": "./data/" }, + "SSOSettings": { + "AllowGitLabSSO": true, + "GitLabSecret" : "8526ada64f38a1a67cafe6650d54310f1484f8a5d06ad23abb9f8e4b8af1c429", + "GitLabId": "0af4138195d246d5d4e958a93100379066bb087fa9892cd323b0c97bbd696008", + "GitLabUrl": "http://dockerhost:8080" + }, "SqlSettings": { "DriverName": "mysql", "DataSource": "mmuser:mostest@tcp(dockerhost:3306)/mattermost_test", diff --git a/model/access.go b/model/access.go new file mode 100644 index 000000000..f9e36ce07 --- /dev/null +++ b/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 + } +} diff --git a/model/user.go b/model/user.go index 727165b8c..22158b6ac 100644 --- a/model/user.go +++ b/model/user.go @@ -8,22 +8,24 @@ import ( "encoding/json" "io" "regexp" + "strconv" "strings" ) const ( - ROLE_ADMIN = "admin" - ROLE_SYSTEM_ADMIN = "system_admin" - ROLE_SYSTEM_SUPPORT = "system_support" - USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes - USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute - USER_OFFLINE = "offline" - USER_AWAY = "away" - USER_ONLINE = "online" - USER_NOTIFY_ALL = "all" - USER_NOTIFY_MENTION = "mention" - USER_NOTIFY_NONE = "none" - BOT_USERNAME = "valet" + ROLE_ADMIN = "admin" + ROLE_SYSTEM_ADMIN = "system_admin" + ROLE_SYSTEM_SUPPORT = "system_support" + USER_AWAY_TIMEOUT = 5 * 60 * 1000 // 5 minutes + USER_OFFLINE_TIMEOUT = 1 * 60 * 1000 // 1 minute + USER_OFFLINE = "offline" + USER_AWAY = "away" + USER_ONLINE = "online" + USER_NOTIFY_ALL = "all" + USER_NOTIFY_MENTION = "mention" + USER_NOTIFY_NONE = "none" + BOT_USERNAME = "valet" + USER_AUTH_SERVICE_GITLAB = "gitlab" ) type User struct { @@ -48,6 +50,14 @@ type User struct { NotifyProps StringMap `json:"notify_props"` LastPasswordUpdate int64 `json:"last_password_update"` LastPictureUpdate int64 `json:"last_picture_update"` + AuthService string `json:"auth_service"` +} + +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 @@ -96,6 +106,22 @@ func (u *User) IsValid() *AppError { 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 } @@ -328,3 +354,34 @@ func IsUsernameValid(username string) bool { 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 + } +} diff --git a/store/sql_user_store.go b/store/sql_user_store.go index d8ab4482e..3c25dbb44 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -31,8 +31,10 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { table.ColMap("Roles").SetMaxSize(64) table.ColMap("Props").SetMaxSize(4000) table.ColMap("NotifyProps").SetMaxSize(2000) + table.ColMap("AuthService").SetMaxSize(32) table.SetUniqueTogether("Email", "TeamId") table.SetUniqueTogether("Username", "TeamId") + table.SetUniqueTogether("AuthData", "AuthService", "TeamId") } return us @@ -57,6 +59,8 @@ func (us SqlUserStore) UpgradeSchemaIfNeeded() { panic("Failed to set last name from nickname " + err.Error()) } } + + us.CreateColumnIfNotExists("Users", "AuthService", "LastPictureUpdate", "varchar(32)", "") // for OAuth Client } //func (ss SqlStore) CreateColumnIfNotExists(tableName string, columnName string, afterName string, colType string, defaultValue string) bool { @@ -369,6 +373,28 @@ func (us SqlUserStore) GetByEmail(teamId string, email string) 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 { storeChannel := make(StoreChannel) diff --git a/store/store.go b/store/store.go index 5b0e13fce..fac3a5bdb 100644 --- a/store/store.go +++ b/store/store.go @@ -85,6 +85,7 @@ type UserStore interface { Get(id string) StoreChannel GetProfiles(teamId string) StoreChannel GetByEmail(teamId string, email string) StoreChannel + GetByAuth(teamId string, authData string, authService string) StoreChannel GetByUsername(teamId string, username string) StoreChannel VerifyEmail(userId string) StoreChannel GetEtagForProfiles(teamId string) StoreChannel diff --git a/utils/config.go b/utils/config.go index e8fa9a477..163c912bf 100644 --- a/utils/config.go +++ b/utils/config.go @@ -32,6 +32,13 @@ type ServiceSettings struct { StorageDirectory string } +type SSOSettings struct { + AllowGitLabSSO bool + GitLabSecret string + GitLabId string + GitLabUrl string +} + type SqlSettings struct { DriverName string DataSource string @@ -109,6 +116,7 @@ type Config struct { EmailSettings EmailSettings PrivacySettings PrivacySettings TeamSettings TeamSettings + SSOSettings SSOSettings } func (o *Config) ToJson() string { diff --git a/web/react/components/signup_user_oauth.jsx b/web/react/components/signup_user_oauth.jsx new file mode 100644 index 000000000..40ed07ef8 --- /dev/null +++ b/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 = '/login/'+user.auth_service+'?id='+user.team_id; + }.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 ? : null; + var server_error = this.state.server_error ?
: null; + + var yourEmailIs = this.state.user.email == "" ? "" : Your email address is { this.state.user.email }.; + + return ( +
+ +

Welcome to { config.SiteName }

+

{"To continue signing up with " + this.state.user.auth_type + ", please register a username."}

+

Your username can be made of lowercase letters and numbers.

+ +
+ + { name_error } +
+

{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others"}

+

{ yourEmailIs } You’ll use this address to sign in to {config.SiteName}.

+
+

+ { server_error } +

By proceeding to create your account and use { config.SiteName }, you agree to our Terms of Service and Privacy Policy. If you do not agree, you cannot use {config.SiteName}.

+
+ ); + } +}); + + diff --git a/web/react/pages/signup_user_oauth.jsx b/web/react/pages/signup_user_oauth.jsx new file mode 100644 index 000000000..3bbb89f41 --- /dev/null +++ b/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) { + React.render( + , + document.getElementById('signup-user-complete') + ); +}; diff --git a/web/templates/signup_user_oauth.html b/web/templates/signup_user_oauth.html new file mode 100644 index 000000000..a973b8385 --- /dev/null +++ b/web/templates/signup_user_oauth.html @@ -0,0 +1,26 @@ +{{define "signup_user_oauth"}} + + +{{template "head" . }} + +
+
+
+
+ +
+ +
+ +
+
+ + + +{{end}} diff --git a/web/web.go b/web/web.go index 3e4bc2d53..85901a8d2 100644 --- a/web/web.go +++ b/web/web.go @@ -14,6 +14,7 @@ import ( "gopkg.in/fsnotify.v1" "html/template" "net/http" + "net/url" "strconv" "strings" ) @@ -52,6 +53,10 @@ 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-]+}/login", api.AppHandler(login)).Methods("GET") + + mainrouter.Handle("/login/gitlab", api.AppHandlerIndependent(loginWithGitLab)).Methods("GET") + mainrouter.Handle("/login/gitlab/complete", api.AppHandlerIndependent(loginCompleteGitLab)).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") // Bug in gorilla.mux pervents us from using regex here. @@ -61,6 +66,10 @@ func InitWeb() { mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).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/gitlab", api.AppHandlerIndependent(signupWithGitLab)).Methods("GET") + mainrouter.Handle("/signup/gitlab/complete", api.AppHandlerIndependent(signupCompleteGitLab)).Methods("GET") + mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") @@ -439,3 +448,81 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { page.Props["IsReset"] = strconv.FormatBool(isResetLink) page.Render(c, w) } + +func signupWithGitLab(c *api.Context, w http.ResponseWriter, r *http.Request) { + teamId := r.FormValue("id") + + if len(teamId) != 26 { + c.Err = model.NewAppError("signupWithGitLab", "Invalid team id", "team_id="+teamId) + return + } + + state := model.HashPassword(utils.Cfg.SSOSettings.GitLabId) + + authUrl := utils.Cfg.SSOSettings.GitLabUrl + "/oauth/authorize" + authUrl += "?response_type=code&client_id=" + utils.Cfg.SSOSettings.GitLabId + "&redirect_uri=" + url.QueryEscape("http://localhost:8065/signup/gitlab/complete?id="+teamId) + "&state=" + url.QueryEscape(state) + http.Redirect(w, r, authUrl, http.StatusFound) +} + +func signupCompleteGitLab(c *api.Context, w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + teamId := r.FormValue("id") + + uri := "http://localhost:8065/signup/gitlab/complete?id=" + teamId + + if glu, err := api.AuthorizeGitLabUser(code, state, uri); err != nil { + c.Err = err + return + } else { + user := model.UserFromGitLabUser(glu) + user.TeamId = teamId + + page := NewHtmlTemplatePage("signup_user_oauth", "Complete User Sign Up") + page.Props["User"] = user.ToJson() + page.Render(c, w) + } +} + +func loginWithGitLab(c *api.Context, w http.ResponseWriter, r *http.Request) { + teamId := r.FormValue("id") + + if len(teamId) != 26 { + c.Err = model.NewAppError("signupWithGitLab", "Invalid team id", "team_id="+teamId) + return + } + + state := model.HashPassword(utils.Cfg.SSOSettings.GitLabId) + + authUrl := utils.Cfg.SSOSettings.GitLabUrl + "/oauth/authorize" + authUrl += "?response_type=code&client_id=" + utils.Cfg.SSOSettings.GitLabId + "&redirect_uri=" + url.QueryEscape("http://localhost:8065/login/gitlab/complete?id="+teamId) + "&state=" + url.QueryEscape(state) + http.Redirect(w, r, authUrl, http.StatusFound) +} + +func loginCompleteGitLab(c *api.Context, w http.ResponseWriter, r *http.Request) { + code := r.URL.Query().Get("code") + state := r.URL.Query().Get("state") + teamId := r.FormValue("id") + + uri := "http://localhost:8065/login/gitlab/complete?id=" + teamId + + if glu, err := api.AuthorizeGitLabUser(code, state, uri); err != nil { + c.Err = err + return + } else { + var user *model.User + if result := <-api.Srv.Store.User().GetByAuth(teamId, strconv.FormatInt(glu.Id, 10), model.USER_AUTH_SERVICE_GITLAB); 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) + } + } +} -- cgit v1.2.3-1-g7c22 From 712f9a0b8c13f2d97a26b9030f215161f9b09511 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 15 Jul 2015 17:50:07 -0400 Subject: removed change password UI for oauth accounts --- web/react/components/login.jsx | 3 ++ web/react/components/setting_item_max.jsx | 2 +- web/react/components/user_settings.jsx | 74 +++++++++++++++++++------------ 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 71fefff5b..3fdaac32b 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -112,6 +112,9 @@ module.exports = React.createClass({
+
+ {"Log in with GitLab"} +
{"Find other " + strings.TeamPlural}
diff --git a/web/react/components/setting_item_max.jsx b/web/react/components/setting_item_max.jsx index b8b667e1a..49eb58773 100644 --- a/web/react/components/setting_item_max.jsx +++ b/web/react/components/setting_item_max.jsx @@ -20,7 +20,7 @@ module.exports = React.createClass({
{ server_error } { client_error } - Submit + { this.props.submit ? Submit : "" } Cancel diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index 59c97c309..0d3349dbc 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -590,7 +590,7 @@ var SecurityTab = React.createClass({ submitPassword: function(e) { e.preventDefault(); - var user = UserStore.getCurrentUser(); + var user = this.props.user; var currentPassword = this.state.current_password; var newPassword = this.state.new_password; var confirmPassword = this.state.confirm_password; @@ -648,53 +648,69 @@ var SecurityTab = React.createClass({ var self = this; if (this.props.activeSection === 'password') { var inputs = []; + var submit = null; - inputs.push( -
- -
- + if (this.props.user.auth_service === "") { + inputs.push( +
+ +
+ +
-
- ); - inputs.push( -
- -
- + ); + inputs.push( +
+ +
+ +
-
- ); - inputs.push( -
- -
- + ); + inputs.push( +
+ +
+ +
-
- ); + ); + + submit = this.submitPassword; + } else { + inputs.push( +
+ +
+ ); + } passwordSection = ( ); } else { - var d = new Date(this.props.user.last_password_update); - var hour = d.getHours() % 12 ? String(d.getHours() % 12) : "12"; - var min = d.getMinutes() < 10 ? "0" + d.getMinutes() : String(d.getMinutes()); - var timeOfDay = d.getHours() >= 12 ? " pm" : " am"; - var dateStr = "Last updated " + Constants.MONTHS[d.getMonth()] + " " + d.getDate() + ", " + d.getFullYear() + " at " + hour + ":" + min + timeOfDay; + var describe; + if (this.props.user.auth_service === "") { + var d = new Date(this.props.user.last_password_update); + var hour = d.getHours() % 12 ? String(d.getHours() % 12) : "12"; + 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 = ( ); -- cgit v1.2.3-1-g7c22 From 03528b9619747b8bd184b852497dcf14ee1e0081 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Fri, 17 Jul 2015 09:47:25 -0400 Subject: made oauth architecture more generalized --- api/user.go | 61 +++++++++++++++++++++++------- config/config.json | 12 ++++-- model/user.go | 4 ++ utils/config.go | 14 ++++--- web/web.go | 108 ++++++++++++++++++++++++++++++++++++----------------- 5 files changed, 141 insertions(+), 58 deletions(-) diff --git a/api/user.go b/api/user.go index 4765a5611..a9c8a0065 100644 --- a/api/user.go +++ b/api/user.go @@ -20,6 +20,7 @@ import ( _ "image/gif" _ "image/jpeg" "image/png" + "io" "net/http" "net/url" "strconv" @@ -1222,51 +1223,85 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) { } } -func AuthorizeGitLabUser(code, state, uri string) (*model.GitLabUser, *model.AppError) { - if !model.ComparePassword(state, utils.Cfg.SSOSettings.GitLabId) { - return nil, model.NewAppError("AuthorizeGitLabUser", "Invalid state", "") +func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, service, redirectUri string) { + + teamId := r.FormValue("id") + + if len(teamId) != 26 { + c.Err = model.NewAppError("GetAuthorizationCode", "Invalid team id", "team_id="+teamId) + c.Err.StatusCode = http.StatusBadRequest + return + } + + // Make sure team exists + if result := <-Srv.Store.Team().Get(teamId); result.Err != nil { + c.Err = result.Err + return + } + + 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+"?id="+teamId) + "&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.GitLabId) - p.Set("client_secret", utils.Cfg.SSOSettings.GitLabSecret) + 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", uri) + p.Set("redirect_uri", redirectUri) client := &http.Client{} - req, _ := http.NewRequest("POST", utils.Cfg.SSOSettings.GitLabUrl+"/oauth/token", strings.NewReader(p.Encode())) + 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("AuthorizeGitLabUser", "Token request to GitLab failed", err.Error()) + return nil, model.NewAppError("AuthorizeOAuthUser", "Token request to GitLab failed", err.Error()) } else { ar = model.AccessResponseFromJson(resp.Body) } if ar.TokenType != model.ACCESS_TOKEN_TYPE { - return nil, model.NewAppError("AuthorizeGitLabUser", "Bad token type", "token_type="+ar.TokenType) + return nil, model.NewAppError("AuthorizeOAuthUser", "Bad token type", "token_type="+ar.TokenType) } if len(ar.AccessToken) == 0 { - return nil, model.NewAppError("AuthorizeGitLabUser", "Missing access token", "") + return nil, model.NewAppError("AuthorizeOAuthUser", "Missing access token", "") } p = url.Values{} p.Set("access_token", ar.AccessToken) - req, _ = http.NewRequest("GET", utils.Cfg.SSOSettings.GitLabUrl+"/api/v3/user", strings.NewReader("")) + 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("AuthorizeGitLabUser", "Token request to GitLab failed", err.Error()) + return nil, model.NewAppError("AuthorizeOAuthUser", "Token request to "+service+" failed", err.Error()) } else { - return model.GitLabUserFromJson(resp.Body), nil + return resp.Body, nil } } diff --git a/config/config.json b/config/config.json index 61183948f..f92b873d1 100644 --- a/config/config.json +++ b/config/config.json @@ -24,10 +24,14 @@ "StorageDirectory": "./data/" }, "SSOSettings": { - "AllowGitLabSSO": true, - "GitLabSecret" : "8526ada64f38a1a67cafe6650d54310f1484f8a5d06ad23abb9f8e4b8af1c429", - "GitLabId": "0af4138195d246d5d4e958a93100379066bb087fa9892cd323b0c97bbd696008", - "GitLabUrl": "http://dockerhost:8080" + "gitlab": { + "Allow": true, + "Secret" : "8526ada64f38a1a67cafe6650d54310f1484f8a5d06ad23abb9f8e4b8af1c429", + "Id": "0af4138195d246d5d4e958a93100379066bb087fa9892cd323b0c97bbd696008", + "AuthEndpoint": "http://dockerhost:8080/oauth/authorize", + "TokenEndpoint": "http://dockerhost:8080/oauth/token", + "UserApiEndpoint": "http://dockerhost:8080/api/v3/user" + } }, "SqlSettings": { "DriverName": "mysql", diff --git a/model/user.go b/model/user.go index 22158b6ac..78b033327 100644 --- a/model/user.go +++ b/model/user.go @@ -385,3 +385,7 @@ func GitLabUserFromJson(data io.Reader) *GitLabUser { return nil } } + +func (glu *GitLabUser) GetAuthData() string { + return strconv.FormatInt(glu.Id, 10) +} diff --git a/utils/config.go b/utils/config.go index 163c912bf..b90337b7e 100644 --- a/utils/config.go +++ b/utils/config.go @@ -32,11 +32,13 @@ type ServiceSettings struct { StorageDirectory string } -type SSOSettings struct { - AllowGitLabSSO bool - GitLabSecret string - GitLabId string - GitLabUrl string +type SSOSetting struct { + Allow bool + Secret string + Id string + AuthEndpoint string + TokenEndpoint string + UserApiEndpoint string } type SqlSettings struct { @@ -116,7 +118,7 @@ type Config struct { EmailSettings EmailSettings PrivacySettings PrivacySettings TeamSettings TeamSettings - SSOSettings SSOSettings + SSOSettings map[string]SSOSetting } func (o *Config) ToJson() string { diff --git a/web/web.go b/web/web.go index 85901a8d2..71cf87335 100644 --- a/web/web.go +++ b/web/web.go @@ -14,7 +14,6 @@ import ( "gopkg.in/fsnotify.v1" "html/template" "net/http" - "net/url" "strconv" "strings" ) @@ -54,8 +53,8 @@ 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-]+}/login", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/login/gitlab", api.AppHandlerIndependent(loginWithGitLab)).Methods("GET") - mainrouter.Handle("/login/gitlab/complete", api.AppHandlerIndependent(loginCompleteGitLab)).Methods("GET") + mainrouter.Handle("/login/{service:[A-Za-z]+}", api.AppHandlerIndependent(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-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") @@ -67,8 +66,8 @@ func InitWeb() { mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") - mainrouter.Handle("/signup/gitlab", api.AppHandlerIndependent(signupWithGitLab)).Methods("GET") - mainrouter.Handle("/signup/gitlab/complete", api.AppHandlerIndependent(signupCompleteGitLab)).Methods("GET") + mainrouter.Handle("/signup/{service:[A-Za-z]+}", api.AppHandlerIndependent(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("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") @@ -449,33 +448,52 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { page.Render(c, w) } -func signupWithGitLab(c *api.Context, w http.ResponseWriter, r *http.Request) { - teamId := r.FormValue("id") - - if len(teamId) != 26 { - c.Err = model.NewAppError("signupWithGitLab", "Invalid team id", "team_id="+teamId) - return - } +func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] - state := model.HashPassword(utils.Cfg.SSOSettings.GitLabId) + redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" - authUrl := utils.Cfg.SSOSettings.GitLabUrl + "/oauth/authorize" - authUrl += "?response_type=code&client_id=" + utils.Cfg.SSOSettings.GitLabId + "&redirect_uri=" + url.QueryEscape("http://localhost:8065/signup/gitlab/complete?id="+teamId) + "&state=" + url.QueryEscape(state) - http.Redirect(w, r, authUrl, http.StatusFound) + api.GetAuthorizationCode(c, w, r, service, redirectUri) } -func signupCompleteGitLab(c *api.Context, w http.ResponseWriter, r *http.Request) { +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") teamId := r.FormValue("id") - uri := "http://localhost:8065/signup/gitlab/complete?id=" + teamId + uri := c.GetSiteURL() + "/signup/" + service + "/complete?id=" + teamId + + if len(teamId) != 26 { + c.Err = model.NewAppError("signupCompleteOAuth", "Invalid team id", "team_id="+teamId) + c.Err.StatusCode = http.StatusBadRequest + return + } + + // Make sure team exists + if result := <-api.Srv.Store.Team().Get(teamId); result.Err != nil { + c.Err = result.Err + return + } - if glu, err := api.AuthorizeGitLabUser(code, state, uri); err != nil { + if body, err := api.AuthorizeOAuthUser(service, code, state, uri); err != nil { c.Err = err return } else { - user := model.UserFromGitLabUser(glu) + 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 + } + user.TeamId = teamId page := NewHtmlTemplatePage("signup_user_oauth", "Complete User Sign Up") @@ -484,34 +502,54 @@ func signupCompleteGitLab(c *api.Context, w http.ResponseWriter, r *http.Request } } -func loginWithGitLab(c *api.Context, w http.ResponseWriter, r *http.Request) { - teamId := r.FormValue("id") - - if len(teamId) != 26 { - c.Err = model.NewAppError("signupWithGitLab", "Invalid team id", "team_id="+teamId) - return - } +func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + service := params["service"] - state := model.HashPassword(utils.Cfg.SSOSettings.GitLabId) + redirectUri := c.GetSiteURL() + "/login/" + service + "/complete" - authUrl := utils.Cfg.SSOSettings.GitLabUrl + "/oauth/authorize" - authUrl += "?response_type=code&client_id=" + utils.Cfg.SSOSettings.GitLabId + "&redirect_uri=" + url.QueryEscape("http://localhost:8065/login/gitlab/complete?id="+teamId) + "&state=" + url.QueryEscape(state) - http.Redirect(w, r, authUrl, http.StatusFound) + api.GetAuthorizationCode(c, w, r, service, redirectUri) } -func loginCompleteGitLab(c *api.Context, w http.ResponseWriter, r *http.Request) { +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") teamId := r.FormValue("id") - uri := "http://localhost:8065/login/gitlab/complete?id=" + teamId + uri := c.GetSiteURL() + "/login/" + service + "/complete?id=" + teamId - if glu, err := api.AuthorizeGitLabUser(code, state, uri); err != nil { + if len(teamId) != 26 { + c.Err = model.NewAppError("loginCompleteOAuth", "Invalid team id", "team_id="+teamId) + c.Err.StatusCode = http.StatusBadRequest + return + } + + // Make sure team exists + if result := <-api.Srv.Store.Team().Get(teamId); result.Err != nil { + c.Err = result.Err + return + } + + 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(teamId, strconv.FormatInt(glu.Id, 10), model.USER_AUTH_SERVICE_GITLAB); result.Err != nil { + if result := <-api.Srv.Store.User().GetByAuth(teamId, authData, service); result.Err != nil { c.Err = result.Err return } else { -- cgit v1.2.3-1-g7c22 From 62c0603c50c7cc85003fb03ed55d1a585c32dc34 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 22 Jul 2015 10:12:28 -0400 Subject: merged with new team domain changes and added signup/login links for gitlab --- api/user.go | 12 ++++----- config/config.json | 4 +-- web/react/components/login.jsx | 2 +- web/react/components/signup_user_complete.jsx | 11 +++++--- web/react/components/signup_user_oauth.jsx | 6 ++--- web/react/pages/signup_user_complete.jsx | 6 ++--- web/react/pages/signup_user_oauth.jsx | 4 +-- web/templates/signup_user_oauth.html | 2 +- web/web.go | 39 +++++++++++++++++---------- 9 files changed, 50 insertions(+), 36 deletions(-) diff --git a/api/user.go b/api/user.go index a9c8a0065..5aba21e79 100644 --- a/api/user.go +++ b/api/user.go @@ -1224,17 +1224,17 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) { } func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, service, redirectUri string) { + params := mux.Vars(r) + teamName := params["team"] - teamId := r.FormValue("id") - - if len(teamId) != 26 { - c.Err = model.NewAppError("GetAuthorizationCode", "Invalid team id", "team_id="+teamId) + if len(teamName) == 0 { + c.Err = model.NewAppError("GetAuthorizationCode", "Invalid team name", "team_name="+teamName) c.Err.StatusCode = http.StatusBadRequest return } // Make sure team exists - if result := <-Srv.Store.Team().Get(teamId); result.Err != nil { + if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { c.Err = result.Err return } @@ -1249,7 +1249,7 @@ func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, se endpoint := utils.Cfg.SSOSettings[service].AuthEndpoint state := model.HashPassword(clientId) - authUrl := endpoint + "?response_type=code&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirectUri+"?id="+teamId) + "&state=" + url.QueryEscape(state) + 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) } diff --git a/config/config.json b/config/config.json index f92b873d1..84e675a72 100644 --- a/config/config.json +++ b/config/config.json @@ -26,8 +26,8 @@ "SSOSettings": { "gitlab": { "Allow": true, - "Secret" : "8526ada64f38a1a67cafe6650d54310f1484f8a5d06ad23abb9f8e4b8af1c429", - "Id": "0af4138195d246d5d4e958a93100379066bb087fa9892cd323b0c97bbd696008", + "Secret" : "0495d3d6e528d91ba46605622a3645a8409ac5971ee287b1c3a6519fe27e6f6a", + "Id": "87a4aeb746c67e87a54df78f6eccf85229dd30a3a797bfdb423b82ba4e749cd0", "AuthEndpoint": "http://dockerhost:8080/oauth/authorize", "TokenEndpoint": "http://dockerhost:8080/oauth/token", "UserApiEndpoint": "http://dockerhost:8080/api/v3/user" diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 3fdaac32b..908e10f31 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -113,7 +113,7 @@ module.exports = React.createClass({
{"Find other " + strings.TeamPlural} diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index eed323d1f..1b1fe15fb 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -46,7 +46,7 @@ module.exports = React.createClass({ function(data) { 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) { UserStore.setLastEmail(this.state.user.email); UserStore.setCurrentUser(data); @@ -58,7 +58,7 @@ module.exports = React.createClass({ }.bind(this), function(err) { 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 { this.state.server_error = err.message; this.setState(this.state); @@ -79,7 +79,7 @@ module.exports = React.createClass({ props = {}; props.wizard = "welcome"; props.user = {}; - props.user.team_id = this.props.team_id; + props.user.team_id = this.props.teamId; props.user.email = this.props.email; props.hash = this.props.hash; props.data = this.props.data; @@ -116,7 +116,10 @@ module.exports = React.createClass({

Welcome to { config.SiteName }

-

{"Choose your username and password for the " + this.props.team_name + " " + strings.Team +"."}

+
+ +
+

{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team} {"or sign up with GitLab."}

Your username can be made of lowercase letters and numbers.

diff --git a/web/react/components/signup_user_oauth.jsx b/web/react/components/signup_user_oauth.jsx index 40ed07ef8..6322aedee 100644 --- a/web/react/components/signup_user_oauth.jsx +++ b/web/react/components/signup_user_oauth.jsx @@ -33,7 +33,7 @@ module.exports = React.createClass({ client.createUser(user, "", "", function(data) { client.track('signup', 'signup_user_oauth_02'); - window.location.href = '/login/'+user.auth_service+'?id='+user.team_id; + window.location.href = '/' + this.props.teamName + '/login/'+user.auth_service; }.bind(this), function(err) { this.state.server_error = err.message; @@ -63,14 +63,14 @@ module.exports = React.createClass({

Welcome to { config.SiteName }

-

{"To continue signing up with " + this.state.user.auth_type + ", please register a username."}

+

{"To continue signing up with " + this.state.user.auth_service + ", please register a username."}

Your username can be made of lowercase letters and numbers.

{ name_error }
-

{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others"}

+

{"Pick something " + strings.Team + "mates will recognize. Your username is how you will appear to others."}

{ yourEmailIs } You’ll use this address to sign in to {config.SiteName}.

diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx index a24c8d4c8..490702d3c 100644 --- a/web/react/pages/signup_user_complete.jsx +++ b/web/react/pages/signup_user_complete.jsx @@ -3,9 +3,9 @@ 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) { React.render( - , + , document.getElementById('signup-user-complete') ); -}; \ No newline at end of file +}; diff --git a/web/react/pages/signup_user_oauth.jsx b/web/react/pages/signup_user_oauth.jsx index 3bbb89f41..6a0707702 100644 --- a/web/react/pages/signup_user_oauth.jsx +++ b/web/react/pages/signup_user_oauth.jsx @@ -3,9 +3,9 @@ var SignupUserOAuth = require('../components/signup_user_oauth.jsx'); -global.window.setup_signup_user_oauth_page = function(user) { +global.window.setup_signup_user_oauth_page = function(user, team_name, team_display_name) { React.render( - , + , document.getElementById('signup-user-complete') ); }; diff --git a/web/templates/signup_user_oauth.html b/web/templates/signup_user_oauth.html index a973b8385..2eddb50d2 100644 --- a/web/templates/signup_user_oauth.html +++ b/web/templates/signup_user_oauth.html @@ -19,7 +19,7 @@
diff --git a/web/web.go b/web/web.go index 71cf87335..b0b926cd8 100644 --- a/web/web.go +++ b/web/web.go @@ -53,7 +53,8 @@ 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-]+}/login", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/login/{service:[A-Za-z]+}", api.AppHandlerIndependent(loginWithOAuth)).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") @@ -66,7 +67,8 @@ func InitWeb() { mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") - mainrouter.Handle("/signup/{service:[A-Za-z]+}", api.AppHandlerIndependent(signupWithOAuth)).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") @@ -463,20 +465,23 @@ func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) code := r.URL.Query().Get("code") state := r.URL.Query().Get("state") - teamId := r.FormValue("id") + teamName := r.FormValue("team") - uri := c.GetSiteURL() + "/signup/" + service + "/complete?id=" + teamId + uri := c.GetSiteURL() + "/signup/" + service + "/complete?team=" + teamName - if len(teamId) != 26 { - c.Err = model.NewAppError("signupCompleteOAuth", "Invalid team id", "team_id="+teamId) + 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 - if result := <-api.Srv.Store.Team().Get(teamId); result.Err != nil { + 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 { @@ -494,10 +499,12 @@ func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) return } - user.TeamId = teamId + 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) } } @@ -505,6 +512,7 @@ func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) service := params["service"] + l4g.Debug(service) redirectUri := c.GetSiteURL() + "/login/" + service + "/complete" @@ -517,20 +525,23 @@ func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) code := r.URL.Query().Get("code") state := r.URL.Query().Get("state") - teamId := r.FormValue("id") + teamName := r.FormValue("team") - uri := c.GetSiteURL() + "/login/" + service + "/complete?id=" + teamId + uri := c.GetSiteURL() + "/login/" + service + "/complete?team=" + teamName - if len(teamId) != 26 { - c.Err = model.NewAppError("loginCompleteOAuth", "Invalid team id", "team_id="+teamId) + 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 - if result := <-api.Srv.Store.Team().Get(teamId); result.Err != nil { + 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 { @@ -549,7 +560,7 @@ func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) } var user *model.User - if result := <-api.Srv.Store.User().GetByAuth(teamId, authData, service); result.Err != nil { + if result := <-api.Srv.Store.User().GetByAuth(team.Id, authData, service); result.Err != nil { c.Err = result.Err return } else { -- cgit v1.2.3-1-g7c22 From 7635e646a007123f989ba5c5747c4af1ebd27178 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 22 Jul 2015 11:14:51 -0400 Subject: added error message when trying to sign up with gitlab using an email already used in the system --- web/web.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/web.go b/web/web.go index b0b926cd8..15c978ff1 100644 --- a/web/web.go +++ b/web/web.go @@ -499,6 +499,11 @@ func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) 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") -- cgit v1.2.3-1-g7c22 From a2bd8b8676701ee5ccf5d84a2f4fe6afb0dae4b1 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 22 Jul 2015 11:26:55 -0400 Subject: add error for trying to sign up with the same oauth account twice --- api/user.go | 2 +- web/web.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/api/user.go b/api/user.go index 5aba21e79..68a4e6d56 100644 --- a/api/user.go +++ b/api/user.go @@ -1277,7 +1277,7 @@ func AuthorizeOAuthUser(service, code, state, redirectUri string) (io.ReadCloser var ar *model.AccessResponse if resp, err := client.Do(req); err != nil { - return nil, model.NewAppError("AuthorizeOAuthUser", "Token request to GitLab failed", err.Error()) + return nil, model.NewAppError("AuthorizeOAuthUser", "Token request failed", err.Error()) } else { ar = model.AccessResponseFromJson(resp.Body) } diff --git a/web/web.go b/web/web.go index 15c978ff1..ef2bae624 100644 --- a/web/web.go +++ b/web/web.go @@ -499,6 +499,11 @@ func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) 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 -- cgit v1.2.3-1-g7c22 From 4f0364d87656138d5e262b53373706ff122f3f4c Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 22 Jul 2015 12:13:45 -0400 Subject: added signup link verification to oauth signup flow --- api/user.go | 79 ++++++++++++--------------- web/react/components/signup_user_complete.jsx | 2 +- web/web.go | 56 ++++++++++++++++++- 3 files changed, 88 insertions(+), 49 deletions(-) diff --git a/api/user.go b/api/user.go index 68a4e6d56..40bac7bd5 100644 --- a/api/user.go +++ b/api/user.go @@ -81,36 +81,7 @@ func createUser(c *Context, w http.ResponseWriter, r *http.Request) { hash := r.URL.Query().Get("h") - shouldVerifyHash := true - - 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 { + if IsVerifyHashRequired(user, team, hash) { data := r.URL.Query().Get("d") props := model.MapFromJson(strings.NewReader(data)) @@ -147,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 { valet := &model.User{} valet.TeamId = team.Id @@ -1223,21 +1226,7 @@ func getStatuses(c *Context, w http.ResponseWriter, r *http.Request) { } } -func GetAuthorizationCode(c *Context, w http.ResponseWriter, r *http.Request, service, redirectUri string) { - params := mux.Vars(r) - teamName := params["team"] - - if len(teamName) == 0 { - c.Err = model.NewAppError("GetAuthorizationCode", "Invalid team name", "team_name="+teamName) - c.Err.StatusCode = http.StatusBadRequest - return - } - - // Make sure team exists - if result := <-Srv.Store.Team().GetByName(teamName); result.Err != nil { - c.Err = result.Err - 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) diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 1b1fe15fb..577651b90 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -119,7 +119,7 @@ module.exports = React.createClass({
-

{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team} {"or sign up with GitLab."}

+

{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team} {"or sign up with GitLab."}

Your username can be made of lowercase letters and numbers.

diff --git a/web/web.go b/web/web.go index ef2bae624..975b65002 100644 --- a/web/web.go +++ b/web/web.go @@ -453,10 +453,48 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { 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("createUser", "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("createUser", "The signup link has expired", "") + return + } + + if team.Id != props["id"] { + c.Err = model.NewAppError("createUser", "Invalid team name", data) + return + } + } redirectUri := c.GetSiteURL() + "/signup/" + service + "/complete" - api.GetAuthorizationCode(c, w, r, service, redirectUri) + api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri) } func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { @@ -522,11 +560,23 @@ func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) func loginWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) service := params["service"] - l4g.Debug(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, service, redirectUri) + api.GetAuthorizationCode(c, w, r, teamName, service, redirectUri) } func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { -- cgit v1.2.3-1-g7c22 From 44cfa364fd3c328523054d8ee2221d6019ad6de1 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 22 Jul 2015 12:42:03 -0400 Subject: added error case for login and removed authdata + authservice unique constraint in users table --- api/user.go | 32 +++++++++++++++++++++++--------- store/sql_user_store.go | 1 - 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/api/user.go b/api/user.go index 40bac7bd5..d16ad300a 100644 --- a/api/user.go +++ b/api/user.go @@ -241,38 +241,44 @@ func FireAndForgetVerifyEmail(userId, name, email, teamDisplayName, teamURL stri }() } -func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, deviceId string) { +func LoginById(c *Context, w http.ResponseWriter, r *http.Request, userId, password, deviceId string) *model.User { if result := <-Srv.Store.User().Get(userId); result.Err != nil { 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 } } + + return nil } -func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, deviceId string) { +func LoginByEmail(c *Context, w http.ResponseWriter, r *http.Request, email, name, password, deviceId string) *model.User { var team *model.Team if result := <-Srv.Store.Team().GetByName(name); result.Err != nil { c.Err = result.Err - return + return nil } else { team = result.Data.(*model.Team) } if result := <-Srv.Store.User().GetByEmail(team.Id, email); result.Err != nil { 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 } } + + return nil } func checkUserPassword(c *Context, user *model.User, password string) bool { @@ -356,7 +362,6 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, } http.SetCookie(w, sessionCookie) - user.Sanitize(map[string]bool{}) c.Session = *session c.LogAuditWithUserId(user.Id, "success") @@ -365,17 +370,26 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, func login(c *Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(r.Body) + var user *model.User if len(props["id"]) != 0 { - LoginById(c, w, r, props["id"], props["password"], props["device_id"]) + user = LoginById(c, w, r, props["id"], props["password"], props["device_id"]) } else if len(props["email"]) != 0 && len(props["name"]) != 0 { - LoginByEmail(c, w, r, props["email"], props["name"], props["password"], props["device_id"]) + 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", "") + return } if c.Err != nil { return } - w.Write([]byte("{}")) + 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) { diff --git a/store/sql_user_store.go b/store/sql_user_store.go index 3c25dbb44..fdc101b22 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -34,7 +34,6 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { table.ColMap("AuthService").SetMaxSize(32) table.SetUniqueTogether("Email", "TeamId") table.SetUniqueTogether("Username", "TeamId") - table.SetUniqueTogether("AuthData", "AuthService", "TeamId") } return us -- cgit v1.2.3-1-g7c22 From 41bbbbf4462205348c978a2cce5162f73e35f6b7 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 22 Jul 2015 15:05:20 -0400 Subject: add changes from team review --- api/user.go | 1 + config/config.json | 12 ++++++------ model/user.go | 2 +- store/sql_user_store.go | 4 ++-- web/web.go | 6 +++--- 5 files changed, 13 insertions(+), 12 deletions(-) diff --git a/api/user.go b/api/user.go index d16ad300a..7a688f28b 100644 --- a/api/user.go +++ b/api/user.go @@ -377,6 +377,7 @@ func login(c *Context, w http.ResponseWriter, r *http.Request) { 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 } diff --git a/config/config.json b/config/config.json index 84e675a72..14fd6e593 100644 --- a/config/config.json +++ b/config/config.json @@ -25,12 +25,12 @@ }, "SSOSettings": { "gitlab": { - "Allow": true, - "Secret" : "0495d3d6e528d91ba46605622a3645a8409ac5971ee287b1c3a6519fe27e6f6a", - "Id": "87a4aeb746c67e87a54df78f6eccf85229dd30a3a797bfdb423b82ba4e749cd0", - "AuthEndpoint": "http://dockerhost:8080/oauth/authorize", - "TokenEndpoint": "http://dockerhost:8080/oauth/token", - "UserApiEndpoint": "http://dockerhost:8080/api/v3/user" + "Allow": false, + "Secret" : "", + "Id": "", + "AuthEndpoint": "/oauth/authorize", + "TokenEndpoint": "/oauth/token", + "UserApiEndpoint": "/api/v3/user" } }, "SqlSettings": { diff --git a/model/user.go b/model/user.go index 78b033327..c71d75405 100644 --- a/model/user.go +++ b/model/user.go @@ -37,6 +37,7 @@ type User struct { Username string `json:"username"` Password string `json:"password"` AuthData string `json:"auth_data"` + AuthService string `json:"auth_service"` Email string `json:"email"` EmailVerified bool `json:"email_verified"` Nickname string `json:"nickname"` @@ -50,7 +51,6 @@ type User struct { NotifyProps StringMap `json:"notify_props"` LastPasswordUpdate int64 `json:"last_password_update"` LastPictureUpdate int64 `json:"last_picture_update"` - AuthService string `json:"auth_service"` } type GitLabUser struct { diff --git a/store/sql_user_store.go b/store/sql_user_store.go index fdc101b22..6cf12f5b8 100644 --- a/store/sql_user_store.go +++ b/store/sql_user_store.go @@ -24,6 +24,7 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { table.ColMap("Username").SetMaxSize(64) table.ColMap("Password").SetMaxSize(128) table.ColMap("AuthData").SetMaxSize(128) + table.ColMap("AuthService").SetMaxSize(32) table.ColMap("Email").SetMaxSize(128) table.ColMap("Nickname").SetMaxSize(64) table.ColMap("FirstName").SetMaxSize(64) @@ -31,7 +32,6 @@ func NewSqlUserStore(sqlStore *SqlStore) UserStore { table.ColMap("Roles").SetMaxSize(64) table.ColMap("Props").SetMaxSize(4000) table.ColMap("NotifyProps").SetMaxSize(2000) - table.ColMap("AuthService").SetMaxSize(32) table.SetUniqueTogether("Email", "TeamId") table.SetUniqueTogether("Username", "TeamId") } @@ -59,7 +59,7 @@ func (us SqlUserStore) UpgradeSchemaIfNeeded() { } } - us.CreateColumnIfNotExists("Users", "AuthService", "LastPictureUpdate", "varchar(32)", "") // for OAuth Client + 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 { diff --git a/web/web.go b/web/web.go index 975b65002..6bd4d09a0 100644 --- a/web/web.go +++ b/web/web.go @@ -476,18 +476,18 @@ func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { props := model.MapFromJson(strings.NewReader(data)) if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) { - c.Err = model.NewAppError("createUser", "The signup link does not appear to be valid", "") + 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("createUser", "The signup link has expired", "") + c.Err = model.NewAppError("signupWithOAuth", "The signup link has expired", "") return } if team.Id != props["id"] { - c.Err = model.NewAppError("createUser", "Invalid team name", data) + c.Err = model.NewAppError("signupWithOAuth", "Invalid team name", data) return } } -- cgit v1.2.3-1-g7c22 From 56a0a7d1e1fbd9405559a9c6e32962155d9cc562 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 22 Jul 2015 15:32:39 -0400 Subject: only show gitlab signup/login links if gitlab oauth is turned on --- config/config.json | 6 +++--- utils/config.go | 11 +++++++++++ web/react/components/login.jsx | 15 ++++++++++++--- web/react/components/signup_user_complete.jsx | 14 ++++++++++++-- web/react/pages/login.jsx | 4 ++-- web/react/pages/signup_user_complete.jsx | 4 ++-- web/templates/login.html | 2 +- web/templates/signup_user_complete.html | 2 +- web/web.go | 2 ++ 9 files changed, 46 insertions(+), 14 deletions(-) diff --git a/config/config.json b/config/config.json index 14fd6e593..591e38422 100644 --- a/config/config.json +++ b/config/config.json @@ -28,9 +28,9 @@ "Allow": false, "Secret" : "", "Id": "", - "AuthEndpoint": "/oauth/authorize", - "TokenEndpoint": "/oauth/token", - "UserApiEndpoint": "/api/v3/user" + "AuthEndpoint": "", + "TokenEndpoint": "", + "UserApiEndpoint": "" } }, "SqlSettings": { diff --git a/utils/config.go b/utils/config.go index b90337b7e..6a428a5c1 100644 --- a/utils/config.go +++ b/utils/config.go @@ -253,3 +253,14 @@ func IsS3Configured() bool { return true } + +func GetAllowedAuthServices() []string { + authServices := []string{} + for name, service := range Cfg.SSOSettings { + if service.Allow { + authServices = append(authServices, name) + } + } + + return authServices +} diff --git a/web/react/components/login.jsx b/web/react/components/login.jsx index 908e10f31..05918650b 100644 --- a/web/react/components/login.jsx +++ b/web/react/components/login.jsx @@ -90,6 +90,17 @@ module.exports = React.createClass({ focusEmail = true; } + var auth_services = JSON.parse(this.props.authServices); + + var login_message; + if (auth_services.indexOf("gitlab") >= 0) { + login_message = ( + + ); + } + return (
@@ -112,9 +123,7 @@ module.exports = React.createClass({
- + { login_message } diff --git a/web/react/components/signup_user_complete.jsx b/web/react/components/signup_user_complete.jsx index 577651b90..dc5ba64aa 100644 --- a/web/react/components/signup_user_complete.jsx +++ b/web/react/components/signup_user_complete.jsx @@ -103,7 +103,7 @@ module.exports = React.createClass({ var yourEmailIs = this.state.user.email == "" ? "" : Your email address is { this.state.user.email }. - var email = + var email = (
@@ -111,6 +111,16 @@ module.exports = React.createClass({ { email_error }
+ ); + + var auth_services = JSON.parse(this.props.authServices); + + var signup_message; + if (auth_services.indexOf("gitlab") >= 0) { + signup_message =

{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team} {"or sign up with GitLab."}

; + } else { + signup_message =

{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team + "."}

; + } return (
@@ -119,7 +129,7 @@ module.exports = React.createClass({
-

{"Choose your username and password for the " + this.props.teamDisplayName + " " + strings.Team} {"or sign up with GitLab."}

+ { signup_message }

Your username can be made of lowercase letters and numbers.

diff --git a/web/react/pages/login.jsx b/web/react/pages/login.jsx index 8348f0b5d..6e7528373 100644 --- a/web/react/pages/login.jsx +++ b/web/react/pages/login.jsx @@ -3,9 +3,9 @@ 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( - , + , document.getElementById('login') ); }; diff --git a/web/react/pages/signup_user_complete.jsx b/web/react/pages/signup_user_complete.jsx index 490702d3c..60c3a609a 100644 --- a/web/react/pages/signup_user_complete.jsx +++ b/web/react/pages/signup_user_complete.jsx @@ -3,9 +3,9 @@ var SignupUserComplete =require('../components/signup_user_complete.jsx'); -global.window.setup_signup_user_complete_page = function(email, name, ui_name, id, data, hash) { +global.window.setup_signup_user_complete_page = function(email, name, ui_name, id, data, hash, auth_services) { React.render( - , + , document.getElementById('signup-user-complete') ); }; diff --git a/web/templates/login.html b/web/templates/login.html index 24cebec8f..4b2813358 100644 --- a/web/templates/login.html +++ b/web/templates/login.html @@ -20,7 +20,7 @@
diff --git a/web/templates/signup_user_complete.html b/web/templates/signup_user_complete.html index 0cc655b63..176ca77b1 100644 --- a/web/templates/signup_user_complete.html +++ b/web/templates/signup_user_complete.html @@ -19,7 +19,7 @@
diff --git a/web/web.go b/web/web.go index 6bd4d09a0..1d59ef946 100644 --- a/web/web.go +++ b/web/web.go @@ -188,6 +188,7 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) { page := NewHtmlTemplatePage("login", "Login") page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamName"] = teamName + page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices()) page.Render(c, w) } @@ -274,6 +275,7 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) page.Props["TeamId"] = props["id"] page.Props["Data"] = data page.Props["Hash"] = hash + page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices()) page.Render(c, w) } -- cgit v1.2.3-1-g7c22 From dd970604d1790aa8b35a42862bace300c601f325 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 22 Jul 2015 15:36:58 -0400 Subject: add check on server to prevent password updating for users who log in through oauth --- api/user.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/api/user.go b/api/user.go index 7a688f28b..03f8b9e3e 100644 --- a/api/user.go +++ b/api/user.go @@ -821,6 +821,13 @@ func updatePassword(c *Context, w http.ResponseWriter, r *http.Request) { 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) { c.Err = model.NewAppError("updatePassword", "Update password failed because of invalid password", "") c.Err.StatusCode = http.StatusForbidden -- cgit v1.2.3-1-g7c22 From 14c1f2424de88545ec7e4386d53751cb3ec7d825 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Thu, 23 Jul 2015 00:54:40 +0500 Subject: Minor UI Improvements --- api/templates/post_body.html | 2 +- web/react/components/access_history_modal.jsx | 2 +- web/react/components/activity_log_modal.jsx | 5 ++++- web/react/components/setting_picture.jsx | 6 +++--- web/react/components/signup_team.jsx | 4 +++- web/react/components/signup_team_complete.jsx | 2 +- web/react/components/team_settings.jsx | 3 +-- web/react/components/user_settings.jsx | 4 ++-- web/sass-files/sass/partials/_access-history.scss | 4 ++-- web/sass-files/sass/partials/_headers.scss | 1 + web/sass-files/sass/partials/_responsive.scss | 8 +++++--- web/sass-files/sass/partials/_settings.scss | 8 +++++--- web/sass-files/sass/partials/_signup.scss | 2 +- 13 files changed, 30 insertions(+), 21 deletions(-) diff --git a/api/templates/post_body.html b/api/templates/post_body.html index 41a29d020..c0f4375d8 100644 --- a/api/templates/post_body.html +++ b/api/templates/post_body.html @@ -18,7 +18,7 @@

You were mentioned

-

CHANNEL: {{.Props.ChannelName}}
{{.Props.SenderName}} - {{.Props.Hour}}:{{.Props.Minute}} GMT, {{.Props.Month}} {{.Props.Day}}

{{.Props.PostMessage}}

+

CHANNEL: {{.Props.ChannelName}}
{{.Props.SenderName}} - {{.Props.Hour}}:{{.Props.Minute}} GMT, {{.Props.Month}} {{.Props.Day}}

{{.Props.PostMessage}}

Go To Channel

diff --git a/web/react/components/access_history_modal.jsx b/web/react/components/access_history_modal.jsx index b23b3213f..462f046f6 100644 --- a/web/react/components/access_history_modal.jsx +++ b/web/react/components/access_history_modal.jsx @@ -64,7 +64,7 @@ module.exports = React.createClass({
{"URL: " + currentAudit.action.replace("/api/v1", "")}
: - More info + More info }
{i < this.state.audits.length - 1 ? diff --git a/web/react/components/activity_log_modal.jsx b/web/react/components/activity_log_modal.jsx index d6f8f40eb..7cce807a9 100644 --- a/web/react/components/activity_log_modal.jsx +++ b/web/react/components/activity_log_modal.jsx @@ -68,6 +68,9 @@ module.exports = React.createClass({ else if (currentSession.props.platform === "Macintosh" || currentSession.props.platform === "iPhone") { devicePicture = "fa fa-apple"; } + else if (currentSession.props.platform === "Linux") { + devicePicture = "fa fa-linux"; + } activityList[i] = (
@@ -83,7 +86,7 @@ module.exports = React.createClass({
{"Session ID: " + currentSession.alt_id}
: - More info + More info }
diff --git a/web/react/components/setting_picture.jsx b/web/react/components/setting_picture.jsx index 62c889b7f..6cfb74d60 100644 --- a/web/react/components/setting_picture.jsx +++ b/web/react/components/setting_picture.jsx @@ -25,9 +25,9 @@ module.exports = React.createClass({ var img = null; if (this.props.picture) { - img = (); + img = (); } else { - img = (); + img = (); } var self = this; @@ -37,7 +37,7 @@ module.exports = React.createClass({
  • {this.props.title}
    • -
    • +
    • {img}
    • diff --git a/web/react/components/signup_team.jsx b/web/react/components/signup_team.jsx index cf982cc1e..362f79163 100644 --- a/web/react/components/signup_team.jsx +++ b/web/react/components/signup_team.jsx @@ -69,7 +69,9 @@ module.exports = React.createClass({ { name_error }
  • { server_error } - +
    + +
    {"Find my " + strings.Team}
    diff --git a/web/react/components/signup_team_complete.jsx b/web/react/components/signup_team_complete.jsx index 9ceeb6324..3e8a57308 100644 --- a/web/react/components/signup_team_complete.jsx +++ b/web/react/components/signup_team_complete.jsx @@ -246,7 +246,7 @@ TeamURLPage = React.createClass({

    {utils.toTitleCase(strings.Team) + " URL"}

    -
    +
    { window.location.origin + "/" } diff --git a/web/react/components/team_settings.jsx b/web/react/components/team_settings.jsx index 166b1f38b..3bbb5e892 100644 --- a/web/react/components/team_settings.jsx +++ b/web/react/components/team_settings.jsx @@ -73,13 +73,12 @@ var FeatureTab = React.createClass({ var inputs = []; inputs.push( -
    +

    Valet is a preview feature for enabling a non-user account limited to basic member permissions that can be manipulated by 3rd parties.

    IMPORTANT: The preview version of Valet should not be used without a secure connection and a trusted 3rd party, since user credentials are used to connect. OAuth2 will be used in the final release.
    -

    ); diff --git a/web/react/components/user_settings.jsx b/web/react/components/user_settings.jsx index ad890334e..1fbbf16ed 100644 --- a/web/react/components/user_settings.jsx +++ b/web/react/components/user_settings.jsx @@ -577,9 +577,9 @@ var SecurityTab = React.createClass({ { passwordSection }
    ); diff --git a/web/sass-files/sass/partials/_access-history.scss b/web/sass-files/sass/partials/_access-history.scss index f54c9a122..412a2a1d0 100644 --- a/web/sass-files/sass/partials/_access-history.scss +++ b/web/sass-files/sass/partials/_access-history.scss @@ -12,7 +12,7 @@ } .access__date { font-weight: 600; - font-size: 16px; + font-size: 15px; width: 190px; } .access__report { @@ -21,7 +21,7 @@ } .report__time { font-weight: 600; - font-size: 16px; + font-size: 15px; } .report__info { color: #999; diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index eab4becac..8dd0218ce 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -119,6 +119,7 @@ } .header__info { color: #fff; + padding-left: 3px } .team__name, .user__name { display: block; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index d8a8fd982..43796601d 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -451,13 +451,15 @@ color: #fff; .search__form { border: none; - padding: 0 60px 0 25px; + padding: 0 60px 0 35px; .form-control { - line-height: 31px; + line-height: normal; background: none; color: #fff; border-radius: 0; - padding: 0 10px 0; + padding: 0; + border-bottom: 1px solid #FFF; + border-bottom: 1px solid rgba(#fff, 0.6); @include input-placeholder { color: rgba(#fff, 0.6); } diff --git a/web/sass-files/sass/partials/_settings.scss b/web/sass-files/sass/partials/_settings.scss index b8dc9e997..1fb078bb9 100644 --- a/web/sass-files/sass/partials/_settings.scss +++ b/web/sass-files/sass/partials/_settings.scss @@ -163,8 +163,10 @@ } .profile-img { - width:158px; - max-height:128px; + width:128px; + height:128px; + margin-bottom: 10px; + @include border-radius(128px); } .sel-btn { @@ -232,4 +234,4 @@ .color-btn { margin:4px; -} \ No newline at end of file +} diff --git a/web/sass-files/sass/partials/_signup.scss b/web/sass-files/sass/partials/_signup.scss index 98931279b..db22718d2 100644 --- a/web/sass-files/sass/partials/_signup.scss +++ b/web/sass-files/sass/partials/_signup.scss @@ -6,7 +6,7 @@ } .signup-team__container { padding: 100px 0px 50px 0px; - max-width: 600px; + max-width: 380px; margin: 0 auto; font-size: 1.1em; position: relative; -- cgit v1.2.3-1-g7c22 From 78e3cdefb94c0fe378e4046e79e27be3c7997cb5 Mon Sep 17 00:00:00 2001 From: Asaad Mahmood Date: Thu, 23 Jul 2015 00:58:49 +0500 Subject: Updating dropdown icon in LHS header --- web/sass-files/sass/partials/_headers.scss | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/sass-files/sass/partials/_headers.scss b/web/sass-files/sass/partials/_headers.scss index 8dd0218ce..4351e167b 100644 --- a/web/sass-files/sass/partials/_headers.scss +++ b/web/sass-files/sass/partials/_headers.scss @@ -92,10 +92,10 @@ .navbar-right { font-size: 0.85em; position: absolute; - top: 20px; + top: 10px; right: 22px; .dropdown-toggle { - padding: 0 10px; + padding: 10px; } .dropdown-menu { li a { -- cgit v1.2.3-1-g7c22 From 8476062d214230e2370af87a9495457c7a5a2a6d Mon Sep 17 00:00:00 2001 From: ralder Date: Wed, 8 Jul 2015 12:51:43 -0700 Subject: [webui] fix command-list css --- web/react/components/command_list.jsx | 17 ++++---- web/react/components/error_bar.jsx | 42 +++++++++++--------- web/react/components/sidebar_header.jsx | 10 ++--- web/react/stores/user_store.jsx | 4 +- web/sass-files/sass/partials/_base.scss | 54 +------------------------- web/sass-files/sass/partials/_command-box.scss | 25 ++++++++++++ web/sass-files/sass/partials/_error-bar.scss | 25 ++++++++++++ web/sass-files/sass/partials/_mentions.scss | 1 + web/sass-files/sass/partials/_responsive.scss | 18 ++------- web/sass-files/sass/partials/_variables.scss | 6 ++- web/sass-files/sass/styles.scss | 2 + 11 files changed, 101 insertions(+), 103 deletions(-) create mode 100644 web/sass-files/sass/partials/_command-box.scss create mode 100644 web/sass-files/sass/partials/_error-bar.scss diff --git a/web/react/components/command_list.jsx b/web/react/components/command_list.jsx index 023f5f760..5efe98dc6 100644 --- a/web/react/components/command_list.jsx +++ b/web/react/components/command_list.jsx @@ -20,12 +20,7 @@ module.exports = React.createClass({ }, getSuggestedCommands: function(cmd) { - if (cmd == "") { - this.setState({ suggestions: [ ], cmd: "" }); - return; - } - - if (cmd.indexOf("/") != 0) { + if (!cmd || cmd.charAt(0) != '/') { this.setState({ suggestions: [ ], cmd: "" }); return; } @@ -35,17 +30,19 @@ module.exports = React.createClass({ cmd, true, function(data) { - if (data.suggestions.length === 1 && data.suggestions[0].suggestion === cmd) data.suggestions = []; + if (data.suggestions.length === 1 && data.suggestions[0].suggestion === cmd) { + data.suggestions = []; + } this.setState({ suggestions: data.suggestions, cmd: cmd }); }.bind(this), function(err){ - }.bind(this) + } ); }, render: function() { if (this.state.suggestions.length == 0) return (
    ); - var suggestions = [] + var suggestions = []; for (var i = 0; i < this.state.suggestions.length; i++) { if (this.state.suggestions[i].suggestion != this.state.cmd) { @@ -59,7 +56,7 @@ module.exports = React.createClass({ } return ( -
    +
    { suggestions }
    ); diff --git a/web/react/components/error_bar.jsx b/web/react/components/error_bar.jsx index d9d91ef51..f7514a009 100644 --- a/web/react/components/error_bar.jsx +++ b/web/react/components/error_bar.jsx @@ -8,21 +8,25 @@ var Constants = require('../utils/constants.jsx'); var ActionTypes = Constants.ActionTypes; function getStateFromStores() { - var error = ErrorStore.getLastError(); - if (error && error.message !== "There appears to be a problem with your internet connection") { - return { message: error.message }; - } else { - return { message: null }; - } + var error = ErrorStore.getLastError(); + if (error && error.message !== "There appears to be a problem with your internet connection") { + return { message: error.message }; + } else { + return { message: null }; + } } module.exports = React.createClass({ + displayName: 'ErrorBar', + componentDidMount: function() { ErrorStore.addChangeListener(this._onChange); - $('body').css('padding-top', $('#error_bar').outerHeight()); - $(window).resize(function(){ - $('body').css('padding-top', $('#error_bar').outerHeight()); - }); + $('body').css('padding-top', $(React.findDOMNode(this)).outerHeight()); + $(window).resize(function() { + if (this.state.message) { + $('body').css('padding-top', $(React.findDOMNode(this)).outerHeight()); + } + }.bind(this)); }, componentWillUnmount: function() { ErrorStore.removeChangeListener(this._onChange); @@ -31,39 +35,39 @@ module.exports = React.createClass({ var newState = getStateFromStores(); if (!utils.areStatesEqual(newState, this.state)) { if (newState.message) { - var self = this; - setTimeout(function(){self.handleClose();}, 10000); + setTimeout(this.handleClose, 10000); } + this.setState(newState); } }, handleClose: function(e) { if (e) e.preventDefault(); + AppDispatcher.handleServerAction({ type: ActionTypes.RECIEVED_ERROR, err: null }); + $('body').css('padding-top', '0'); }, getInitialState: function() { var state = getStateFromStores(); if (state.message) { - var self = this; - setTimeout(function(){self.handleClose();}, 10000); + setTimeout(this.handleClose, 10000); } return state; }, render: function() { - var message = this.state.message; - if (message) { + if (this.state.message) { return (
    - {message} - × + {this.state.message} + ×
    ); } else { return
    ; } } -}); +}); \ No newline at end of file diff --git a/web/react/components/sidebar_header.jsx b/web/react/components/sidebar_header.jsx index 7a7e92854..859e425a6 100644 --- a/web/react/components/sidebar_header.jsx +++ b/web/react/components/sidebar_header.jsx @@ -101,13 +101,13 @@ module.exports = React.createClass({ getDefaultProps: function() { return { - teamName: config.SiteName + teamDisplayName: config.SiteName }; }, render: function() { - var teamDisplayName = this.props.teamDisplayName ? this.props.teamDisplayName : config.SiteName; - var me = UserStore.getCurrentUser() + var me = UserStore.getCurrentUser(); + if (!me) { return null; } @@ -118,11 +118,11 @@ module.exports = React.createClass({ { me.last_picture_update ?
    { '@' + me.username}
    -
    { teamDisplayName }
    +
    { this.props.teamDisplayName }
    diff --git a/web/react/stores/user_store.jsx b/web/react/stores/user_store.jsx index d03016c5d..001162f47 100644 --- a/web/react/stores/user_store.jsx +++ b/web/react/stores/user_store.jsx @@ -183,6 +183,9 @@ var UserStore = assign({}, EventEmitter.prototype, { var keys = []; + if (!user) + return keys; + if (user.notify_props && user.notify_props.mention_keys) keys = keys.concat(user.notify_props.mention_keys.split(',')); if (user.first_name && user.notify_props.first_name === "true") keys.push(user.first_name); if (user.notify_props.all === "true") keys.push('@all'); @@ -258,4 +261,3 @@ UserStore.dispatchToken = AppDispatcher.register(function(payload) { UserStore.setMaxListeners(0); global.window.UserStore = UserStore; module.exports = UserStore; - diff --git a/web/sass-files/sass/partials/_base.scss b/web/sass-files/sass/partials/_base.scss index 1fb970075..52659521d 100644 --- a/web/sass-files/sass/partials/_base.scss +++ b/web/sass-files/sass/partials/_base.scss @@ -5,7 +5,7 @@ html, body { body { font-family: 'Open Sans', sans-serif; -webkit-font-smoothing: antialiased; - background: #e9e9e9; + background: $body-bg; position: relative; height: 100%; &.white { @@ -96,32 +96,6 @@ div.theme { position:relative; } -.command-box { - position:absolute; - background-color:#fff; - width:100%; - border:1px solid #ddd; - bottom: 38; -} - -.command-name { - position: relative; - width: 100%; - background-color: #fff; - height: 37px; - line-height: 37px; - padding: 2px 10px 2px 5px; - z-index: 101; -} - -.command-name:hover { - background-color:#e8eaed; -} - -.command-desc { - color: #a7a8ab; -} - @-webkit-keyframes spin2 { from { -webkit-transform: rotate(0deg);} to { -webkit-transform: rotate(360deg);} @@ -139,29 +113,3 @@ div.theme { .black-bg { background-color: black !important; } - -#error_bar { - background-color: #0099FF; - text-align:center; - position: relative; - color: #fff; - position: fixed; - top: 0; - width: 100%; - z-index: 9999; - - .error-bar { - padding: 5px 30px; - } - - .error-close { - position: absolute; - right: 0; - top: 0; - color: #FFF; - font-size: 20px; - font-weight: 600; - text-decoration: none; - padding: 0 10px; - } -} \ No newline at end of file diff --git a/web/sass-files/sass/partials/_command-box.scss b/web/sass-files/sass/partials/_command-box.scss new file mode 100644 index 000000000..44eb9b8df --- /dev/null +++ b/web/sass-files/sass/partials/_command-box.scss @@ -0,0 +1,25 @@ +.command-box { + position: absolute; + background-color: #fff; + width: 100%; + border: $border-gray; + bottom: 38px; + @extend %popover-box-shadow; +} + +.command-name { + position: relative; + width: 100%; + background-color: #fff; + height: 37px; + line-height: 37px; + padding: 2px 10px 2px 5px; + z-index: 101; + &:hover { + background-color: #e8eaed; + } +} + +.command-desc { + color: #a7a8ab; +} \ No newline at end of file diff --git a/web/sass-files/sass/partials/_error-bar.scss b/web/sass-files/sass/partials/_error-bar.scss new file mode 100644 index 000000000..2e3d3c87e --- /dev/null +++ b/web/sass-files/sass/partials/_error-bar.scss @@ -0,0 +1,25 @@ +.error-bar { + background-color: #0099FF; + text-align:center; + position: relative; + color: #fff; + position: fixed; + top: 0; + width: 100%; + z-index: 9999; + padding: 5px 30px; + &__close { + position: absolute; + right: 0; + top: 0; + color: #FFF; + font-size: 20px; + font-weight: 600; + text-decoration: none; + padding: 0 10px; + &:hover { + color: #FFF; + text-decoration: none; + } + } +} diff --git a/web/sass-files/sass/partials/_mentions.scss b/web/sass-files/sass/partials/_mentions.scss index 1396f21a1..a8c4dec26 100644 --- a/web/sass-files/sass/partials/_mentions.scss +++ b/web/sass-files/sass/partials/_mentions.scss @@ -10,6 +10,7 @@ .mentions--top { position: absolute; z-index: 1060; + @extend %popover-box-shadow; .mentions-box { width: 100%; height: 100%; diff --git a/web/sass-files/sass/partials/_responsive.scss b/web/sass-files/sass/partials/_responsive.scss index d8a8fd982..b4e4fa1df 100644 --- a/web/sass-files/sass/partials/_responsive.scss +++ b/web/sass-files/sass/partials/_responsive.scss @@ -462,20 +462,10 @@ color: rgba(#fff, 0.6); } } - ::-webkit-input-placeholder { - color: #fff; - } - - :-moz-placeholder { /* Firefox 18- */ - color: #fff; - } - - ::-moz-placeholder { /* Firefox 19+ */ - color: #fff; - } - - :-ms-input-placeholder { - color: #fff; + input[type=text] { + @include input-placeholder { + color: #fff; + } } } } diff --git a/web/sass-files/sass/partials/_variables.scss b/web/sass-files/sass/partials/_variables.scss index 5d883ab44..78952abb5 100644 --- a/web/sass-files/sass/partials/_variables.scss +++ b/web/sass-files/sass/partials/_variables.scss @@ -7,4 +7,8 @@ $primary-color: #2389D7; $primary-color--hover: darken(#2389D7, 5%); $body-bg: #e9e9e9; $header-bg: #f9f9f9; -$border-gray: 1px solid #ddd; \ No newline at end of file +$border-gray: 1px solid #ddd; + +%popover-box-shadow { + @include box-shadow(rgba(black, 0.175) 1px -3px 12px); +} diff --git a/web/sass-files/sass/styles.scss b/web/sass-files/sass/styles.scss index 294f6122a..ffd1f42b8 100644 --- a/web/sass-files/sass/styles.scss +++ b/web/sass-files/sass/styles.scss @@ -29,7 +29,9 @@ @import "partials/settings"; @import "partials/modal"; @import "partials/mentions"; +@import "partials/command-box"; @import "partials/error"; +@import "partials/error-bar"; @import "partials/loading"; // Responsive Css -- cgit v1.2.3-1-g7c22 From fabdc9a32661fd0410560ca918f5302249f9cfad Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Wed, 22 Jul 2015 16:27:08 -0400 Subject: added SSOSettings to docker config file --- config/config_docker.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/config_docker.json b/config/config_docker.json index 062cdef65..a9ed98f1a 100644 --- a/config/config_docker.json +++ b/config/config_docker.json @@ -23,6 +23,16 @@ "UseLocalStorage": true, "StorageDirectory": "/mattermost/data/" }, + "SSOSettings": { + "gitlab": { + "Allow": false, + "Secret" : "", + "Id": "", + "AuthEndpoint": "", + "TokenEndpoint": "", + "UserApiEndpoint": "" + } + }, "SqlSettings": { "DriverName": "mysql", "DataSource": "mmuser:mostest@tcp(localhost:3306)/mattermost_test", -- cgit v1.2.3-1-g7c22 From d42d0e3467c8eec38fdca429ba9ba5ac2af68db8 Mon Sep 17 00:00:00 2001 From: JoramWilander Date: Thu, 23 Jul 2015 08:19:51 -0400 Subject: added store unit test for user.GetByAuth and added password length checking in api.login --- api/user.go | 6 ++++++ store/sql_user_store_test.go | 19 +++++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/api/user.go b/api/user.go index 03f8b9e3e..e1d5e83dd 100644 --- a/api/user.go +++ b/api/user.go @@ -370,6 +370,12 @@ func Login(c *Context, w http.ResponseWriter, r *http.Request, user *model.User, 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"]) diff --git a/store/sql_user_store_test.go b/store/sql_user_store_test.go index 12737caa8..1f94021b2 100644 --- a/store/sql_user_store_test.go +++ b/store/sql_user_store_test.go @@ -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) { Setup() -- cgit v1.2.3-1-g7c22