mirror of
https://github.com/mattermost/mattermost.git
synced 2025-02-25 18:55:24 -06:00
PLT-3484 OAuth2 Service Provider (#3632)
* PLT-3484 OAuth2 Service Provider * PM text review for OAuth 2.0 Service Provider * PLT-3484 OAuth2 Service Provider UI tweaks (#3668) * Tweaks to help text * Pushing OAuth improvements (#3680) * Re-arrange System Console for OAuth 2.0 Provider
This commit is contained in:
@@ -68,6 +68,10 @@ func UserRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Han
|
||||
return &handler{h, true, false, false, false, false, false}
|
||||
}
|
||||
|
||||
func AppHandlerTrustRequester(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, false, false, false, false, false, true}
|
||||
}
|
||||
|
||||
func ApiAdminSystemRequired(h func(*Context, http.ResponseWriter, *http.Request)) http.Handler {
|
||||
return &handler{h, true, true, true, false, false, false}
|
||||
}
|
||||
@@ -102,7 +106,6 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
c.RequestId = model.NewId()
|
||||
c.IpAddress = GetIpAddress(r)
|
||||
c.TeamId = mux.Vars(r)["team_id"]
|
||||
h.isApi = IsApiCall(r)
|
||||
|
||||
token := ""
|
||||
isTokenFromQueryString := false
|
||||
@@ -147,10 +150,10 @@ func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set(model.HEADER_REQUEST_ID, c.RequestId)
|
||||
w.Header().Set(model.HEADER_VERSION_ID, fmt.Sprintf("%v.%v", model.CurrentVersion, utils.CfgLastModified))
|
||||
|
||||
// Instruct the browser not to display us in an iframe for anti-clickjacking
|
||||
// Instruct the browser not to display us in an iframe unless is the same origin for anti-clickjacking
|
||||
if !h.isApi {
|
||||
w.Header().Set("X-Frame-Options", "DENY")
|
||||
w.Header().Set("Content-Security-Policy", "frame-ancestors 'none'")
|
||||
w.Header().Set("X-Frame-Options", "SAMEORIGIN")
|
||||
w.Header().Set("Content-Security-Policy", "frame-ancestors 'self'")
|
||||
} else {
|
||||
// All api response bodies will be JSON formatted by default
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
|
||||
505
api/oauth.go
505
api/oauth.go
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/mattermost/platform/einterfaces"
|
||||
"github.com/mattermost/platform/model"
|
||||
"github.com/mattermost/platform/store"
|
||||
"github.com/mattermost/platform/utils"
|
||||
)
|
||||
|
||||
@@ -25,14 +26,17 @@ func InitOAuth() {
|
||||
l4g.Debug(utils.T("api.oauth.init.debug"))
|
||||
|
||||
BaseRoutes.OAuth.Handle("/register", ApiUserRequired(registerOAuthApp)).Methods("POST")
|
||||
BaseRoutes.OAuth.Handle("/list", ApiUserRequired(getOAuthApps)).Methods("GET")
|
||||
BaseRoutes.OAuth.Handle("/app/{client_id}", ApiUserRequired(getOAuthAppInfo)).Methods("GET")
|
||||
BaseRoutes.OAuth.Handle("/allow", ApiUserRequired(allowOAuth)).Methods("GET")
|
||||
BaseRoutes.OAuth.Handle("/delete", ApiUserRequired(deleteOAuthApp)).Methods("POST")
|
||||
BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/complete", AppHandlerIndependent(completeOAuth)).Methods("GET")
|
||||
BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/login", AppHandlerIndependent(loginWithOAuth)).Methods("GET")
|
||||
BaseRoutes.OAuth.Handle("/{service:[A-Za-z0-9]+}/signup", AppHandlerIndependent(signupWithOAuth)).Methods("GET")
|
||||
BaseRoutes.OAuth.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET")
|
||||
BaseRoutes.OAuth.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST")
|
||||
|
||||
BaseRoutes.Root.Handle("/authorize", ApiUserRequired(authorizeOAuth)).Methods("GET")
|
||||
BaseRoutes.Root.Handle("/authorize", AppHandlerTrustRequester(authorizeOAuth)).Methods("GET")
|
||||
BaseRoutes.Root.Handle("/access_token", ApiAppHandler(getAccessToken)).Methods("POST")
|
||||
|
||||
// Handle all the old routes, to be later removed
|
||||
@@ -48,6 +52,14 @@ func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
|
||||
if !c.IsSystemAdmin() {
|
||||
c.Err = model.NewLocAppError("registerOAuthApp", "api.command.admin_only.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
app := model.OAuthAppFromJson(r.Body)
|
||||
|
||||
if app == nil {
|
||||
@@ -65,7 +77,6 @@ func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
} else {
|
||||
app = result.Data.(*model.OAuthApp)
|
||||
app.ClientSecret = secret
|
||||
|
||||
c.LogAudit("client_id=" + app.Id)
|
||||
|
||||
@@ -75,6 +86,62 @@ func registerOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
||||
|
||||
func getOAuthApps(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
c.Err = model.NewLocAppError("getOAuthAppsByUser", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
isSystemAdmin := c.IsSystemAdmin()
|
||||
|
||||
if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
|
||||
if !isSystemAdmin {
|
||||
c.Err = model.NewLocAppError("getOAuthAppsByUser", "api.command.admin_only.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var ochan store.StoreChannel
|
||||
if isSystemAdmin {
|
||||
ochan = Srv.Store.OAuth().GetApps()
|
||||
} else {
|
||||
ochan = Srv.Store.OAuth().GetAppByUser(c.Session.UserId)
|
||||
}
|
||||
|
||||
if result := <-ochan; result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
apps := result.Data.([]*model.OAuthApp)
|
||||
w.Write([]byte(model.OAuthAppListToJson(apps)))
|
||||
}
|
||||
}
|
||||
|
||||
func getOAuthAppInfo(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
c.Err = model.NewLocAppError("getOAuthAppInfo", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
params := mux.Vars(r)
|
||||
|
||||
clientId := params["client_id"]
|
||||
|
||||
var app *model.OAuthApp
|
||||
if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getOAuthAppInfo", "api.oauth.allow_oauth.database.app_error", nil, "")
|
||||
return
|
||||
} else {
|
||||
app = result.Data.(*model.OAuthApp)
|
||||
}
|
||||
|
||||
app.Sanitize()
|
||||
w.Write([]byte(app.ToJson()))
|
||||
}
|
||||
|
||||
func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
|
||||
@@ -108,6 +175,10 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
scope := r.URL.Query().Get("scope")
|
||||
state := r.URL.Query().Get("state")
|
||||
|
||||
if len(scope) == 0 {
|
||||
scope = model.DEFAULT_SCOPE
|
||||
}
|
||||
|
||||
var app *model.OAuthApp
|
||||
if result := <-Srv.Store.OAuth().GetApp(clientId); result.Err != nil {
|
||||
c.Err = model.NewLocAppError("allowOAuth", "api.oauth.allow_oauth.database.app_error", nil, "")
|
||||
@@ -131,6 +202,20 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
authData := &model.AuthData{UserId: c.Session.UserId, ClientId: clientId, CreateAt: model.GetMillis(), RedirectUri: redirectUri, State: state, Scope: scope}
|
||||
authData.Code = model.HashPassword(fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, c.Session.UserId))
|
||||
|
||||
// this saves the OAuth2 app as authorized
|
||||
authorizedApp := model.Preference{
|
||||
UserId: c.Session.UserId,
|
||||
Category: model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP,
|
||||
Name: clientId,
|
||||
Value: scope,
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.Preference().Save(&model.Preferences{authorizedApp}); result.Err != nil {
|
||||
responseData["redirect"] = redirectUri + "?error=server_error&state=" + state
|
||||
w.Write([]byte(model.MapToJson(responseData)))
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.OAuth().SaveAuthData(authData); result.Err != nil {
|
||||
responseData["redirect"] = redirectUri + "?error=server_error&state=" + state
|
||||
w.Write([]byte(model.MapToJson(responseData)))
|
||||
@@ -140,7 +225,7 @@ func allowOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.LogAudit("success")
|
||||
|
||||
responseData["redirect"] = redirectUri + "?code=" + url.QueryEscape(authData.Code) + "&state=" + url.QueryEscape(authData.State)
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(model.MapToJson(responseData)))
|
||||
}
|
||||
|
||||
@@ -149,24 +234,16 @@ func RevokeAccessToken(token string) *model.AppError {
|
||||
schan := Srv.Store.Session().Remove(token)
|
||||
sessionCache.Remove(token)
|
||||
|
||||
var accessData *model.AccessData
|
||||
if result := <-Srv.Store.OAuth().GetAccessData(token); result.Err != nil {
|
||||
return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.get.app_error", nil, "")
|
||||
} else {
|
||||
accessData = result.Data.(*model.AccessData)
|
||||
}
|
||||
|
||||
tchan := Srv.Store.OAuth().RemoveAccessData(token)
|
||||
cchan := Srv.Store.OAuth().RemoveAuthData(accessData.AuthCode)
|
||||
|
||||
if result := <-tchan; result.Err != nil {
|
||||
return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_token.app_error", nil, "")
|
||||
}
|
||||
|
||||
if result := <-cchan; result.Err != nil {
|
||||
return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_code.app_error", nil, "")
|
||||
}
|
||||
|
||||
if result := <-schan; result.Err != nil {
|
||||
return model.NewLocAppError("RevokeAccessToken", "api.oauth.revoke_access_token.del_session.app_error", nil, "")
|
||||
}
|
||||
@@ -215,6 +292,10 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
c.Err = JoinUserToTeamById(teamId, user)
|
||||
}
|
||||
if c.Err == nil {
|
||||
if val, ok := props["redirect_to"]; ok {
|
||||
http.Redirect(w, r, c.GetSiteURL()+val, http.StatusTemporaryRedirect)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect)
|
||||
}
|
||||
break
|
||||
@@ -242,7 +323,7 @@ func completeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.disabled.app_error", nil, "")
|
||||
c.Err = model.NewLocAppError("authorizeOAuth", "api.oauth.authorize_oauth.disabled.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
@@ -253,8 +334,12 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
scope := r.URL.Query().Get("scope")
|
||||
state := r.URL.Query().Get("state")
|
||||
|
||||
if len(scope) == 0 {
|
||||
scope = model.DEFAULT_SCOPE
|
||||
}
|
||||
|
||||
if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 {
|
||||
c.Err = model.NewLocAppError("authorizeOAuth", "web.authorize_oauth.missing.app_error", nil, "")
|
||||
c.Err = model.NewLocAppError("authorizeOAuth", "api.oauth.authorize_oauth.missing.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
@@ -266,31 +351,67 @@ func authorizeOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
app = result.Data.(*model.OAuthApp)
|
||||
}
|
||||
|
||||
var team *model.Team
|
||||
if result := <-Srv.Store.Team().Get(c.TeamId); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
// here we should check if the user is logged in
|
||||
if len(c.Session.UserId) == 0 {
|
||||
http.Redirect(w, r, c.GetSiteURL()+"/login?redirect_to="+url.QueryEscape(r.RequestURI), http.StatusFound)
|
||||
return
|
||||
} else {
|
||||
team = result.Data.(*model.Team)
|
||||
}
|
||||
|
||||
page := utils.NewHTMLTemplate("authorize", c.Locale)
|
||||
page.Props["Title"] = c.T("web.authorize_oauth.title")
|
||||
page.Props["TeamName"] = team.Name
|
||||
page.Props["AppName"] = app.Name
|
||||
page.Props["ResponseType"] = responseType
|
||||
page.Props["ClientId"] = clientId
|
||||
page.Props["RedirectUri"] = redirect
|
||||
page.Props["Scope"] = scope
|
||||
page.Props["State"] = state
|
||||
if err := page.RenderToWriter(w); err != nil {
|
||||
c.SetUnknownError(page.TemplateName, err.Error())
|
||||
isAuthorized := false
|
||||
if result := <-Srv.Store.Preference().Get(c.Session.UserId, model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, clientId); result.Err == nil {
|
||||
// when we support scopes we should check if the scopes match
|
||||
isAuthorized = true
|
||||
}
|
||||
|
||||
// Automatically allow if the app is trusted
|
||||
if app.IsTrusted || isAuthorized {
|
||||
closeBody := func(r *http.Response) {
|
||||
if r.Body != nil {
|
||||
ioutil.ReadAll(r.Body)
|
||||
r.Body.Close()
|
||||
}
|
||||
}
|
||||
|
||||
doAllow := func() (*http.Response, *model.AppError) {
|
||||
HttpClient := &http.Client{}
|
||||
url := c.GetSiteURL() + "/api/v3/oauth/allow?response_type=" + model.AUTHCODE_RESPONSE_TYPE + "&client_id=" + clientId + "&redirect_uri=" + url.QueryEscape(redirect) + "&scope=" + scope + "&state=" + url.QueryEscape(state)
|
||||
rq, _ := http.NewRequest("GET", url, strings.NewReader(""))
|
||||
|
||||
rq.Header.Set(model.HEADER_AUTH, model.HEADER_BEARER+" "+c.Session.Token)
|
||||
|
||||
if rp, err := HttpClient.Do(rq); err != nil {
|
||||
return nil, model.NewLocAppError(url, "model.client.connecting.app_error", nil, err.Error())
|
||||
} else if rp.StatusCode == 304 {
|
||||
return rp, nil
|
||||
} else if rp.StatusCode >= 300 {
|
||||
defer closeBody(rp)
|
||||
return rp, model.AppErrorFromJson(rp.Body)
|
||||
} else {
|
||||
return rp, nil
|
||||
}
|
||||
}
|
||||
|
||||
if result, err := doAllow(); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
//defer closeBody(result)
|
||||
data := model.MapFromJson(result.Body)
|
||||
redirectTo := data["redirect"]
|
||||
http.Redirect(w, r, redirectTo, http.StatusFound)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
|
||||
w.Header().Set("Cache-Control", "no-cache, max-age=31556926, public")
|
||||
http.ServeFile(w, r, utils.FindDir("webapp/dist")+"root.html")
|
||||
}
|
||||
|
||||
func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.disabled.app_error", nil, "")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.disabled.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
@@ -299,126 +420,167 @@ func getAccessToken(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
r.ParseForm()
|
||||
|
||||
code := r.FormValue("code")
|
||||
refreshToken := r.FormValue("refresh_token")
|
||||
|
||||
grantType := r.FormValue("grant_type")
|
||||
if grantType != model.ACCESS_TOKEN_GRANT_TYPE {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_grant.app_error", nil, "")
|
||||
switch grantType {
|
||||
case model.ACCESS_TOKEN_GRANT_TYPE:
|
||||
if len(code) == 0 {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.missing_code.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
case model.REFRESH_TOKEN_GRANT_TYPE:
|
||||
if len(refreshToken) == 0 {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.missing_refresh_token.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
default:
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.bad_grant.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
clientId := r.FormValue("client_id")
|
||||
if len(clientId) != 26 {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_id.app_error", nil, "")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.bad_client_id.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
secret := r.FormValue("client_secret")
|
||||
if len(secret) == 0 {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.bad_client_secret.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
code := r.FormValue("code")
|
||||
if len(code) == 0 {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.missing_code.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
redirectUri := r.FormValue("redirect_uri")
|
||||
|
||||
achan := Srv.Store.OAuth().GetApp(clientId)
|
||||
tchan := Srv.Store.OAuth().GetAccessDataByAuthCode(code)
|
||||
|
||||
authData := GetAuthData(code)
|
||||
|
||||
if authData == nil {
|
||||
c.LogAudit("fail - invalid auth code")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
uchan := Srv.Store.User().Get(authData.UserId)
|
||||
|
||||
if authData.IsExpired() {
|
||||
c.LogAudit("fail - auth code expired")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
if authData.RedirectUri != redirectUri {
|
||||
c.LogAudit("fail - redirect uri provided did not match previous redirect uri")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.redirect_uri.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) {
|
||||
c.LogAudit("fail - auth code is invalid")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.expired_code.app_error", nil, "")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.bad_client_secret.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
var app *model.OAuthApp
|
||||
achan := Srv.Store.OAuth().GetApp(clientId)
|
||||
if result := <-achan; result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "")
|
||||
return
|
||||
} else {
|
||||
app = result.Data.(*model.OAuthApp)
|
||||
}
|
||||
|
||||
if !model.ComparePassword(app.ClientSecret, secret) {
|
||||
if app.ClientSecret != secret {
|
||||
c.LogAudit("fail - invalid client credentials")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.credentials.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
callback := redirectUri
|
||||
if len(callback) == 0 {
|
||||
callback = app.CallbackUrls[0]
|
||||
}
|
||||
|
||||
if result := <-tchan; result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal.app_error", nil, "")
|
||||
return
|
||||
} else if result.Data != nil {
|
||||
c.LogAudit("fail - auth code has been used previously")
|
||||
accessData := result.Data.(*model.AccessData)
|
||||
|
||||
// Revoke access token, related auth code, and session from DB as well as from cache
|
||||
if err := RevokeAccessToken(accessData.Token); err != nil {
|
||||
l4g.Error(utils.T("web.get_access_token.revoking.error") + err.Message)
|
||||
}
|
||||
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.exchanged.app_error", nil, "")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.credentials.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
var user *model.User
|
||||
if result := <-uchan; result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_user.app_error", nil, "")
|
||||
return
|
||||
var accessData *model.AccessData
|
||||
var accessRsp *model.AccessResponse
|
||||
if grantType == model.ACCESS_TOKEN_GRANT_TYPE {
|
||||
redirectUri := r.FormValue("redirect_uri")
|
||||
authData := GetAuthData(code)
|
||||
|
||||
if authData == nil {
|
||||
c.LogAudit("fail - invalid auth code")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
if authData.IsExpired() {
|
||||
<-Srv.Store.OAuth().RemoveAuthData(authData.Code)
|
||||
c.LogAudit("fail - auth code expired")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
if authData.RedirectUri != redirectUri {
|
||||
c.LogAudit("fail - redirect uri provided did not match previous redirect uri")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.redirect_uri.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) {
|
||||
c.LogAudit("fail - auth code is invalid")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.expired_code.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
uchan := Srv.Store.User().Get(authData.UserId)
|
||||
if result := <-uchan; result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "")
|
||||
return
|
||||
} else {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
tchan := Srv.Store.OAuth().GetPreviousAccessData(user.Id, clientId)
|
||||
if result := <-tchan; result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal.app_error", nil, "")
|
||||
return
|
||||
} else if result.Data != nil {
|
||||
accessData := result.Data.(*model.AccessData)
|
||||
if accessData.IsExpired() {
|
||||
if access, err := newSessionUpdateToken(app.Name, accessData, user); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
accessRsp = access
|
||||
}
|
||||
} else {
|
||||
//return the same token and no need to create a new session
|
||||
accessRsp = &model.AccessResponse{
|
||||
AccessToken: accessData.Token,
|
||||
TokenType: model.ACCESS_TOKEN_TYPE,
|
||||
ExpiresIn: int32((accessData.ExpiresAt - model.GetMillis()) / 1000),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// create a new session and return new access token
|
||||
var session *model.Session
|
||||
if result, err := newSession(app.Name, user); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
session = result
|
||||
}
|
||||
|
||||
accessData = &model.AccessData{ClientId: clientId, UserId: user.Id, Token: session.Token, RefreshToken: model.NewId(), RedirectUri: redirectUri, ExpiresAt: session.ExpiresAt}
|
||||
|
||||
if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
|
||||
l4g.Error(result.Err)
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_saving.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
accessRsp = &model.AccessResponse{
|
||||
AccessToken: session.Token,
|
||||
TokenType: model.ACCESS_TOKEN_TYPE,
|
||||
RefreshToken: accessData.RefreshToken,
|
||||
ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24),
|
||||
}
|
||||
}
|
||||
|
||||
<-Srv.Store.OAuth().RemoveAuthData(authData.Code)
|
||||
} else {
|
||||
user = result.Data.(*model.User)
|
||||
// when grantType is refresh_token
|
||||
if result := <-Srv.Store.OAuth().GetAccessDataByRefreshToken(refreshToken); result.Err != nil {
|
||||
c.LogAudit("fail - refresh token is invalid")
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.refresh_token.app_error", nil, "")
|
||||
return
|
||||
} else {
|
||||
accessData = result.Data.(*model.AccessData)
|
||||
}
|
||||
|
||||
uchan := Srv.Store.User().Get(accessData.UserId)
|
||||
if result := <-uchan; result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_user.app_error", nil, "")
|
||||
return
|
||||
} else {
|
||||
user = result.Data.(*model.User)
|
||||
}
|
||||
|
||||
if access, err := newSessionUpdateToken(app.Name, accessData, user); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
accessRsp = access
|
||||
}
|
||||
}
|
||||
|
||||
session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true}
|
||||
|
||||
if result := <-Srv.Store.Session().Save(session); result.Err != nil {
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_session.app_error", nil, "")
|
||||
return
|
||||
} else {
|
||||
session = result.Data.(*model.Session)
|
||||
AddSessionToCache(session)
|
||||
}
|
||||
|
||||
accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback}
|
||||
|
||||
if result := <-Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil {
|
||||
l4g.Error(result.Err)
|
||||
c.Err = model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24)}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
@@ -432,6 +594,7 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
params := mux.Vars(r)
|
||||
service := params["service"]
|
||||
loginHint := r.URL.Query().Get("login_hint")
|
||||
redirectTo := r.URL.Query().Get("redirect_to")
|
||||
|
||||
teamId, err := getTeamIdFromQuery(r.URL.Query())
|
||||
if err != nil {
|
||||
@@ -445,6 +608,10 @@ func loginWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
stateProps["team_id"] = teamId
|
||||
}
|
||||
|
||||
if len(redirectTo) != 0 {
|
||||
stateProps["redirect_to"] = redirectTo
|
||||
}
|
||||
|
||||
if authUrl, err := GetAuthorizationCode(c, service, stateProps, loginHint); err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
@@ -462,12 +629,12 @@ func getTeamIdFromQuery(query url.Values) (string, *model.AppError) {
|
||||
props := model.MapFromJson(strings.NewReader(data))
|
||||
|
||||
if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) {
|
||||
return "", model.NewLocAppError("getTeamIdFromQuery", "web.singup_with_oauth.invalid_link.app_error", nil, "")
|
||||
return "", model.NewLocAppError("getTeamIdFromQuery", "api.oauth.singup_with_oauth.invalid_link.app_error", nil, "")
|
||||
}
|
||||
|
||||
t, err := strconv.ParseInt(props["time"], 10, 64)
|
||||
if err != nil || model.GetMillis()-t > 1000*60*60*48 { // 48 hours
|
||||
return "", model.NewLocAppError("getTeamIdFromQuery", "web.singup_with_oauth.expired_link.app_error", nil, "")
|
||||
return "", model.NewLocAppError("getTeamIdFromQuery", "api.oauth.singup_with_oauth.expired_link.app_error", nil, "")
|
||||
}
|
||||
|
||||
return props["id"], nil
|
||||
@@ -488,7 +655,7 @@ func signupWithOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
service := params["service"]
|
||||
|
||||
if !utils.Cfg.TeamSettings.EnableUserCreation {
|
||||
c.Err = model.NewLocAppError("signupWithOAuth", "web.singup_with_oauth.disabled.app_error", nil, "")
|
||||
c.Err = model.NewLocAppError("signupWithOAuth", "api.oauth.singup_with_oauth.disabled.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
@@ -666,3 +833,93 @@ func CompleteSwitchWithOAuth(c *Context, w http.ResponseWriter, r *http.Request,
|
||||
|
||||
go sendSignInChangeEmail(c, user.Email, c.GetSiteURL(), strings.Title(service)+" SSO")
|
||||
}
|
||||
|
||||
func deleteOAuthApp(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
c.Err = model.NewLocAppError("deleteOAuthApp", "api.oauth.allow_oauth.turn_off.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusNotImplemented
|
||||
return
|
||||
}
|
||||
|
||||
isSystemAdmin := c.IsSystemAdmin()
|
||||
|
||||
if *utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations {
|
||||
if !isSystemAdmin {
|
||||
c.Err = model.NewLocAppError("deleteOAuthApp", "api.command.admin_only.app_error", nil, "")
|
||||
c.Err.StatusCode = http.StatusForbidden
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.LogAudit("attempt")
|
||||
|
||||
props := model.MapFromJson(r.Body)
|
||||
|
||||
id := props["id"]
|
||||
if len(id) == 0 {
|
||||
c.SetInvalidParam("deleteOAuthApp", "id")
|
||||
return
|
||||
}
|
||||
|
||||
if result := <-Srv.Store.OAuth().GetApp(id); result.Err != nil {
|
||||
c.Err = result.Err
|
||||
return
|
||||
} else {
|
||||
if c.Session.UserId != result.Data.(*model.OAuthApp).CreatorId && !isSystemAdmin {
|
||||
c.LogAudit("fail - inappropriate permissions")
|
||||
c.Err = model.NewLocAppError("deleteOAuthApp", "api.oauth.delete.permissions.app_error", nil, "user_id="+c.Session.UserId)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err := (<-Srv.Store.OAuth().DeleteApp(id)).Err; err != nil {
|
||||
c.Err = err
|
||||
return
|
||||
}
|
||||
|
||||
c.LogAudit("success")
|
||||
ReturnStatusOK(w)
|
||||
}
|
||||
|
||||
func newSession(appName string, user *model.User) (*model.Session, *model.AppError) {
|
||||
// set new token an session
|
||||
session := &model.Session{UserId: user.Id, Roles: user.Roles, IsOAuth: true}
|
||||
session.SetExpireInDays(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays)
|
||||
session.AddProp(model.SESSION_PROP_PLATFORM, appName)
|
||||
session.AddProp(model.SESSION_PROP_OS, "OAuth2")
|
||||
session.AddProp(model.SESSION_PROP_BROWSER, "OAuth2")
|
||||
|
||||
if result := <-Srv.Store.Session().Save(session); result.Err != nil {
|
||||
return nil, model.NewLocAppError("getAccessToken", "api.oauth.get_access_token.internal_session.app_error", nil, "")
|
||||
} else {
|
||||
session = result.Data.(*model.Session)
|
||||
AddSessionToCache(session)
|
||||
}
|
||||
|
||||
return session, nil
|
||||
}
|
||||
|
||||
func newSessionUpdateToken(appName string, accessData *model.AccessData, user *model.User) (*model.AccessResponse, *model.AppError) {
|
||||
var session *model.Session
|
||||
<-Srv.Store.Session().Remove(accessData.Token) //remove the previous session
|
||||
|
||||
if result, err := newSession(appName, user); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
session = result
|
||||
}
|
||||
|
||||
accessData.Token = session.Token
|
||||
accessData.ExpiresAt = session.ExpiresAt
|
||||
if result := <-Srv.Store.OAuth().UpdateAccessData(accessData); result.Err != nil {
|
||||
l4g.Error(result.Err)
|
||||
return nil, model.NewLocAppError("getAccessToken", "web.get_access_token.internal_saving.app_error", nil, "")
|
||||
}
|
||||
accessRsp := &model.AccessResponse{
|
||||
AccessToken: session.Token,
|
||||
TokenType: model.ACCESS_TOKEN_TYPE,
|
||||
ExpiresIn: int32(*utils.Cfg.ServiceSettings.SessionLengthSSOInDays * 60 * 60 * 24),
|
||||
}
|
||||
|
||||
return accessRsp, nil
|
||||
}
|
||||
|
||||
@@ -11,131 +11,245 @@ import (
|
||||
)
|
||||
|
||||
func TestRegisterApp(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
Client := th.BasicClient
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
Client := th.SystemAdminClient
|
||||
|
||||
app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
|
||||
if _, err := Client.RegisterApp(app); err == nil {
|
||||
t.Fatal("should have failed - oauth providing turned off")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true
|
||||
|
||||
Client.Logout()
|
||||
|
||||
if _, err := Client.RegisterApp(app); err == nil {
|
||||
t.Fatal("not logged in - should have failed")
|
||||
}
|
||||
|
||||
th.LoginSystemAdmin()
|
||||
|
||||
if result, err := Client.RegisterApp(app); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
|
||||
Client.Logout()
|
||||
|
||||
if _, err := Client.RegisterApp(app); err == nil {
|
||||
t.Fatal("not logged in - should have failed")
|
||||
rapp := result.Data.(*model.OAuthApp)
|
||||
if len(rapp.Id) != 26 {
|
||||
t.Fatal("clientid didn't return properly")
|
||||
}
|
||||
|
||||
th.LoginBasic()
|
||||
|
||||
if result, err := Client.RegisterApp(app); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rapp := result.Data.(*model.OAuthApp)
|
||||
if len(rapp.Id) != 26 {
|
||||
t.Fatal("clientid didn't return properly")
|
||||
}
|
||||
if len(rapp.ClientSecret) != 26 {
|
||||
t.Fatal("client secret didn't return properly")
|
||||
}
|
||||
if len(rapp.ClientSecret) != 26 {
|
||||
t.Fatal("client secret didn't return properly")
|
||||
}
|
||||
}
|
||||
|
||||
app = &model.OAuthApp{Name: "", Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
if _, err := Client.RegisterApp(app); err == nil {
|
||||
t.Fatal("missing name - should have failed")
|
||||
}
|
||||
app = &model.OAuthApp{Name: "", Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
if _, err := Client.RegisterApp(app); err == nil {
|
||||
t.Fatal("missing name - should have failed")
|
||||
}
|
||||
|
||||
app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
if _, err := Client.RegisterApp(app); err == nil {
|
||||
t.Fatal("missing homepage - should have failed")
|
||||
}
|
||||
app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
if _, err := Client.RegisterApp(app); err == nil {
|
||||
t.Fatal("missing homepage - should have failed")
|
||||
}
|
||||
|
||||
app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{}}
|
||||
if _, err := Client.RegisterApp(app); err == nil {
|
||||
t.Fatal("missing callback url - should have failed")
|
||||
}
|
||||
app = &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{}}
|
||||
if _, err := Client.RegisterApp(app); err == nil {
|
||||
t.Fatal("missing callback url - should have failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowOAuth(t *testing.T) {
|
||||
th := Setup().InitBasic()
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
Client := th.BasicClient
|
||||
AdminClient := th.SystemAdminClient
|
||||
|
||||
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true
|
||||
app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
app = AdminClient.Must(AdminClient.RegisterApp(app)).Data.(*model.OAuthApp)
|
||||
|
||||
state := "123"
|
||||
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "12345678901234567890123456", app.CallbackUrls[0], "all", state); err == nil {
|
||||
t.Fatal("should have failed - oauth service providing turned off")
|
||||
}
|
||||
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = false
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", state); err == nil {
|
||||
t.Fatal("should have failed - oauth providing turned off")
|
||||
}
|
||||
|
||||
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true
|
||||
|
||||
if result, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", state); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
app = Client.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp)
|
||||
redirect := result.Data.(map[string]string)["redirect"]
|
||||
if len(redirect) == 0 {
|
||||
t.Fatal("redirect url should be set")
|
||||
}
|
||||
|
||||
if result, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", state); err != nil {
|
||||
t.Fatal(err)
|
||||
ru, _ := url.Parse(redirect)
|
||||
if ru == nil {
|
||||
t.Fatal("redirect url unparseable")
|
||||
} else {
|
||||
redirect := result.Data.(map[string]string)["redirect"]
|
||||
if len(redirect) == 0 {
|
||||
t.Fatal("redirect url should be set")
|
||||
if len(ru.Query().Get("code")) == 0 {
|
||||
t.Fatal("authorization code not returned")
|
||||
}
|
||||
|
||||
ru, _ := url.Parse(redirect)
|
||||
if ru == nil {
|
||||
t.Fatal("redirect url unparseable")
|
||||
} else {
|
||||
if len(ru.Query().Get("code")) == 0 {
|
||||
t.Fatal("authorization code not returned")
|
||||
}
|
||||
if ru.Query().Get("state") != state {
|
||||
t.Fatal("returned state doesn't match")
|
||||
}
|
||||
if ru.Query().Get("state") != state {
|
||||
t.Fatal("returned state doesn't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "all", state); err == nil {
|
||||
t.Fatal("should have failed - no redirect_url given")
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "all", state); err == nil {
|
||||
t.Fatal("should have failed - no redirect_url given")
|
||||
}
|
||||
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "", state); err == nil {
|
||||
t.Fatal("should have failed - no redirect_url given")
|
||||
}
|
||||
|
||||
if result, err := Client.AllowOAuth("junk", app.Id, app.CallbackUrls[0], "all", state); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
redirect := result.Data.(map[string]string)["redirect"]
|
||||
if len(redirect) == 0 {
|
||||
t.Fatal("redirect url should be set")
|
||||
}
|
||||
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "", "", state); err == nil {
|
||||
t.Fatal("should have failed - no redirect_url given")
|
||||
}
|
||||
|
||||
if result, err := Client.AllowOAuth("junk", app.Id, app.CallbackUrls[0], "all", state); err != nil {
|
||||
t.Fatal(err)
|
||||
ru, _ := url.Parse(redirect)
|
||||
if ru == nil {
|
||||
t.Fatal("redirect url unparseable")
|
||||
} else {
|
||||
redirect := result.Data.(map[string]string)["redirect"]
|
||||
if len(redirect) == 0 {
|
||||
t.Fatal("redirect url should be set")
|
||||
if ru.Query().Get("error") != "unsupported_response_type" {
|
||||
t.Fatal("wrong error returned")
|
||||
}
|
||||
|
||||
ru, _ := url.Parse(redirect)
|
||||
if ru == nil {
|
||||
t.Fatal("redirect url unparseable")
|
||||
} else {
|
||||
if ru.Query().Get("error") != "unsupported_response_type" {
|
||||
t.Fatal("wrong error returned")
|
||||
}
|
||||
if ru.Query().Get("state") != state {
|
||||
t.Fatal("returned state doesn't match")
|
||||
}
|
||||
if ru.Query().Get("state") != state {
|
||||
t.Fatal("returned state doesn't match")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "", app.CallbackUrls[0], "all", state); err == nil {
|
||||
t.Fatal("should have failed - empty client id")
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "", app.CallbackUrls[0], "all", state); err == nil {
|
||||
t.Fatal("should have failed - empty client id")
|
||||
}
|
||||
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "junk", app.CallbackUrls[0], "all", state); err == nil {
|
||||
t.Fatal("should have failed - bad client id")
|
||||
}
|
||||
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "https://somewhereelse.com", "all", state); err == nil {
|
||||
t.Fatal("should have failed - redirect uri host does not match app host")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOAuthAppsByUser(t *testing.T) {
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
Client := th.BasicClient
|
||||
AdminClient := th.SystemAdminClient
|
||||
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
if _, err := Client.GetOAuthAppsByUser(); err == nil {
|
||||
t.Fatal("should have failed - oauth providing turned off")
|
||||
}
|
||||
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, "junk", app.CallbackUrls[0], "all", state); err == nil {
|
||||
t.Fatal("should have failed - bad client id")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := Client.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, "https://somewhereelse.com", "all", state); err == nil {
|
||||
t.Fatal("should have failed - redirect uri host does not match app host")
|
||||
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true
|
||||
|
||||
if _, err := Client.GetOAuthAppsByUser(); err == nil {
|
||||
t.Fatal("Should have failed. only admin is permitted")
|
||||
}
|
||||
|
||||
*utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false
|
||||
|
||||
if result, err := Client.GetOAuthAppsByUser(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
apps := result.Data.([]*model.OAuthApp)
|
||||
|
||||
if len(apps) != 0 {
|
||||
t.Fatal("incorrect number of apps should have been 0")
|
||||
}
|
||||
}
|
||||
|
||||
app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
app = Client.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp)
|
||||
|
||||
if result, err := Client.GetOAuthAppsByUser(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
apps := result.Data.([]*model.OAuthApp)
|
||||
|
||||
if len(apps) != 1 {
|
||||
t.Fatal("incorrect number of apps should have been 1")
|
||||
}
|
||||
}
|
||||
|
||||
app = &model.OAuthApp{Name: "TestApp4" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
app = AdminClient.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp)
|
||||
|
||||
if result, err := AdminClient.GetOAuthAppsByUser(); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
apps := result.Data.([]*model.OAuthApp)
|
||||
|
||||
if len(apps) != 4 {
|
||||
t.Fatal("incorrect number of apps should have been 4")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetOAuthAppInfo(t *testing.T) {
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
Client := th.BasicClient
|
||||
AdminClient := th.SystemAdminClient
|
||||
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
if _, err := Client.GetOAuthAppInfo("fakeId"); err == nil {
|
||||
t.Fatal("should have failed - oauth providing turned off")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true
|
||||
|
||||
app := &model.OAuthApp{Name: "TestApp5" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
|
||||
app = AdminClient.Must(AdminClient.RegisterApp(app)).Data.(*model.OAuthApp)
|
||||
|
||||
if _, err := Client.GetOAuthAppInfo(app.Id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuthDeleteApp(t *testing.T) {
|
||||
th := Setup().InitBasic().InitSystemAdmin()
|
||||
Client := th.BasicClient
|
||||
AdminClient := th.SystemAdminClient
|
||||
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
if _, err := Client.DeleteOAuthApp("fakeId"); err == nil {
|
||||
t.Fatal("should have failed - oauth providing turned off")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true
|
||||
*utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false
|
||||
|
||||
app := &model.OAuthApp{Name: "TestApp5" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
|
||||
app = Client.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp)
|
||||
|
||||
if _, err := Client.DeleteOAuthApp(app.Id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
app = &model.OAuthApp{Name: "TestApp5" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
|
||||
app = Client.Must(Client.RegisterApp(app)).Data.(*model.OAuthApp)
|
||||
|
||||
if _, err := AdminClient.DeleteOAuthApp(app.Id); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
15
api/user.go
15
api/user.go
@@ -2484,15 +2484,23 @@ func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
action := r.URL.Query().Get("action")
|
||||
redirectTo := r.URL.Query().Get("redirect_to")
|
||||
relayProps := map[string]string{}
|
||||
relayState := ""
|
||||
|
||||
if len(action) != 0 {
|
||||
relayProps := map[string]string{}
|
||||
relayProps["team_id"] = teamId
|
||||
relayProps["action"] = action
|
||||
if action == model.OAUTH_ACTION_EMAIL_TO_SSO {
|
||||
relayProps["email"] = r.URL.Query().Get("email")
|
||||
}
|
||||
}
|
||||
|
||||
if len(redirectTo) != 0 {
|
||||
relayProps["redirect_to"] = redirectTo
|
||||
}
|
||||
|
||||
if len(relayProps) > 0 {
|
||||
relayState = b64.StdEncoding.EncodeToString([]byte(model.MapToJson(relayProps)))
|
||||
}
|
||||
|
||||
@@ -2555,6 +2563,11 @@ func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
break
|
||||
}
|
||||
doLogin(c, w, r, user, "")
|
||||
|
||||
if val, ok := relayProps["redirect_to"]; ok {
|
||||
http.Redirect(w, r, c.GetSiteURL()+val, http.StatusFound)
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
214
i18n/en.json
214
i18n/en.json
@@ -889,7 +889,75 @@
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.allow_oauth.turn_off.app_error",
|
||||
"translation": "The system admin has turned off OAuth service providing."
|
||||
"translation": "The system admin has turned off OAuth2 Service Provider."
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.authorize_oauth.disabled.app_error",
|
||||
"translation": "The system admin has turned off OAuth2 Service Provider."
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.authorize_oauth.missing.app_error",
|
||||
"translation": "Missing one or more of response_type, client_id, or redirect_uri"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.delete.permissions.app_error",
|
||||
"translation": "Inappropriate permissions to delete the OAuth2 App"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.bad_client_id.app_error",
|
||||
"translation": "invalid_request: Bad client_id"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.bad_client_secret.app_error",
|
||||
"translation": "invalid_request: Missing client_secret"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.bad_grant.app_error",
|
||||
"translation": "invalid_request: Bad grant_type"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.credentials.app_error",
|
||||
"translation": "invalid_client: Invalid client credentials"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.disabled.app_error",
|
||||
"translation": "The system admin has turned off OAuth2 Service Provider."
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.expired_code.app_error",
|
||||
"translation": "invalid_grant: Invalid or expired authorization code"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.internal.app_error",
|
||||
"translation": "server_error: Encountered internal server error while accessing database"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.internal_saving.app_error",
|
||||
"translation": "server_error: Encountered internal server error while saving access token to database"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.internal_session.app_error",
|
||||
"translation": "server_error: Encountered internal server error while saving session to database"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.internal_user.app_error",
|
||||
"translation": "server_error: Encountered internal server error while pulling user from database"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.missing_code.app_error",
|
||||
"translation": "invalid_request: Missing code"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.missing_refresh_token.app_error",
|
||||
"translation": "invalid_request: Missing refresh_token"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.redirect_uri.app_error",
|
||||
"translation": "invalid_request: Supplied redirect_uri does not match authorization code redirect_uri"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_access_token.refresh_token.app_error",
|
||||
"translation": "invalid_grant: Invalid refresh token"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.get_auth_data.find.error",
|
||||
@@ -901,11 +969,7 @@
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.register_oauth_app.turn_off.app_error",
|
||||
"translation": "The system admin has turned off OAuth service providing."
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.revoke_access_token.del_code.app_error",
|
||||
"translation": "Error deleting authorization code from DB"
|
||||
"translation": "The system admin has turned off OAuth2 Service Provider."
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.revoke_access_token.del_session.app_error",
|
||||
@@ -919,6 +983,18 @@
|
||||
"id": "api.oauth.revoke_access_token.get.app_error",
|
||||
"translation": "Error getting access token from DB before deletion"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.singup_with_oauth.disabled.app_error",
|
||||
"translation": "User sign-up is disabled."
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.singup_with_oauth.expired_link.app_error",
|
||||
"translation": "The signup link has expired"
|
||||
},
|
||||
{
|
||||
"id": "api.oauth.singup_with_oauth.invalid_link.app_error",
|
||||
"translation": "The signup link does not appear to be valid"
|
||||
},
|
||||
{
|
||||
"id": "api.post.check_for_out_of_channel_mentions.message.multiple",
|
||||
"translation": "{{.Usernames}} and {{.LastUsername}} were mentioned, but they did not receive notifications because they do not belong to this channel."
|
||||
@@ -2440,8 +2516,8 @@
|
||||
"translation": "Invalid access token"
|
||||
},
|
||||
{
|
||||
"id": "model.access.is_valid.auth_code.app_error",
|
||||
"translation": "Invalid auth code"
|
||||
"id": "model.access.is_valid.client_id.app_error",
|
||||
"translation": "Invalid client id"
|
||||
},
|
||||
{
|
||||
"id": "model.access.is_valid.redirect_uri.app_error",
|
||||
@@ -2451,6 +2527,10 @@
|
||||
"id": "model.access.is_valid.refresh_token.app_error",
|
||||
"translation": "Invalid refresh token"
|
||||
},
|
||||
{
|
||||
"id": "model.access.is_valid.user_id.app_error",
|
||||
"translation": "Invalid user id"
|
||||
},
|
||||
{
|
||||
"id": "model.authorize.is_valid.auth_code.app_error",
|
||||
"translation": "Invalid authorization code"
|
||||
@@ -2901,7 +2981,7 @@
|
||||
},
|
||||
{
|
||||
"id": "model.oauth.is_valid.callback.app_error",
|
||||
"translation": "Invalid callback urls"
|
||||
"translation": "Callback URL must be a valid URL and start with http:// or https://."
|
||||
},
|
||||
{
|
||||
"id": "model.oauth.is_valid.client_secret.app_error",
|
||||
@@ -2921,7 +3001,11 @@
|
||||
},
|
||||
{
|
||||
"id": "model.oauth.is_valid.homepage.app_error",
|
||||
"translation": "Invalid homepage"
|
||||
"translation": "Homepage must be a valid URL and start with http:// or https://."
|
||||
},
|
||||
{
|
||||
"id": "model.oauth.is_valid.icon_url.app_error",
|
||||
"translation": "Icon URL must be a valid URL and start with http:// or https://."
|
||||
},
|
||||
{
|
||||
"id": "model.oauth.is_valid.name.app_error",
|
||||
@@ -3655,17 +3739,29 @@
|
||||
"id": "store.sql_license.save.app_error",
|
||||
"translation": "We encountered an error saving the license"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.delete.commit_transaction.app_error",
|
||||
"translation": "Unable to commit transaction"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.delete.open_transaction.app_error",
|
||||
"translation": "Unable to open transaction to delete the OAuth2 app"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.delete.rollback_transaction.app_error",
|
||||
"translation": "Unable to rollback transaction to delete the OAuth2 App"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.delete_app.app_error",
|
||||
"translation": "An error occurred while deleting the OAuth2 App"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.get_access_data.app_error",
|
||||
"translation": "We encountered an error finding the access token"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.get_access_data_by_code.app_error",
|
||||
"translation": "We encountered an error finding the access token"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.get_app.find.app_error",
|
||||
"translation": "We couldn't find the existing app"
|
||||
"translation": "We couldn't find the requested app"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.get_app.finding.app_error",
|
||||
@@ -3675,6 +3771,10 @@
|
||||
"id": "store.sql_oauth.get_app_by_user.find.app_error",
|
||||
"translation": "We couldn't find any existing apps"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.get_apps.find.app_error",
|
||||
"translation": "An error occurred while finding the OAuth2 Apps"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.get_auth_data.find.app_error",
|
||||
"translation": "We couldn't find the existing authorization code"
|
||||
@@ -3683,6 +3783,10 @@
|
||||
"id": "store.sql_oauth.get_auth_data.finding.app_error",
|
||||
"translation": "We encountered an error finding the authorization code"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.get_previous_access_data.app_error",
|
||||
"translation": "We encountered an error finding the access token"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.permanent_delete_auth_data_by_user.app_error",
|
||||
"translation": "We couldn't remove the authorization code"
|
||||
@@ -3711,6 +3815,10 @@
|
||||
"id": "store.sql_oauth.save_auth_data.app_error",
|
||||
"translation": "We couldn't save the authorization code."
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.update_access_data.app_error",
|
||||
"translation": "We encountered an error updating the access token"
|
||||
},
|
||||
{
|
||||
"id": "store.sql_oauth.update_app.find.app_error",
|
||||
"translation": "We couldn't find the existing app to update"
|
||||
@@ -4407,14 +4515,6 @@
|
||||
"id": "web.admin_console.title",
|
||||
"translation": "Admin Console"
|
||||
},
|
||||
{
|
||||
"id": "web.authorize_oauth.disabled.app_error",
|
||||
"translation": "The system admin has turned off OAuth service providing."
|
||||
},
|
||||
{
|
||||
"id": "web.authorize_oauth.missing.app_error",
|
||||
"translation": "Missing one or more of response_type, client_id, or redirect_uri"
|
||||
},
|
||||
{
|
||||
"id": "web.authorize_oauth.title",
|
||||
"translation": "Authorize Application"
|
||||
@@ -4459,62 +4559,6 @@
|
||||
"id": "web.find_team.title",
|
||||
"translation": "Find Team"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.bad_client_id.app_error",
|
||||
"translation": "invalid_request: Bad client_id"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.bad_client_secret.app_error",
|
||||
"translation": "invalid_request: Missing client_secret"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.bad_grant.app_error",
|
||||
"translation": "invalid_request: Bad grant_type"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.credentials.app_error",
|
||||
"translation": "invalid_client: Invalid client credentials"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.disabled.app_error",
|
||||
"translation": "The system admin has turned off OAuth service providing."
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.exchanged.app_error",
|
||||
"translation": "invalid_grant: Authorization code already exchanged for an access token"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.expired_code.app_error",
|
||||
"translation": "invalid_grant: Invalid or expired authorization code"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.internal.app_error",
|
||||
"translation": "server_error: Encountered internal server error while accessing database"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.internal_saving.app_error",
|
||||
"translation": "server_error: Encountered internal server error while saving access token to database"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.internal_session.app_error",
|
||||
"translation": "server_error: Encountered internal server error while saving session to database"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.internal_user.app_error",
|
||||
"translation": "server_error: Encountered internal server error while pulling user from database"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.missing_code.app_error",
|
||||
"translation": "invalid_request: Missing code"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.redirect_uri.app_error",
|
||||
"translation": "invalid_request: Supplied redirect_uri does not match authorization code redirect_uri"
|
||||
},
|
||||
{
|
||||
"id": "web.get_access_token.revoking.error",
|
||||
"translation": "Encountered an error revoking an access token, err="
|
||||
},
|
||||
{
|
||||
"id": "web.header.back",
|
||||
"translation": "Back"
|
||||
@@ -4627,18 +4671,6 @@
|
||||
"id": "web.signup_user_complete.title",
|
||||
"translation": "Complete User Sign Up"
|
||||
},
|
||||
{
|
||||
"id": "web.singup_with_oauth.disabled.app_error",
|
||||
"translation": "User sign-up is disabled."
|
||||
},
|
||||
{
|
||||
"id": "web.singup_with_oauth.expired_link.app_error",
|
||||
"translation": "The signup link has expired"
|
||||
},
|
||||
{
|
||||
"id": "web.singup_with_oauth.invalid_link.app_error",
|
||||
"translation": "The signup link does not appear to be valid"
|
||||
},
|
||||
{
|
||||
"id": "web.singup_with_oauth.invalid_team.app_error",
|
||||
"translation": "Invalid team name"
|
||||
|
||||
@@ -15,10 +15,12 @@ const (
|
||||
)
|
||||
|
||||
type AccessData struct {
|
||||
AuthCode string `json:"auth_code"`
|
||||
ClientId string `json:"client_id"`
|
||||
UserId string `json:"user_id"`
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
RedirectUri string `json:"redirect_uri"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
}
|
||||
|
||||
type AccessResponse struct {
|
||||
@@ -33,8 +35,12 @@ type AccessResponse struct {
|
||||
// correctly.
|
||||
func (ad *AccessData) IsValid() *AppError {
|
||||
|
||||
if len(ad.AuthCode) == 0 || len(ad.AuthCode) > 128 {
|
||||
return NewLocAppError("AccessData.IsValid", "model.access.is_valid.auth_code.app_error", nil, "")
|
||||
if len(ad.ClientId) == 0 || len(ad.ClientId) > 26 {
|
||||
return NewLocAppError("AccessData.IsValid", "model.access.is_valid.client_id.app_error", nil, "")
|
||||
}
|
||||
|
||||
if len(ad.UserId) == 0 || len(ad.UserId) > 26 {
|
||||
return NewLocAppError("AccessData.IsValid", "model.access.is_valid.user_id.app_error", nil, "")
|
||||
}
|
||||
|
||||
if len(ad.Token) != 26 {
|
||||
@@ -52,6 +58,19 @@ func (ad *AccessData) IsValid() *AppError {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (me *AccessData) IsExpired() bool {
|
||||
|
||||
if me.ExpiresAt <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
if GetMillis() > me.ExpiresAt {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (ad *AccessData) ToJson() string {
|
||||
b, err := json.Marshal(ad)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
|
||||
func TestAccessJson(t *testing.T) {
|
||||
a1 := AccessData{}
|
||||
a1.AuthCode = NewId()
|
||||
a1.ClientId = NewId()
|
||||
a1.UserId = NewId()
|
||||
a1.Token = NewId()
|
||||
a1.RefreshToken = NewId()
|
||||
|
||||
@@ -29,7 +30,12 @@ func TestAccessIsValid(t *testing.T) {
|
||||
t.Fatal("should have failed")
|
||||
}
|
||||
|
||||
ad.AuthCode = NewId()
|
||||
ad.ClientId = NewId()
|
||||
if err := ad.IsValid(); err == nil {
|
||||
t.Fatal("should have failed")
|
||||
}
|
||||
|
||||
ad.UserId = NewId()
|
||||
if err := ad.IsValid(); err == nil {
|
||||
t.Fatal("should have failed")
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
const (
|
||||
AUTHCODE_EXPIRE_TIME = 60 * 10 // 10 minutes
|
||||
AUTHCODE_RESPONSE_TYPE = "code"
|
||||
DEFAULT_SCOPE = "user"
|
||||
)
|
||||
|
||||
type AuthData struct {
|
||||
@@ -71,6 +72,10 @@ func (ad *AuthData) PreSave() {
|
||||
if ad.CreateAt == 0 {
|
||||
ad.CreateAt = GetMillis()
|
||||
}
|
||||
|
||||
if len(ad.Scope) == 0 {
|
||||
ad.Scope = DEFAULT_SCOPE
|
||||
}
|
||||
}
|
||||
|
||||
func (ad *AuthData) ToJson() string {
|
||||
|
||||
@@ -1446,6 +1446,8 @@ func (c *Client) GetTeamMembers(teamId string) (*Result, *AppError) {
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterApp creates a new OAuth2 app to be used with the OAuth2 Provider. On success
|
||||
// it returns the created app. Must be authenticated as a user.
|
||||
func (c *Client) RegisterApp(app *OAuthApp) (*Result, *AppError) {
|
||||
if r, err := c.DoApiPost("/oauth/register", app.ToJson()); err != nil {
|
||||
return nil, err
|
||||
@@ -1456,6 +1458,9 @@ func (c *Client) RegisterApp(app *OAuthApp) (*Result, *AppError) {
|
||||
}
|
||||
}
|
||||
|
||||
// AllowOAuth allows a new session by an OAuth2 App. On success
|
||||
// it returns the url to be redirected back to the app which initiated the oauth2 flow.
|
||||
// Must be authenticated as a user.
|
||||
func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/oauth/allow?response_type="+rspType+"&client_id="+clientId+"&redirect_uri="+url.QueryEscape(redirect)+"&scope="+scope+"&state="+url.QueryEscape(state), "", ""); err != nil {
|
||||
return nil, err
|
||||
@@ -1466,8 +1471,47 @@ func (c *Client) AllowOAuth(rspType, clientId, redirect, scope, state string) (*
|
||||
}
|
||||
}
|
||||
|
||||
// GetOAuthAppsByUser returns the OAuth2 Apps registered by the user. On success
|
||||
// it returns a list of OAuth2 Apps from the same user or all the registered apps if the user
|
||||
// is a System Administrator. Must be authenticated as a user.
|
||||
func (c *Client) GetOAuthAppsByUser() (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/oauth/list", "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
r.Header.Get(HEADER_ETAG_SERVER), OAuthAppListFromJson(r.Body)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// GetOAuthAppInfo lookup an OAuth2 App using the client_id. On success
|
||||
// it returns a Sanitized OAuth2 App. Must be authenticated as a user.
|
||||
func (c *Client) GetOAuthAppInfo(clientId string) (*Result, *AppError) {
|
||||
if r, err := c.DoApiGet("/oauth/app/"+clientId, "", ""); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
r.Header.Get(HEADER_ETAG_SERVER), OAuthAppFromJson(r.Body)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// DeleteOAuthApp deletes an OAuth2 app, the app must be deleted by the same user who created it or
|
||||
// a System Administrator. On success returs Status OK. Must be authenticated as a user.
|
||||
func (c *Client) DeleteOAuthApp(id string) (*Result, *AppError) {
|
||||
data := make(map[string]string)
|
||||
data["id"] = id
|
||||
if r, err := c.DoApiPost("/oauth/delete", MapToJson(data)); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
return &Result{r.Header.Get(HEADER_REQUEST_ID),
|
||||
r.Header.Get(HEADER_ETAG_SERVER), MapFromJson(r.Body)}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Client) GetAccessToken(data url.Values) (*Result, *AppError) {
|
||||
if r, err := c.DoApiPost("/oauth/access_token", data.Encode()); err != nil {
|
||||
if r, err := c.DoPost(API_URL_SUFFIX+"/oauth/access_token", data.Encode(), "application/x-www-form-urlencoded"); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
defer closeBody(r)
|
||||
|
||||
@@ -25,8 +25,10 @@ type OAuthApp struct {
|
||||
ClientSecret string `json:"client_secret"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
IconURL string `json:"icon_url"`
|
||||
CallbackUrls StringArray `json:"callback_urls"`
|
||||
Homepage string `json:"homepage"`
|
||||
IsTrusted bool `json:"is_trusted"`
|
||||
}
|
||||
|
||||
// IsValid validates the app and returns an error if it isn't configured
|
||||
@@ -61,7 +63,13 @@ func (a *OAuthApp) IsValid() *AppError {
|
||||
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "app_id="+a.Id)
|
||||
}
|
||||
|
||||
if len(a.Homepage) == 0 || len(a.Homepage) > 256 {
|
||||
for _, callback := range a.CallbackUrls {
|
||||
if !IsValidHttpUrl(callback) {
|
||||
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.callback.app_error", nil, "")
|
||||
}
|
||||
}
|
||||
|
||||
if len(a.Homepage) == 0 || len(a.Homepage) > 256 || !IsValidHttpUrl(a.Homepage) {
|
||||
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.homepage.app_error", nil, "app_id="+a.Id)
|
||||
}
|
||||
|
||||
@@ -69,6 +77,12 @@ func (a *OAuthApp) IsValid() *AppError {
|
||||
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.description.app_error", nil, "app_id="+a.Id)
|
||||
}
|
||||
|
||||
if len(a.IconURL) > 0 {
|
||||
if len(a.IconURL) > 512 || !IsValidHttpUrl(a.IconURL) {
|
||||
return NewLocAppError("OAuthApp.IsValid", "model.oauth.is_valid.icon_url.app_error", nil, "app_id="+a.Id)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -85,10 +99,6 @@ func (a *OAuthApp) PreSave() {
|
||||
|
||||
a.CreateAt = GetMillis()
|
||||
a.UpdateAt = a.CreateAt
|
||||
|
||||
if len(a.ClientSecret) > 0 {
|
||||
a.ClientSecret = HashPassword(a.ClientSecret)
|
||||
}
|
||||
}
|
||||
|
||||
// PreUpdate should be run before updating the app in the db.
|
||||
@@ -157,3 +167,23 @@ func OAuthAppMapFromJson(data io.Reader) map[string]*OAuthApp {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func OAuthAppListToJson(l []*OAuthApp) string {
|
||||
b, err := json.Marshal(l)
|
||||
if err != nil {
|
||||
return ""
|
||||
} else {
|
||||
return string(b)
|
||||
}
|
||||
}
|
||||
|
||||
func OAuthAppListFromJson(data io.Reader) []*OAuthApp {
|
||||
decoder := json.NewDecoder(data)
|
||||
var o []*OAuthApp
|
||||
err := decoder.Decode(&o)
|
||||
if err == nil {
|
||||
return o
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ func TestOAuthAppJson(t *testing.T) {
|
||||
a1.Name = "TestOAuthApp" + NewId()
|
||||
a1.CallbackUrls = []string{"https://nowhere.com"}
|
||||
a1.Homepage = "https://nowhere.com"
|
||||
a1.IconURL = "https://nowhere.com/icon_image.png"
|
||||
a1.ClientSecret = NewId()
|
||||
|
||||
json := a1.ToJson()
|
||||
@@ -30,6 +31,7 @@ func TestOAuthAppPreSave(t *testing.T) {
|
||||
a1.Name = "TestOAuthApp" + NewId()
|
||||
a1.CallbackUrls = []string{"https://nowhere.com"}
|
||||
a1.Homepage = "https://nowhere.com"
|
||||
a1.IconURL = "https://nowhere.com/icon_image.png"
|
||||
a1.ClientSecret = NewId()
|
||||
a1.PreSave()
|
||||
a1.Etag()
|
||||
@@ -42,6 +44,7 @@ func TestOAuthAppPreUpdate(t *testing.T) {
|
||||
a1.Name = "TestOAuthApp" + NewId()
|
||||
a1.CallbackUrls = []string{"https://nowhere.com"}
|
||||
a1.Homepage = "https://nowhere.com"
|
||||
a1.IconURL = "https://nowhere.com/icon_image.png"
|
||||
a1.ClientSecret = NewId()
|
||||
a1.PreUpdate()
|
||||
}
|
||||
@@ -92,4 +95,9 @@ func TestOAuthAppIsValid(t *testing.T) {
|
||||
if err := app.IsValid(); err != nil {
|
||||
t.Fatal()
|
||||
}
|
||||
|
||||
app.IconURL = "https://nowhere.com/icon_image.png"
|
||||
if err := app.IsValid(); err != nil {
|
||||
t.Fatal()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,9 @@ const (
|
||||
PREFERENCE_CATEGORY_THEME = "theme"
|
||||
// the name for theme props is the team id
|
||||
|
||||
PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP = "oauth_app"
|
||||
// the name for oauth_app is the client_id and value is the current scope
|
||||
|
||||
PREFERENCE_CATEGORY_LAST = "last"
|
||||
PREFERENCE_NAME_LAST_CHANNEL = "channel"
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"github.com/go-gorp/gorp"
|
||||
"github.com/mattermost/platform/model"
|
||||
"strings"
|
||||
)
|
||||
@@ -24,6 +25,7 @@ func NewSqlOAuthStore(sqlStore *SqlStore) OAuthStore {
|
||||
table.ColMap("Description").SetMaxSize(512)
|
||||
table.ColMap("CallbackUrls").SetMaxSize(1024)
|
||||
table.ColMap("Homepage").SetMaxSize(256)
|
||||
table.ColMap("IconURL").SetMaxSize(512)
|
||||
|
||||
tableAuth := db.AddTableWithName(model.AuthData{}, "OAuthAuthData").SetKeys(false, "Code")
|
||||
tableAuth.ColMap("UserId").SetMaxSize(26)
|
||||
@@ -34,21 +36,36 @@ func NewSqlOAuthStore(sqlStore *SqlStore) OAuthStore {
|
||||
tableAuth.ColMap("Scope").SetMaxSize(128)
|
||||
|
||||
tableAccess := db.AddTableWithName(model.AccessData{}, "OAuthAccessData").SetKeys(false, "Token")
|
||||
tableAccess.ColMap("AuthCode").SetMaxSize(128)
|
||||
tableAccess.ColMap("ClientId").SetMaxSize(26)
|
||||
tableAccess.ColMap("UserId").SetMaxSize(26)
|
||||
tableAccess.ColMap("Token").SetMaxSize(26)
|
||||
tableAccess.ColMap("RefreshToken").SetMaxSize(26)
|
||||
tableAccess.ColMap("RedirectUri").SetMaxSize(256)
|
||||
tableAccess.SetUniqueTogether("ClientId", "UserId")
|
||||
}
|
||||
|
||||
return as
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) UpgradeSchemaIfNeeded() {
|
||||
as.CreateColumnIfNotExists("OAuthApps", "IsTrusted", "tinyint(1)", "boolean", "0")
|
||||
as.CreateColumnIfNotExists("OAuthApps", "IconURL", "varchar(512)", "varchar(512)", "")
|
||||
as.CreateColumnIfNotExists("OAuthAccessData", "ClientId", "varchar(26)", "varchar(26)", "")
|
||||
as.CreateColumnIfNotExists("OAuthAccessData", "UserId", "varchar(26)", "varchar(26)", "")
|
||||
as.CreateColumnIfNotExists("OAuthAccessData", "ExpiresAt", "bigint", "bigint", "0")
|
||||
|
||||
// ADDED for 3.3 REMOVE for 3.7
|
||||
if as.DoesColumnExist("OAuthAccessData", "AuthCode") {
|
||||
as.RemoveIndexIfExists("idx_oauthaccessdata_auth_code", "OAuthAccessData")
|
||||
as.RemoveColumnIfExists("OAuthAccessData", "AuthCode")
|
||||
}
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) CreateIndexesIfNotExists() {
|
||||
as.CreateIndexIfNotExists("idx_oauthapps_creator_id", "OAuthApps", "CreatorId")
|
||||
as.CreateIndexIfNotExists("idx_oauthaccessdata_auth_code", "OAuthAccessData", "AuthCode")
|
||||
as.CreateIndexIfNotExists("idx_oauthaccessdata_client_id", "OAuthAccessData", "ClientId")
|
||||
as.CreateIndexIfNotExists("idx_oauthaccessdata_user_id", "OAuthAccessData", "UserId")
|
||||
as.CreateIndexIfNotExists("idx_oauthaccessdata_refresh_token", "OAuthAccessData", "RefreshToken")
|
||||
as.CreateIndexIfNotExists("idx_oauthauthdata_client_id", "OAuthAuthData", "Code")
|
||||
}
|
||||
|
||||
@@ -172,6 +189,62 @@ func (as SqlOAuthStore) GetAppByUser(userId string) StoreChannel {
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetApps() StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
var apps []*model.OAuthApp
|
||||
|
||||
if _, err := as.GetReplica().Select(&apps, "SELECT * FROM OAuthApps"); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.GetAppByUser", "store.sql_oauth.get_apps.find.app_error", nil, "err="+err.Error())
|
||||
}
|
||||
|
||||
result.Data = apps
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) DeleteApp(id string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
// wrap in a transaction so that if one fails, everything fails
|
||||
transaction, err := as.GetMaster().Begin()
|
||||
if err != nil {
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.DeleteApp", "store.sql_oauth.delete.open_transaction.app_error", nil, err.Error())
|
||||
} else {
|
||||
if extrasResult := as.deleteApp(transaction, id); extrasResult.Err != nil {
|
||||
result = extrasResult
|
||||
}
|
||||
|
||||
if result.Err == nil {
|
||||
if err := transaction.Commit(); err != nil {
|
||||
// don't need to rollback here since the transaction is already closed
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.DeleteApp", "store.sql_oauth.delete.commit_transaction.app_error", nil, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err := transaction.Rollback(); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.DeleteApp", "store.sql_oauth.delete.rollback_transaction.app_error", nil, err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) SaveAccessData(accessData *model.AccessData) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel)
|
||||
@@ -221,7 +294,7 @@ func (as SqlOAuthStore) GetAccessData(token string) StoreChannel {
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetAccessDataByAuthCode(authCode string) StoreChannel {
|
||||
func (as SqlOAuthStore) GetAccessDataByRefreshToken(token string) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
@@ -230,11 +303,35 @@ func (as SqlOAuthStore) GetAccessDataByAuthCode(authCode string) StoreChannel {
|
||||
|
||||
accessData := model.AccessData{}
|
||||
|
||||
if err := as.GetReplica().SelectOne(&accessData, "SELECT * FROM OAuthAccessData WHERE AuthCode = :AuthCode", map[string]interface{}{"AuthCode": authCode}); err != nil {
|
||||
if err := as.GetReplica().SelectOne(&accessData, "SELECT * FROM OAuthAccessData WHERE RefreshToken = :Token", map[string]interface{}{"Token": token}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.GetAccessData", "store.sql_oauth.get_access_data.app_error", nil, err.Error())
|
||||
} else {
|
||||
result.Data = &accessData
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) GetPreviousAccessData(userId, clientId string) StoreChannel {
|
||||
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
accessData := model.AccessData{}
|
||||
|
||||
if err := as.GetReplica().SelectOne(&accessData, "SELECT * FROM OAuthAccessData WHERE ClientId = :ClientId AND UserId = :UserId",
|
||||
map[string]interface{}{"ClientId": clientId, "UserId": userId}); err != nil {
|
||||
if strings.Contains(err.Error(), "no rows") {
|
||||
result.Data = nil
|
||||
} else {
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.GetAccessDataByAuthCode", "store.sql_oauth.get_access_data_by_code.app_error", nil, err.Error())
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.GetPreviousAccessData", "store.sql_oauth.get_previous_access_data.app_error", nil, err.Error())
|
||||
}
|
||||
} else {
|
||||
result.Data = &accessData
|
||||
@@ -248,6 +345,27 @@ func (as SqlOAuthStore) GetAccessDataByAuthCode(authCode string) StoreChannel {
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) UpdateAccessData(accessData *model.AccessData) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
if _, err := as.GetMaster().Exec("UPDATE OAuthAccessData SET Token = :Token, ExpiresAt = :ExpiresAt WHERE ClientId = :ClientId AND UserID = :UserId",
|
||||
map[string]interface{}{"Token": accessData.Token, "ExpiresAt": accessData.ExpiresAt, "ClientId": accessData.ClientId, "UserId": accessData.UserId}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.Update", "store.sql_oauth.update_access_data.app_error", nil,
|
||||
"clientId="+accessData.ClientId+",userId="+accessData.UserId+", "+err.Error())
|
||||
} else {
|
||||
result.Data = accessData
|
||||
}
|
||||
|
||||
storeChannel <- result
|
||||
close(storeChannel)
|
||||
}()
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) RemoveAccessData(token string) StoreChannel {
|
||||
storeChannel := make(StoreChannel)
|
||||
|
||||
@@ -339,7 +457,7 @@ func (as SqlOAuthStore) PermanentDeleteAuthDataByUser(userId string) StoreChanne
|
||||
go func() {
|
||||
result := StoreResult{}
|
||||
|
||||
_, err := as.GetMaster().Exec("DELETE FROM OAuthAuthData WHERE UserId = :UserId", map[string]interface{}{"UserId": userId})
|
||||
_, err := as.GetMaster().Exec("DELETE FROM OAuthAccessData WHERE UserId = :UserId", map[string]interface{}{"UserId": userId})
|
||||
if err != nil {
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.RemoveAuthDataByUserId", "store.sql_oauth.permanent_delete_auth_data_by_user.app_error", nil, "err="+err.Error())
|
||||
}
|
||||
@@ -350,3 +468,41 @@ func (as SqlOAuthStore) PermanentDeleteAuthDataByUser(userId string) StoreChanne
|
||||
|
||||
return storeChannel
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) deleteApp(transaction *gorp.Transaction, clientId string) StoreResult {
|
||||
result := StoreResult{}
|
||||
|
||||
if _, err := transaction.Exec("DELETE FROM OAuthApps WHERE Id = :Id", map[string]interface{}{"Id": clientId}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.DeleteApp", "store.sql_oauth.delete_app.app_error", nil, "id="+clientId+", err="+err.Error())
|
||||
return result
|
||||
}
|
||||
|
||||
return as.deleteOAuthTokens(transaction, clientId)
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) deleteOAuthTokens(transaction *gorp.Transaction, clientId string) StoreResult {
|
||||
result := StoreResult{}
|
||||
|
||||
if _, err := transaction.Exec("DELETE FROM OAuthAccessData WHERE ClientId = :Id", map[string]interface{}{"Id": clientId}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.DeleteApp", "store.sql_oauth.delete_app.app_error", nil, "id="+clientId+", err="+err.Error())
|
||||
return result
|
||||
}
|
||||
|
||||
return as.deleteAppExtras(transaction, clientId)
|
||||
}
|
||||
|
||||
func (as SqlOAuthStore) deleteAppExtras(transaction *gorp.Transaction, clientId string) StoreResult {
|
||||
result := StoreResult{}
|
||||
|
||||
if _, err := transaction.Exec(
|
||||
`DELETE FROM
|
||||
Preferences
|
||||
WHERE
|
||||
Category = :Category
|
||||
AND Name = :Name`, map[string]interface{}{"Category": model.PREFERENCE_CATEGORY_AUTHORIZED_OAUTH_APP, "Name": clientId}); err != nil {
|
||||
result.Err = model.NewLocAppError("SqlOAuthStore.DeleteApp", "store.sql_preference.delete.app_error", nil, err.Error())
|
||||
return result
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ func TestOAuthStoreGetApp(t *testing.T) {
|
||||
if err := (<-store.OAuth().GetAppByUser(a1.CreatorId)).Err; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := (<-store.OAuth().GetApps()).Err; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuthStoreUpdateApp(t *testing.T) {
|
||||
@@ -78,7 +82,8 @@ func TestOAuthStoreSaveAccessData(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
a1 := model.AccessData{}
|
||||
a1.AuthCode = model.NewId()
|
||||
a1.ClientId = model.NewId()
|
||||
a1.UserId = model.NewId()
|
||||
a1.Token = model.NewId()
|
||||
a1.RefreshToken = model.NewId()
|
||||
|
||||
@@ -91,9 +96,11 @@ func TestOAuthStoreGetAccessData(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
a1 := model.AccessData{}
|
||||
a1.AuthCode = model.NewId()
|
||||
a1.ClientId = model.NewId()
|
||||
a1.UserId = model.NewId()
|
||||
a1.Token = model.NewId()
|
||||
a1.RefreshToken = model.NewId()
|
||||
a1.ExpiresAt = model.GetMillis()
|
||||
Must(store.OAuth().SaveAccessData(&a1))
|
||||
|
||||
if result := <-store.OAuth().GetAccessData(a1.Token); result.Err != nil {
|
||||
@@ -105,11 +112,11 @@ func TestOAuthStoreGetAccessData(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if err := (<-store.OAuth().GetAccessDataByAuthCode(a1.AuthCode)).Err; err != nil {
|
||||
if err := (<-store.OAuth().GetPreviousAccessData(a1.UserId, a1.ClientId)).Err; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := (<-store.OAuth().GetAccessDataByAuthCode("junk")).Err; err != nil {
|
||||
if err := (<-store.OAuth().GetPreviousAccessData("user", "junk")).Err; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -118,7 +125,8 @@ func TestOAuthStoreRemoveAccessData(t *testing.T) {
|
||||
Setup()
|
||||
|
||||
a1 := model.AccessData{}
|
||||
a1.AuthCode = model.NewId()
|
||||
a1.ClientId = model.NewId()
|
||||
a1.UserId = model.NewId()
|
||||
a1.Token = model.NewId()
|
||||
a1.RefreshToken = model.NewId()
|
||||
Must(store.OAuth().SaveAccessData(&a1))
|
||||
@@ -127,8 +135,7 @@ func TestOAuthStoreRemoveAccessData(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if result := <-store.OAuth().GetAccessDataByAuthCode(a1.AuthCode); result.Err != nil {
|
||||
t.Fatal(result.Err)
|
||||
if result := (<-store.OAuth().GetPreviousAccessData(a1.UserId, a1.ClientId)); result.Err != nil {
|
||||
} else {
|
||||
if result.Data != nil {
|
||||
t.Fatal("did not delete access token")
|
||||
@@ -194,3 +201,16 @@ func TestOAuthStoreRemoveAuthDataByUser(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOAuthStoreDeleteApp(t *testing.T) {
|
||||
a1 := model.OAuthApp{}
|
||||
a1.CreatorId = model.NewId()
|
||||
a1.Name = "TestApp" + model.NewId()
|
||||
a1.CallbackUrls = []string{"https://nowhere.com"}
|
||||
a1.Homepage = "https://nowhere.com"
|
||||
Must(store.OAuth().SaveApp(&a1))
|
||||
|
||||
if err := (<-store.OAuth().DeleteApp(a1.Id)).Err; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,13 +186,17 @@ type OAuthStore interface {
|
||||
UpdateApp(app *model.OAuthApp) StoreChannel
|
||||
GetApp(id string) StoreChannel
|
||||
GetAppByUser(userId string) StoreChannel
|
||||
GetApps() StoreChannel
|
||||
DeleteApp(id string) StoreChannel
|
||||
SaveAuthData(authData *model.AuthData) StoreChannel
|
||||
GetAuthData(code string) StoreChannel
|
||||
RemoveAuthData(code string) StoreChannel
|
||||
PermanentDeleteAuthDataByUser(userId string) StoreChannel
|
||||
SaveAccessData(accessData *model.AccessData) StoreChannel
|
||||
UpdateAccessData(accessData *model.AccessData) StoreChannel
|
||||
GetAccessData(token string) StoreChannel
|
||||
GetAccessDataByAuthCode(authCode string) StoreChannel
|
||||
GetAccessDataByRefreshToken(token string) StoreChannel
|
||||
GetPreviousAccessData(userId, clientId string) StoreChannel
|
||||
RemoveAccessData(token string) StoreChannel
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
{{define "authorize"}}
|
||||
<html>
|
||||
{{template "head" . }}
|
||||
<body>
|
||||
<div id="authorize">
|
||||
</div>
|
||||
<script>
|
||||
window.setup_authorize_page({{.Props}});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
231
web/web_test.go
231
web/web_test.go
@@ -72,122 +72,125 @@ func TestGetAccessToken(t *testing.T) {
|
||||
|
||||
app := &model.OAuthApp{Name: "TestApp" + model.NewId(), Homepage: "https://nowhere.com", Description: "test", CallbackUrls: []string{"https://nowhere.com"}}
|
||||
|
||||
if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider {
|
||||
data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{app.CallbackUrls[0]}}
|
||||
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = false
|
||||
data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{"12345678901234567890123456"}, "client_secret": []string{"12345678901234567890123456"}, "code": []string{"junk"}, "redirect_uri": []string{app.CallbackUrls[0]}}
|
||||
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - oauth providing turned off")
|
||||
}
|
||||
} else {
|
||||
|
||||
ApiClient.Must(ApiClient.LoginById(ruser.Id, "passwd1"))
|
||||
ApiClient.SetTeamId(rteam.Data.(*model.Team).Id)
|
||||
app = ApiClient.Must(ApiClient.RegisterApp(app)).Data.(*model.OAuthApp)
|
||||
|
||||
redirect := ApiClient.Must(ApiClient.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", "123")).Data.(map[string]string)["redirect"]
|
||||
rurl, _ := url.Parse(redirect)
|
||||
|
||||
ApiClient.Logout()
|
||||
|
||||
data := url.Values{"grant_type": []string{"junk"}, "client_id": []string{app.Id}, "client_secret": []string{app.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{app.CallbackUrls[0]}}
|
||||
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - bad grant type")
|
||||
}
|
||||
|
||||
data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
|
||||
data.Set("client_id", "")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - missing client id")
|
||||
}
|
||||
data.Set("client_id", "junk")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - bad client id")
|
||||
}
|
||||
|
||||
data.Set("client_id", app.Id)
|
||||
data.Set("client_secret", "")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - missing client secret")
|
||||
}
|
||||
|
||||
data.Set("client_secret", "junk")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - bad client secret")
|
||||
}
|
||||
|
||||
data.Set("client_secret", app.ClientSecret)
|
||||
data.Set("code", "")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - missing code")
|
||||
}
|
||||
|
||||
data.Set("code", "junk")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - bad code")
|
||||
}
|
||||
|
||||
data.Set("code", rurl.Query().Get("code"))
|
||||
data.Set("redirect_uri", "junk")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - non-matching redirect uri")
|
||||
}
|
||||
|
||||
// reset data for successful request
|
||||
data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
|
||||
data.Set("client_id", app.Id)
|
||||
data.Set("client_secret", app.ClientSecret)
|
||||
data.Set("code", rurl.Query().Get("code"))
|
||||
data.Set("redirect_uri", app.CallbackUrls[0])
|
||||
|
||||
token := ""
|
||||
if result, err := ApiClient.GetAccessToken(data); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rsp := result.Data.(*model.AccessResponse)
|
||||
if len(rsp.AccessToken) == 0 {
|
||||
t.Fatal("access token not returned")
|
||||
} else {
|
||||
token = rsp.AccessToken
|
||||
}
|
||||
if rsp.TokenType != model.ACCESS_TOKEN_TYPE {
|
||||
t.Fatal("access token type incorrect")
|
||||
}
|
||||
}
|
||||
|
||||
if result, err := ApiClient.DoApiGet("/users/profiles?access_token="+token, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
userMap := model.UserMapFromJson(result.Body)
|
||||
if len(userMap) == 0 {
|
||||
t.Fatal("user map empty - did not get results correctly")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := ApiClient.DoApiGet("/users/profiles", "", ""); err == nil {
|
||||
t.Fatal("should have failed - no access token provided")
|
||||
}
|
||||
|
||||
if _, err := ApiClient.DoApiGet("/users/profiles?access_token=junk", "", ""); err == nil {
|
||||
t.Fatal("should have failed - bad access token provided")
|
||||
}
|
||||
|
||||
ApiClient.SetOAuthToken(token)
|
||||
if result, err := ApiClient.DoApiGet("/users/profiles", "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
userMap := model.UserMapFromJson(result.Body)
|
||||
if len(userMap) == 0 {
|
||||
t.Fatal("user map empty - did not get results correctly")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - tried to reuse auth code")
|
||||
}
|
||||
|
||||
ApiClient.ClearOAuthToken()
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - oauth providing turned off")
|
||||
}
|
||||
utils.Cfg.ServiceSettings.EnableOAuthServiceProvider = true
|
||||
|
||||
ApiClient.Must(ApiClient.LoginById(ruser.Id, "passwd1"))
|
||||
ApiClient.SetTeamId(rteam.Data.(*model.Team).Id)
|
||||
*utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = false
|
||||
app = ApiClient.Must(ApiClient.RegisterApp(app)).Data.(*model.OAuthApp)
|
||||
*utils.Cfg.ServiceSettings.EnableOnlyAdminIntegrations = true
|
||||
|
||||
redirect := ApiClient.Must(ApiClient.AllowOAuth(model.AUTHCODE_RESPONSE_TYPE, app.Id, app.CallbackUrls[0], "all", "123")).Data.(map[string]string)["redirect"]
|
||||
rurl, _ := url.Parse(redirect)
|
||||
|
||||
teamId := rteam.Data.(*model.Team).Id
|
||||
|
||||
ApiClient.Logout()
|
||||
|
||||
data = url.Values{"grant_type": []string{"junk"}, "client_id": []string{app.Id}, "client_secret": []string{app.ClientSecret}, "code": []string{rurl.Query().Get("code")}, "redirect_uri": []string{app.CallbackUrls[0]}}
|
||||
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - bad grant type")
|
||||
}
|
||||
|
||||
data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
|
||||
data.Set("client_id", "")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - missing client id")
|
||||
}
|
||||
data.Set("client_id", "junk")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - bad client id")
|
||||
}
|
||||
|
||||
data.Set("client_id", app.Id)
|
||||
data.Set("client_secret", "")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - missing client secret")
|
||||
}
|
||||
|
||||
data.Set("client_secret", "junk")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - bad client secret")
|
||||
}
|
||||
|
||||
data.Set("client_secret", app.ClientSecret)
|
||||
data.Set("code", "")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - missing code")
|
||||
}
|
||||
|
||||
data.Set("code", "junk")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - bad code")
|
||||
}
|
||||
|
||||
data.Set("code", rurl.Query().Get("code"))
|
||||
data.Set("redirect_uri", "junk")
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - non-matching redirect uri")
|
||||
}
|
||||
|
||||
// reset data for successful request
|
||||
data.Set("grant_type", model.ACCESS_TOKEN_GRANT_TYPE)
|
||||
data.Set("client_id", app.Id)
|
||||
data.Set("client_secret", app.ClientSecret)
|
||||
data.Set("code", rurl.Query().Get("code"))
|
||||
data.Set("redirect_uri", app.CallbackUrls[0])
|
||||
|
||||
token := ""
|
||||
if result, err := ApiClient.GetAccessToken(data); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
rsp := result.Data.(*model.AccessResponse)
|
||||
if len(rsp.AccessToken) == 0 {
|
||||
t.Fatal("access token not returned")
|
||||
} else {
|
||||
token = rsp.AccessToken
|
||||
}
|
||||
if rsp.TokenType != model.ACCESS_TOKEN_TYPE {
|
||||
t.Fatal("access token type incorrect")
|
||||
}
|
||||
}
|
||||
|
||||
if result, err := ApiClient.DoApiGet("/users/profiles/"+teamId+"?access_token="+token, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
userMap := model.UserMapFromJson(result.Body)
|
||||
if len(userMap) == 0 {
|
||||
t.Fatal("user map empty - did not get results correctly")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := ApiClient.DoApiGet("/users/profiles/"+teamId, "", ""); err == nil {
|
||||
t.Fatal("should have failed - no access token provided")
|
||||
}
|
||||
|
||||
if _, err := ApiClient.DoApiGet("/users/profiles/"+teamId+"?access_token=junk", "", ""); err == nil {
|
||||
t.Fatal("should have failed - bad access token provided")
|
||||
}
|
||||
|
||||
ApiClient.SetOAuthToken(token)
|
||||
if result, err := ApiClient.DoApiGet("/users/profiles/"+teamId, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
userMap := model.UserMapFromJson(result.Body)
|
||||
if len(userMap) == 0 {
|
||||
t.Fatal("user map empty - did not get results correctly")
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := ApiClient.GetAccessToken(data); err == nil {
|
||||
t.Fatal("should have failed - tried to reuse auth code")
|
||||
}
|
||||
|
||||
ApiClient.ClearOAuthToken()
|
||||
}
|
||||
|
||||
func TestIncomingWebhook(t *testing.T) {
|
||||
|
||||
@@ -308,13 +308,6 @@ export function showLeaveTeamModal() {
|
||||
});
|
||||
}
|
||||
|
||||
export function showRegisterAppModal() {
|
||||
AppDispatcher.handleViewAction({
|
||||
type: ActionTypes.TOGGLE_REGISTER_APP_MODAL,
|
||||
value: true
|
||||
});
|
||||
}
|
||||
|
||||
export function emitSuggestionPretextChanged(suggestionId, pretext) {
|
||||
AppDispatcher.handleViewAction({
|
||||
type: ActionTypes.SUGGESTION_PRETEXT_CHANGED,
|
||||
|
||||
60
webapp/actions/oauth_actions.jsx
Normal file
60
webapp/actions/oauth_actions.jsx
Normal file
@@ -0,0 +1,60 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import Client from 'client/web_client.jsx';
|
||||
import AppDispatcher from '../dispatcher/app_dispatcher.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
|
||||
export function listOAuthApps(userId, onSuccess, onError) {
|
||||
Client.listOAuthApps(
|
||||
(data) => {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_OAUTHAPPS,
|
||||
userId,
|
||||
oauthApps: data
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess(data);
|
||||
}
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
export function deleteOAuthApp(id, userId, onSuccess, onError) {
|
||||
Client.deleteOAuthApp(
|
||||
id,
|
||||
() => {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.REMOVED_OAUTHAPP,
|
||||
userId,
|
||||
id
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
|
||||
export function registerOAuthApp(app, onSuccess, onError) {
|
||||
Client.registerOAuthApp(
|
||||
app,
|
||||
(data) => {
|
||||
AppDispatcher.handleServerAction({
|
||||
type: ActionTypes.RECEIVED_OAUTHAPP,
|
||||
oauthApp: data
|
||||
});
|
||||
|
||||
if (onSuccess) {
|
||||
onSuccess();
|
||||
}
|
||||
},
|
||||
onError
|
||||
);
|
||||
}
|
||||
@@ -1498,6 +1498,36 @@ export default class Client {
|
||||
end(this.handleResponse.bind(this, 'allowOAuth2', success, error));
|
||||
}
|
||||
|
||||
listOAuthApps(success, error) {
|
||||
request.
|
||||
get(`${this.getOAuthRoute()}/list`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
send().
|
||||
end(this.handleResponse.bind(this, 'getOAuthApps', success, error));
|
||||
}
|
||||
|
||||
deleteOAuthApp(id, success, error) {
|
||||
request.
|
||||
post(`${this.getOAuthRoute()}/delete`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
send({id}).
|
||||
end(this.handleResponse.bind(this, 'deleteOAuthApp', success, error));
|
||||
}
|
||||
|
||||
getOAuthAppInfo(id, success, error) {
|
||||
request.
|
||||
get(`${this.getOAuthRoute()}/app/${id}`).
|
||||
set(this.defaultHeaders).
|
||||
type('application/json').
|
||||
accept('application/json').
|
||||
send().
|
||||
end(this.handleResponse.bind(this, 'getOAuthAppInfo', success, error));
|
||||
}
|
||||
|
||||
// Routes for Hooks
|
||||
|
||||
addIncomingHook(hook, success, error) {
|
||||
|
||||
@@ -8,7 +8,6 @@ import Client from 'client/web_client.jsx';
|
||||
|
||||
import FormError from 'components/form_error.jsx';
|
||||
import SaveButton from 'components/admin_console/save_button.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
export default class AdminSettings extends React.Component {
|
||||
static get propTypes() {
|
||||
@@ -22,7 +21,6 @@ export default class AdminSettings extends React.Component {
|
||||
|
||||
this.handleChange = this.handleChange.bind(this);
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.onKeyDown = this.onKeyDown.bind(this);
|
||||
|
||||
this.state = Object.assign(this.getStateFromConfig(props.config), {
|
||||
saveNeeded: false,
|
||||
@@ -38,20 +36,6 @@ export default class AdminSettings extends React.Component {
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
document.addEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
document.removeEventListener('keydown', this.onKeyDown);
|
||||
}
|
||||
|
||||
onKeyDown(e) {
|
||||
if (e.keyCode === Constants.KeyCodes.ENTER) {
|
||||
this.handleSubmit(e);
|
||||
}
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -118,6 +102,7 @@ export default class AdminSettings extends React.Component {
|
||||
<form
|
||||
className='form-horizontal'
|
||||
role='form'
|
||||
onSubmit={this.handleSubmit}
|
||||
>
|
||||
{this.renderSettings()}
|
||||
<div className='form-group'>
|
||||
|
||||
@@ -521,11 +521,11 @@ export default class AdminSidebar extends React.Component {
|
||||
}
|
||||
>
|
||||
<AdminSidebarSection
|
||||
name='webhooks'
|
||||
name='custom'
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='admin.sidebar.webhooks'
|
||||
defaultMessage='Webhooks and Commands'
|
||||
id='admin.sidebar.customIntegrations'
|
||||
defaultMessage='Custom Integrations'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -24,6 +24,7 @@ export default class WebhookSettings extends AdminSettings {
|
||||
config.ServiceSettings.EnableOnlyAdminIntegrations = this.state.enableOnlyAdminIntegrations;
|
||||
config.ServiceSettings.EnablePostUsernameOverride = this.state.enablePostUsernameOverride;
|
||||
config.ServiceSettings.EnablePostIconOverride = this.state.enablePostIconOverride;
|
||||
config.ServiceSettings.EnableOAuthServiceProvider = this.state.enableOAuthServiceProvider;
|
||||
|
||||
return config;
|
||||
}
|
||||
@@ -35,7 +36,8 @@ export default class WebhookSettings extends AdminSettings {
|
||||
enableCommands: config.ServiceSettings.EnableCommands,
|
||||
enableOnlyAdminIntegrations: config.ServiceSettings.EnableOnlyAdminIntegrations,
|
||||
enablePostUsernameOverride: config.ServiceSettings.EnablePostUsernameOverride,
|
||||
enablePostIconOverride: config.ServiceSettings.EnablePostIconOverride
|
||||
enablePostIconOverride: config.ServiceSettings.EnablePostIconOverride,
|
||||
enableOAuthServiceProvider: config.ServiceSettings.EnableOAuthServiceProvider
|
||||
};
|
||||
}
|
||||
|
||||
@@ -43,8 +45,8 @@ export default class WebhookSettings extends AdminSettings {
|
||||
return (
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='admin.integrations.webhook'
|
||||
defaultMessage='Webhooks and Commands'
|
||||
id='admin.integrations.custom'
|
||||
defaultMessage='Custom Integrations'
|
||||
/>
|
||||
</h3>
|
||||
);
|
||||
@@ -104,6 +106,23 @@ export default class WebhookSettings extends AdminSettings {
|
||||
value={this.state.enableCommands}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id='enableOAuthServiceProvider'
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='admin.oauth.providerTitle'
|
||||
defaultMessage='Enable OAuth 2.0 Service Provider: '
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='admin.oauth.providerDescription'
|
||||
defaultMessage='When true, Mattermost can act as an OAuth 2.0 service provider allowing external applications to authorize API requests to Mattermost.'
|
||||
/>
|
||||
}
|
||||
value={this.state.enableOAuthServiceProvider}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<BooleanSetting
|
||||
id='enableOnlyAdminIntegrations'
|
||||
label={
|
||||
@@ -10,6 +10,13 @@ import React from 'react';
|
||||
import icon50 from 'images/icon50x50.png';
|
||||
|
||||
export default class Authorize extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
location: React.PropTypes.object.isRequired,
|
||||
params: React.PropTypes.object.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
@@ -18,17 +25,31 @@ export default class Authorize extends React.Component {
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
handleAllow() {
|
||||
const responseType = this.props.responseType;
|
||||
const clientId = this.props.clientId;
|
||||
const redirectUri = this.props.redirectUri;
|
||||
const state = this.props.state;
|
||||
const scope = this.props.scope;
|
||||
|
||||
Client.allowOAuth2(responseType, clientId, redirectUri, state, scope,
|
||||
componentWillMount() {
|
||||
Client.getOAuthAppInfo(
|
||||
this.props.location.query.client_id,
|
||||
(app) => {
|
||||
this.setState({app});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// if we get to this point remove the antiClickjack blocker
|
||||
const blocker = document.getElementById('antiClickjack');
|
||||
if (blocker) {
|
||||
blocker.parentNode.removeChild(blocker);
|
||||
}
|
||||
}
|
||||
|
||||
handleAllow() {
|
||||
const params = this.props.location.query;
|
||||
|
||||
Client.allowOAuth2(params.response_type, params.client_id, params.redirect_uri, params.state, params.scope,
|
||||
(data) => {
|
||||
if (data.redirect) {
|
||||
window.location.replace(data.redirect);
|
||||
window.location.href = data.redirect;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
@@ -36,28 +57,42 @@ export default class Authorize extends React.Component {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
handleDeny() {
|
||||
window.location.replace(this.props.redirectUri + '?error=access_denied');
|
||||
window.location.replace(this.props.location.query.redirect_uri + '?error=access_denied');
|
||||
}
|
||||
|
||||
render() {
|
||||
const app = this.state.app;
|
||||
if (!app) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let icon;
|
||||
if (app.icon_url) {
|
||||
icon = app.icon_url;
|
||||
} else {
|
||||
icon = icon50;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='container-fluid'>
|
||||
<div className='prompt'>
|
||||
<div className='prompt__heading'>
|
||||
<div className='prompt__app-icon'>
|
||||
<img
|
||||
src={icon50}
|
||||
src={icon}
|
||||
width='50'
|
||||
height='50'
|
||||
alt=''
|
||||
/>
|
||||
</div>
|
||||
<div className='text'>
|
||||
<FormattedMessage
|
||||
<FormattedHTMLMessage
|
||||
id='authorize.title'
|
||||
defaultMessage='An application would like to connect to your {teamName} account'
|
||||
defaultMessage='<strong>{appName}</strong> would like to connect to your <strong>Mattermost</strong> user account'
|
||||
values={{
|
||||
teamName: this.props.teamName
|
||||
appName: app.name
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -67,7 +102,7 @@ export default class Authorize extends React.Component {
|
||||
id='authorize.app'
|
||||
defaultMessage='The app <strong>{appName}</strong> would like the ability to access and modify your basic information.'
|
||||
values={{
|
||||
appName: this.props.appName
|
||||
appName: app.name
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
@@ -76,14 +111,14 @@ export default class Authorize extends React.Component {
|
||||
id='authorize.access'
|
||||
defaultMessage='Allow <strong>{appName}</strong> access?'
|
||||
values={{
|
||||
appName: this.props.appName
|
||||
appName: app.name
|
||||
}}
|
||||
/>
|
||||
</h2>
|
||||
<div className='prompt__buttons'>
|
||||
<button
|
||||
type='submit'
|
||||
className='btn authorize-btn'
|
||||
className='btn btn-link authorize-btn'
|
||||
onClick={this.handleDeny}
|
||||
>
|
||||
<FormattedMessage
|
||||
@@ -107,13 +142,3 @@ export default class Authorize extends React.Component {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Authorize.propTypes = {
|
||||
appName: React.PropTypes.string,
|
||||
teamName: React.PropTypes.string,
|
||||
responseType: React.PropTypes.string,
|
||||
clientId: React.PropTypes.string,
|
||||
redirectUri: React.PropTypes.string,
|
||||
state: React.PropTypes.string,
|
||||
scope: React.PropTypes.string
|
||||
};
|
||||
|
||||
@@ -39,20 +39,22 @@ export default class BackstageSidebar extends React.Component {
|
||||
}
|
||||
|
||||
renderIntegrations() {
|
||||
if (window.mm_config.EnableIncomingWebhooks !== 'true' &&
|
||||
window.mm_config.EnableOutgoingWebhooks !== 'true' &&
|
||||
window.mm_config.EnableCommands !== 'true') {
|
||||
const config = window.mm_config;
|
||||
if (config.EnableIncomingWebhooks !== 'true' &&
|
||||
config.EnableOutgoingWebhooks !== 'true' &&
|
||||
config.EnableCommands !== 'true' &&
|
||||
config.EnableOAuthServiceProvider !== 'true') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (window.mm_config.EnableOnlyAdminIntegrations !== 'false' &&
|
||||
if (config.EnableOnlyAdminIntegrations !== 'false' &&
|
||||
!Utils.isSystemAdmin(this.props.user.roles) &&
|
||||
!TeamStore.isTeamAdmin(this.props.user.id, this.props.team.id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let incomingWebhooks = null;
|
||||
if (window.mm_config.EnableIncomingWebhooks === 'true') {
|
||||
if (config.EnableIncomingWebhooks === 'true') {
|
||||
incomingWebhooks = (
|
||||
<BackstageSection
|
||||
name='incoming_webhooks'
|
||||
@@ -67,7 +69,7 @@ export default class BackstageSidebar extends React.Component {
|
||||
}
|
||||
|
||||
let outgoingWebhooks = null;
|
||||
if (window.mm_config.EnableOutgoingWebhooks === 'true') {
|
||||
if (config.EnableOutgoingWebhooks === 'true') {
|
||||
outgoingWebhooks = (
|
||||
<BackstageSection
|
||||
name='outgoing_webhooks'
|
||||
@@ -82,7 +84,7 @@ export default class BackstageSidebar extends React.Component {
|
||||
}
|
||||
|
||||
let commands = null;
|
||||
if (window.mm_config.EnableCommands === 'true') {
|
||||
if (config.EnableCommands === 'true') {
|
||||
commands = (
|
||||
<BackstageSection
|
||||
name='commands'
|
||||
@@ -96,6 +98,21 @@ export default class BackstageSidebar extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
let oauthApps = null;
|
||||
if (config.EnableOAuthServiceProvider === 'true') {
|
||||
oauthApps = (
|
||||
<BackstageSection
|
||||
name='oauth2-apps'
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='backstage_sidebar.integrations.oauthApps'
|
||||
defaultMessage='OAuth 2.0 Applications'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<BackstageCategory
|
||||
name='integrations'
|
||||
@@ -111,6 +128,7 @@ export default class BackstageSidebar extends React.Component {
|
||||
{incomingWebhooks}
|
||||
{outgoingWebhooks}
|
||||
{commands}
|
||||
{oauthApps}
|
||||
</BackstageCategory>
|
||||
);
|
||||
}
|
||||
|
||||
435
webapp/components/integrations/components/add_oauth_app.jsx
Normal file
435
webapp/components/integrations/components/add_oauth_app.jsx
Normal file
@@ -0,0 +1,435 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as OAuthActions from 'actions/oauth_actions.jsx';
|
||||
|
||||
import BackstageHeader from 'components/backstage/components/backstage_header.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import FormError from 'components/form_error.jsx';
|
||||
import {browserHistory, Link} from 'react-router/es6';
|
||||
import SpinnerButton from 'components/spinner_button.jsx';
|
||||
|
||||
export default class AddOAuthApp extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
team: React.propTypes.object.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
|
||||
this.updateName = this.updateName.bind(this);
|
||||
this.updateTrusted = this.updateTrusted.bind(this);
|
||||
this.updateDescription = this.updateDescription.bind(this);
|
||||
this.updateHomepage = this.updateHomepage.bind(this);
|
||||
this.updateIconUrl = this.updateIconUrl.bind(this);
|
||||
this.updateCallbackUrls = this.updateCallbackUrls.bind(this);
|
||||
|
||||
this.imageLoaded = this.imageLoaded.bind(this);
|
||||
this.image = new Image();
|
||||
this.image.onload = this.imageLoaded;
|
||||
|
||||
this.state = {
|
||||
name: '',
|
||||
description: '',
|
||||
homepage: '',
|
||||
icon_url: '',
|
||||
callbackUrls: '',
|
||||
is_trusted: false,
|
||||
has_icon: false,
|
||||
saving: false,
|
||||
serverError: '',
|
||||
clientError: null
|
||||
};
|
||||
}
|
||||
|
||||
imageLoaded() {
|
||||
this.setState({
|
||||
has_icon: true,
|
||||
icon_url: this.refs.icon_url.value
|
||||
});
|
||||
}
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.state.saving) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
saving: true,
|
||||
serverError: '',
|
||||
clientError: ''
|
||||
});
|
||||
|
||||
if (!this.state.name) {
|
||||
this.setState({
|
||||
saving: false,
|
||||
clientError: (
|
||||
<FormattedMessage
|
||||
id='add_oauth_app.nameRequired'
|
||||
defaultMessage='Name for the OAuth 2.0 application is required.'
|
||||
/>
|
||||
)
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.description) {
|
||||
this.setState({
|
||||
saving: false,
|
||||
clientError: (
|
||||
<FormattedMessage
|
||||
id='add_oauth_app.descriptionRequired'
|
||||
defaultMessage='Description for the OAuth 2.0 application is required.'
|
||||
/>
|
||||
)
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.state.homepage) {
|
||||
this.setState({
|
||||
saving: false,
|
||||
clientError: (
|
||||
<FormattedMessage
|
||||
id='add_oauth_app.homepageRequired'
|
||||
defaultMessage='Homepage for the OAuth 2.0 application is required.'
|
||||
/>
|
||||
)
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const callbackUrls = [];
|
||||
for (let callbackUrl of this.state.callbackUrls.split('\n')) {
|
||||
callbackUrl = callbackUrl.trim();
|
||||
|
||||
if (callbackUrl.length > 0) {
|
||||
callbackUrls.push(callbackUrl);
|
||||
}
|
||||
}
|
||||
|
||||
if (callbackUrls.length === 0) {
|
||||
this.setState({
|
||||
saving: false,
|
||||
clientError: (
|
||||
<FormattedMessage
|
||||
id='add_oauth_app.callbackUrlsRequired'
|
||||
defaultMessage='One or more callback URLs are required.'
|
||||
/>
|
||||
)
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const app = {
|
||||
name: this.state.name,
|
||||
callback_urls: callbackUrls,
|
||||
homepage: this.state.homepage,
|
||||
description: this.state.description,
|
||||
is_trusted: this.state.is_trusted,
|
||||
icon_url: this.state.icon_url
|
||||
};
|
||||
|
||||
OAuthActions.registerOAuthApp(
|
||||
app,
|
||||
() => {
|
||||
browserHistory.push('/' + this.props.team.name + '/integrations/oauth2-apps');
|
||||
},
|
||||
(err) => {
|
||||
this.setState({
|
||||
saving: false,
|
||||
serverError: err.message
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateName(e) {
|
||||
this.setState({
|
||||
name: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
updateTrusted(e) {
|
||||
this.setState({
|
||||
is_trusted: e.target.value === 'true'
|
||||
});
|
||||
}
|
||||
|
||||
updateDescription(e) {
|
||||
this.setState({
|
||||
description: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
updateHomepage(e) {
|
||||
this.setState({
|
||||
homepage: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
updateIconUrl(e) {
|
||||
this.setState({
|
||||
has_icon: false,
|
||||
icon_url: ''
|
||||
});
|
||||
this.image.src = e.target.value;
|
||||
}
|
||||
|
||||
updateCallbackUrls(e) {
|
||||
this.setState({
|
||||
callbackUrls: e.target.value
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
let icon;
|
||||
if (this.state.has_icon) {
|
||||
icon = (
|
||||
<div className='integration__icon'>
|
||||
<img src={this.state.icon_url}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='backstage-content'>
|
||||
<BackstageHeader>
|
||||
<Link to={'/' + this.props.team.name + '/integrations/oauth2-apps'}>
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.header'
|
||||
defaultMessage='Installed OAuth2 Apps'
|
||||
/>
|
||||
</Link>
|
||||
<FormattedMessage
|
||||
id='add_oauth_app.header'
|
||||
defaultMessage='Add'
|
||||
/>
|
||||
</BackstageHeader>
|
||||
<div className='backstage-form'>
|
||||
{icon}
|
||||
<form className='form-horizontal'>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='is_trusted'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.trusted'
|
||||
defaultMessage='Is Trusted'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
value='true'
|
||||
name='is_trusted'
|
||||
checked={this.state.is_trusted}
|
||||
onChange={this.updateTrusted}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.trusted.yes'
|
||||
defaultMessage='Yes'
|
||||
/>
|
||||
</label>
|
||||
<label className='radio-inline'>
|
||||
<input
|
||||
type='radio'
|
||||
value='false'
|
||||
name='is_trusted'
|
||||
checked={!this.state.is_trusted}
|
||||
onChange={this.updateTrusted}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.trusted.no'
|
||||
defaultMessage='No'
|
||||
/>
|
||||
</label>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id='add_oauth_app.trusted.help'
|
||||
defaultMessage="When true, the OAuth 2.0 application is considered trusted by the Mattermost server and doesn't require the user to accept authorization. When false, an additional window will appear, asking the user to accept or deny the authorization."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='name'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.name'
|
||||
defaultMessage='Display Name'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<input
|
||||
id='name'
|
||||
type='text'
|
||||
maxLength='64'
|
||||
className='form-control'
|
||||
value={this.state.name}
|
||||
onChange={this.updateName}
|
||||
/>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id='add_oauth_app.name.help'
|
||||
defaultMessage='Display name for your OAuth 2.0 application made of up to 64 characters.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='description'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.description'
|
||||
defaultMessage='Description'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<input
|
||||
id='description'
|
||||
type='text'
|
||||
maxLength='512'
|
||||
className='form-control'
|
||||
value={this.state.description}
|
||||
onChange={this.updateDescription}
|
||||
/>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id='add_oauth_app.description.help'
|
||||
defaultMessage='Description for your OAuth 2.0 application.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='homepage'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.homepage'
|
||||
defaultMessage='Homepage'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<input
|
||||
id='homepage'
|
||||
type='url'
|
||||
maxLength='256'
|
||||
className='form-control'
|
||||
value={this.state.homepage}
|
||||
onChange={this.updateHomepage}
|
||||
/>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id='add_oauth_app.homepage.help'
|
||||
defaultMessage='The URL for the homepage of the OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='icon_url'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.iconUrl'
|
||||
defaultMessage='Icon URL'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<input
|
||||
id='icon_url'
|
||||
ref='icon_url'
|
||||
type='url'
|
||||
maxLength='512'
|
||||
className='form-control'
|
||||
onChange={this.updateIconUrl}
|
||||
/>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id='add_oauth_app.icon.help'
|
||||
defaultMessage='The URL for the homepage of the OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='form-group'>
|
||||
<label
|
||||
className='control-label col-sm-4'
|
||||
htmlFor='callbackUrls'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.callbackUrls'
|
||||
defaultMessage='Callback URLs (One Per Line)'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-md-5 col-sm-8'>
|
||||
<textarea
|
||||
id='callbackUrls'
|
||||
rows='3'
|
||||
maxLength='1024'
|
||||
className='form-control'
|
||||
value={this.state.callbackUrls}
|
||||
onChange={this.updateCallbackUrls}
|
||||
/>
|
||||
<div className='form__help'>
|
||||
<FormattedMessage
|
||||
id='add_oauth_app.callbackUrls.help'
|
||||
defaultMessage='The redirect URIs to which the service will redirect users after accepting or denying authorization of your application, and which will handle authorization codes or access tokens. Must be a valid URL and start with http:// or https://.'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='backstage-form__footer'>
|
||||
<FormError
|
||||
type='backstage'
|
||||
errors={[this.state.serverError, this.state.clientError]}
|
||||
/>
|
||||
<Link
|
||||
className='btn btn-sm'
|
||||
to={'/' + this.props.team.name + '/integrations/oauth2-apps'}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</Link>
|
||||
<SpinnerButton
|
||||
className='btn btn-primary'
|
||||
type='submit'
|
||||
spinning={this.state.saving}
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
</SpinnerButton>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
|
||||
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||
|
||||
const FAKE_SECRET = '***************';
|
||||
|
||||
export default class InstalledOAuthApp extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
oauthApp: React.PropTypes.object.isRequired,
|
||||
onDelete: React.PropTypes.func.isRequired,
|
||||
filter: React.PropTypes.string
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleShowClientSecret = this.handleShowClientSecret.bind(this);
|
||||
this.handleHideClientScret = this.handleHideClientScret.bind(this);
|
||||
this.handleDelete = this.handleDelete.bind(this);
|
||||
|
||||
this.matchesFilter = this.matchesFilter.bind(this);
|
||||
|
||||
this.state = {
|
||||
clientSecret: FAKE_SECRET
|
||||
};
|
||||
}
|
||||
|
||||
handleShowClientSecret(e) {
|
||||
e.preventDefault();
|
||||
this.setState({clientSecret: this.props.oauthApp.client_secret});
|
||||
}
|
||||
|
||||
handleHideClientScret(e) {
|
||||
e.preventDefault();
|
||||
this.setState({clientSecret: FAKE_SECRET});
|
||||
}
|
||||
|
||||
handleDelete(e) {
|
||||
e.preventDefault();
|
||||
|
||||
this.props.onDelete(this.props.oauthApp);
|
||||
}
|
||||
|
||||
matchesFilter(oauthApp, filter) {
|
||||
if (!filter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return oauthApp.name.toLowerCase().indexOf(filter) !== -1;
|
||||
}
|
||||
|
||||
render() {
|
||||
const oauthApp = this.props.oauthApp;
|
||||
|
||||
if (!this.matchesFilter(oauthApp, this.props.filter)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let name;
|
||||
if (oauthApp.name) {
|
||||
name = oauthApp.name;
|
||||
} else {
|
||||
name = (
|
||||
<FormattedMessage
|
||||
id='installed_integrations.unnamed_oauth_app'
|
||||
defaultMessage='Unnamed OAuth 2.0 Application'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
let description;
|
||||
if (oauthApp.description) {
|
||||
description = (
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__description'>
|
||||
{oauthApp.description}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const urls = (
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__url'>
|
||||
<FormattedMessage
|
||||
id='installed_integrations.callback_urls'
|
||||
defaultMessage='Callback URLs: {urls}'
|
||||
values={{
|
||||
urls: oauthApp.callback_urls.join(', ')
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
let isTrusted;
|
||||
if (oauthApp.is_trusted) {
|
||||
isTrusted = Utils.localizeMessage('installed_oauth_apps.trusted.yes', 'Yes');
|
||||
} else {
|
||||
isTrusted = Utils.localizeMessage('installed_oauth_apps.trusted.no', 'No');
|
||||
}
|
||||
|
||||
let action;
|
||||
if (this.state.clientSecret === FAKE_SECRET) {
|
||||
action = (
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.handleShowClientSecret}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_integrations.showSecret'
|
||||
defaultMessage='Show Secret'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
action = (
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.handleHideClientScret}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_integrations.hideSecret'
|
||||
defaultMessage='Hide Secret'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
let icon;
|
||||
if (oauthApp.icon_url) {
|
||||
icon = (
|
||||
<div className='integration__icon integration-list__icon'>
|
||||
<img src={oauthApp.icon_url}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='backstage-list__item'>
|
||||
{icon}
|
||||
<div className='item-details'>
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__name'>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
{description}
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__url'>
|
||||
<FormattedHTMLMessage
|
||||
id='installed_oauth_apps.is_trusted'
|
||||
defaultMessage='Is Trusted: <strong>{isTrusted}</strong>'
|
||||
values={{
|
||||
isTrusted
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__token'>
|
||||
<FormattedHTMLMessage
|
||||
id='installed_integrations.client_id'
|
||||
defaultMessage='Client ID: <strong>{clientId}</strong>'
|
||||
values={{
|
||||
clientId: oauthApp.id
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__token'>
|
||||
<FormattedHTMLMessage
|
||||
id='installed_integrations.client_secret'
|
||||
defaultMessage='Client Secret: <strong>{clientSecret}</strong>'
|
||||
values={{
|
||||
clientSecret: this.state.clientSecret
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
{urls}
|
||||
<div className='item-details__row'>
|
||||
<span className='item-details__creation'>
|
||||
<FormattedMessage
|
||||
id='installed_integrations.creation'
|
||||
defaultMessage='Created by {creator} on {createAt, date, full}'
|
||||
values={{
|
||||
creator: Utils.displayUsername(oauthApp.creator_id),
|
||||
createAt: oauthApp.create_at
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='item-actions'>
|
||||
{action}
|
||||
{' - '}
|
||||
<a
|
||||
href='#'
|
||||
onClick={this.handleDelete}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='installed_integrations.delete'
|
||||
defaultMessage='Delete'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import IntegrationStore from 'stores/integration_store.jsx';
|
||||
import * as OAuthActions from 'actions/oauth_actions.jsx';
|
||||
import {localizeMessage} from 'utils/utils.jsx';
|
||||
|
||||
import BackstageList from 'components/backstage/components/backstage_list.jsx';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import InstalledOAuthApp from './installed_oauth_app.jsx';
|
||||
|
||||
export default class InstalledOAuthApps extends React.Component {
|
||||
static get propTypes() {
|
||||
return {
|
||||
team: React.propTypes.object.isRequired
|
||||
};
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.handleIntegrationChange = this.handleIntegrationChange.bind(this);
|
||||
|
||||
this.deleteOAuthApp = this.deleteOAuthApp.bind(this);
|
||||
|
||||
const userId = UserStore.getCurrentId();
|
||||
|
||||
this.state = {
|
||||
oauthApps: IntegrationStore.getOAuthApps(userId),
|
||||
loading: !IntegrationStore.hasReceivedOAuthApps(userId)
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
IntegrationStore.addChangeListener(this.handleIntegrationChange);
|
||||
|
||||
if (window.mm_config.EnableOAuthServiceProvider === 'true') {
|
||||
OAuthActions.listOAuthApps(UserStore.getCurrentId());
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
IntegrationStore.removeChangeListener(this.handleIntegrationChange);
|
||||
}
|
||||
|
||||
handleIntegrationChange() {
|
||||
const userId = UserStore.getCurrentId();
|
||||
|
||||
this.setState({
|
||||
oauthApps: IntegrationStore.getOAuthApps(userId),
|
||||
loading: !IntegrationStore.hasReceivedOAuthApps(userId)
|
||||
});
|
||||
}
|
||||
|
||||
deleteOAuthApp(app) {
|
||||
const userId = UserStore.getCurrentId();
|
||||
OAuthActions.deleteOAuthApp(app.id, userId);
|
||||
}
|
||||
|
||||
render() {
|
||||
const oauthApps = this.state.oauthApps.map((app) => {
|
||||
return (
|
||||
<InstalledOAuthApp
|
||||
key={app.id}
|
||||
oauthApp={app}
|
||||
onDelete={this.deleteOAuthApp}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<BackstageList
|
||||
header={
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.header'
|
||||
defaultMessage='OAuth 2.0 Applications'
|
||||
/>
|
||||
}
|
||||
helpText={
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.help'
|
||||
defaultMessage='OAuth 2.0 Applications are available to everyone on your server.'
|
||||
/>
|
||||
}
|
||||
addText={
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.add'
|
||||
defaultMessage='Add OAuth 2.0 Application'
|
||||
/>
|
||||
}
|
||||
addLink={'/' + this.props.team.name + '/integrations/oauth2-apps/add'}
|
||||
emptyText={
|
||||
<FormattedMessage
|
||||
id='installed_oauth_apps.empty'
|
||||
defaultMessage='No OAuth 2.0 Applications found'
|
||||
/>
|
||||
}
|
||||
searchPlaceholder={localizeMessage('installed_oauth_apps.search', 'Search OAuth 2.0 Applications')}
|
||||
loading={this.state.loading}
|
||||
>
|
||||
{oauthApps}
|
||||
</BackstageList>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import {FormattedMessage} from 'react-intl';
|
||||
import IntegrationOption from './integration_option.jsx';
|
||||
|
||||
import WebhookIcon from 'images/webhook_icon.jpg';
|
||||
import AppIcon from 'images/oauth_icon.png';
|
||||
|
||||
export default class Integrations extends React.Component {
|
||||
static get propTypes() {
|
||||
@@ -17,8 +18,9 @@ export default class Integrations extends React.Component {
|
||||
|
||||
render() {
|
||||
const options = [];
|
||||
const config = window.mm_config;
|
||||
|
||||
if (window.mm_config.EnableIncomingWebhooks === 'true') {
|
||||
if (config.EnableIncomingWebhooks === 'true') {
|
||||
options.push(
|
||||
<IntegrationOption
|
||||
key='incomingWebhook'
|
||||
@@ -40,7 +42,7 @@ export default class Integrations extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (window.mm_config.EnableOutgoingWebhooks === 'true') {
|
||||
if (config.EnableOutgoingWebhooks === 'true') {
|
||||
options.push(
|
||||
<IntegrationOption
|
||||
key='outgoingWebhook'
|
||||
@@ -62,7 +64,7 @@ export default class Integrations extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (window.mm_config.EnableCommands === 'true') {
|
||||
if (config.EnableCommands === 'true') {
|
||||
options.push(
|
||||
<IntegrationOption
|
||||
key='command'
|
||||
@@ -84,6 +86,28 @@ export default class Integrations extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
if (config.EnableOAuthServiceProvider === 'true') {
|
||||
options.push(
|
||||
<IntegrationOption
|
||||
key='oauth2Apps'
|
||||
image={AppIcon}
|
||||
title={
|
||||
<FormattedMessage
|
||||
id='integrations.oauthApps.title'
|
||||
defaultMessage='OAuth 2.0 Applications'
|
||||
/>
|
||||
}
|
||||
description={
|
||||
<FormattedMessage
|
||||
id='integrations.oauthApps.description'
|
||||
defaultMessage='Auth 2.0 allows external applications to make authorized requests to the Mattermost API.'
|
||||
/>
|
||||
}
|
||||
link={'/' + this.props.team.name + '/integrations/oauth2-apps'}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='backstage-content row'>
|
||||
<div className='backstage-header'>
|
||||
|
||||
@@ -146,11 +146,12 @@ export default class LoginController extends React.Component {
|
||||
token,
|
||||
() => {
|
||||
// check for query params brought over from signup_user_complete
|
||||
if (this.props.location.query.id || this.props.location.query.h) {
|
||||
const query = this.props.location.query;
|
||||
if (query.id || query.h) {
|
||||
Client.addUserToTeamFromInvite(
|
||||
this.props.location.query.d,
|
||||
this.props.location.query.h,
|
||||
this.props.location.query.id,
|
||||
query.d,
|
||||
query.h,
|
||||
query.id,
|
||||
() => {
|
||||
this.finishSignin();
|
||||
},
|
||||
@@ -200,8 +201,13 @@ export default class LoginController extends React.Component {
|
||||
finishSignin() {
|
||||
GlobalActions.emitInitialLoad(
|
||||
() => {
|
||||
const query = this.props.location.query;
|
||||
GlobalActions.loadDefaultLocale();
|
||||
browserHistory.push('/select_team');
|
||||
if (query.redirect_to) {
|
||||
browserHistory.push(query.redirect_to);
|
||||
} else {
|
||||
browserHistory.push('/select_team');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -401,7 +407,7 @@ export default class LoginController extends React.Component {
|
||||
defaultMessage="Don't have an account? "
|
||||
/>
|
||||
<Link
|
||||
to={'/signup_user_complete'}
|
||||
to={'/signup_user_complete' + this.props.location.search}
|
||||
className='signup-team-login'
|
||||
>
|
||||
<FormattedMessage
|
||||
|
||||
@@ -99,6 +99,7 @@ export default class NavbarDropdown extends React.Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
const config = global.window.mm_config;
|
||||
var teamLink = '';
|
||||
var inviteLink = '';
|
||||
var manageLink = '';
|
||||
@@ -131,7 +132,7 @@ export default class NavbarDropdown extends React.Component {
|
||||
</li>
|
||||
);
|
||||
|
||||
if (this.props.teamType === Constants.OPEN_TEAM && global.window.mm_config.EnableUserCreation === 'true') {
|
||||
if (this.props.teamType === Constants.OPEN_TEAM && config.EnableUserCreation === 'true') {
|
||||
teamLink = (
|
||||
<li>
|
||||
<a
|
||||
@@ -148,10 +149,10 @@ export default class NavbarDropdown extends React.Component {
|
||||
}
|
||||
|
||||
if (global.window.mm_license.IsLicensed === 'true') {
|
||||
if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) {
|
||||
if (config.RestrictTeamInvite === Constants.PERMISSIONS_SYSTEM_ADMIN && !isSystemAdmin) {
|
||||
teamLink = null;
|
||||
inviteLink = null;
|
||||
} else if (global.window.mm_config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) {
|
||||
} else if (config.RestrictTeamInvite === Constants.PERMISSIONS_TEAM_ADMIN && !isAdmin) {
|
||||
teamLink = null;
|
||||
inviteLink = null;
|
||||
}
|
||||
@@ -201,10 +202,11 @@ export default class NavbarDropdown extends React.Component {
|
||||
);
|
||||
|
||||
const integrationsEnabled =
|
||||
window.mm_config.EnableIncomingWebhooks === 'true' ||
|
||||
window.mm_config.EnableOutgoingWebhooks === 'true' ||
|
||||
window.mm_config.EnableCommands === 'true';
|
||||
if (integrationsEnabled && (isAdmin || window.mm_config.EnableOnlyAdminIntegrations !== 'true')) {
|
||||
config.EnableIncomingWebhooks === 'true' ||
|
||||
config.EnableOutgoingWebhooks === 'true' ||
|
||||
config.EnableCommands === 'true' ||
|
||||
config.EnableOAuthServiceProvider === 'true';
|
||||
if (integrationsEnabled && (isAdmin || config.EnableOnlyAdminIntegrations !== 'true')) {
|
||||
integrationsLink = (
|
||||
<li>
|
||||
<Link to={'/' + this.props.teamName + '/integrations'}>
|
||||
@@ -234,7 +236,7 @@ export default class NavbarDropdown extends React.Component {
|
||||
|
||||
var teams = [];
|
||||
|
||||
if (global.window.mm_config.EnableTeamCreation === 'true') {
|
||||
if (config.EnableTeamCreation === 'true') {
|
||||
teams.push(
|
||||
<li key='newTeam_li'>
|
||||
<Link
|
||||
@@ -297,13 +299,13 @@ export default class NavbarDropdown extends React.Component {
|
||||
}
|
||||
|
||||
let helpLink = null;
|
||||
if (global.window.mm_config.HelpLink) {
|
||||
if (config.HelpLink) {
|
||||
helpLink = (
|
||||
<li>
|
||||
<Link
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
to={global.window.mm_config.HelpLink}
|
||||
to={config.HelpLink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.help'
|
||||
@@ -315,13 +317,13 @@ export default class NavbarDropdown extends React.Component {
|
||||
}
|
||||
|
||||
let reportLink = null;
|
||||
if (global.window.mm_config.ReportAProblemLink) {
|
||||
if (config.ReportAProblemLink) {
|
||||
reportLink = (
|
||||
<li>
|
||||
<Link
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
to={global.window.mm_config.ReportAProblemLink}
|
||||
to={config.ReportAProblemLink}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='navbar_dropdown.report'
|
||||
|
||||
@@ -31,7 +31,6 @@ import DeletePostModal from 'components/delete_post_modal.jsx';
|
||||
import MoreChannelsModal from 'components/more_channels.jsx';
|
||||
import TeamSettingsModal from 'components/team_settings_modal.jsx';
|
||||
import RemovedFromChannelModal from 'components/removed_from_channel_modal.jsx';
|
||||
import RegisterAppModal from 'components/register_app_modal.jsx';
|
||||
import ImportThemeModal from 'components/user_settings/import_theme_modal.jsx';
|
||||
import InviteMemberModal from 'components/invite_member_modal.jsx';
|
||||
import LeaveTeamModal from 'components/leave_team_modal.jsx';
|
||||
@@ -162,7 +161,6 @@ export default class NeedsTeam extends React.Component {
|
||||
<EditPostModal/>
|
||||
<DeletePostModal/>
|
||||
<RemovedFromChannelModal/>
|
||||
<RegisterAppModal/>
|
||||
<SelectTeamModal/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,411 +0,0 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import Client from 'client/web_client.jsx';
|
||||
import ModalStore from 'stores/modal_store.jsx';
|
||||
|
||||
import {Modal} from 'react-bootstrap';
|
||||
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const ActionTypes = Constants.ActionTypes;
|
||||
|
||||
const holders = defineMessages({
|
||||
required: {
|
||||
id: 'register_app.required',
|
||||
defaultMessage: 'Required'
|
||||
},
|
||||
optional: {
|
||||
id: 'register_app.optional',
|
||||
defaultMessage: 'Optional'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class RegisterAppModal extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.handleSubmit = this.handleSubmit.bind(this);
|
||||
this.onHide = this.onHide.bind(this);
|
||||
this.save = this.save.bind(this);
|
||||
this.updateShow = this.updateShow.bind(this);
|
||||
|
||||
this.state = {
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
saved: false,
|
||||
show: false
|
||||
};
|
||||
}
|
||||
componentDidMount() {
|
||||
ModalStore.addModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow);
|
||||
}
|
||||
componentWillUnmount() {
|
||||
ModalStore.removeModalListener(ActionTypes.TOGGLE_REGISTER_APP_MODAL, this.updateShow);
|
||||
}
|
||||
updateShow(show) {
|
||||
if (!show) {
|
||||
if (this.state.clientId !== '' && !this.state.saved) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
saved: false,
|
||||
homepageError: null,
|
||||
callbackError: null,
|
||||
serverError: null,
|
||||
nameError: null
|
||||
});
|
||||
}
|
||||
|
||||
this.setState({show});
|
||||
}
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var state = this.state;
|
||||
state.serverError = null;
|
||||
|
||||
var app = {};
|
||||
|
||||
var name = this.refs.name.value;
|
||||
if (!name || name.length === 0) {
|
||||
state.nameError = true;
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
state.nameError = null;
|
||||
app.name = name;
|
||||
|
||||
var homepage = this.refs.homepage.value;
|
||||
if (!homepage || homepage.length === 0) {
|
||||
state.homepageError = true;
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
state.homepageError = null;
|
||||
app.homepage = homepage;
|
||||
|
||||
var desc = this.refs.desc.value;
|
||||
app.description = desc;
|
||||
|
||||
var rawCallbacks = this.refs.callback.value.trim();
|
||||
if (!rawCallbacks || rawCallbacks.length === 0) {
|
||||
state.callbackError = true;
|
||||
this.setState(state);
|
||||
return;
|
||||
}
|
||||
state.callbackError = null;
|
||||
app.callback_urls = rawCallbacks.split('\n');
|
||||
|
||||
Client.registerOAuthApp(app,
|
||||
(data) => {
|
||||
state.clientId = data.id;
|
||||
state.clientSecret = data.client_secret;
|
||||
this.setState(state);
|
||||
},
|
||||
(err) => {
|
||||
state.serverError = err.message;
|
||||
this.setState(state);
|
||||
}
|
||||
);
|
||||
}
|
||||
onHide(e) {
|
||||
if (!this.state.saved && this.state.clientId !== '') {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState({clientId: '', clientSecret: '', saved: false});
|
||||
}
|
||||
save() {
|
||||
this.setState({saved: this.refs.save.checked});
|
||||
}
|
||||
render() {
|
||||
const {formatMessage} = this.props.intl;
|
||||
var nameError;
|
||||
if (this.state.nameError) {
|
||||
nameError = (
|
||||
<div className='form-group has-error'>
|
||||
<label className='control-label'>
|
||||
<FormattedMessage
|
||||
id='register_app.nameError'
|
||||
defaultMessage='Application name must be filled in.'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
var homepageError;
|
||||
if (this.state.homepageError) {
|
||||
homepageError = (
|
||||
<div className='form-group has-error'>
|
||||
<label className='control-label'>
|
||||
<FormattedMessage
|
||||
id='register_app.homepageError'
|
||||
defaultMessage='Homepage must be filled in.'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
var callbackError;
|
||||
if (this.state.callbackError) {
|
||||
callbackError = (
|
||||
<div className='form-group has-error'>
|
||||
<label className='control-label'>
|
||||
<FormattedMessage
|
||||
id='register_app.callbackError'
|
||||
defaultMessage='At least one callback URL must be filled in.'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
var serverError;
|
||||
if (this.state.serverError) {
|
||||
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||
}
|
||||
|
||||
var body = '';
|
||||
var footer = '';
|
||||
if (this.state.clientId === '') {
|
||||
body = (
|
||||
<div className='settings-modal'>
|
||||
<div className='form-horizontal user-settings'>
|
||||
<h4 className='padding-bottom x3'>
|
||||
<FormattedMessage
|
||||
id='register_app.title'
|
||||
defaultMessage='Register a New Application'
|
||||
/>
|
||||
</h4>
|
||||
<div className='row'>
|
||||
<label className='col-sm-4 control-label'>
|
||||
<FormattedMessage
|
||||
id='register_app.name'
|
||||
defaultMessage='Application Name'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-7'>
|
||||
<input
|
||||
ref='name'
|
||||
className='form-control'
|
||||
type='text'
|
||||
placeholder={formatMessage(holders.required)}
|
||||
/>
|
||||
{nameError}
|
||||
</div>
|
||||
</div>
|
||||
<div className='row padding-top x2'>
|
||||
<label className='col-sm-4 control-label'>
|
||||
<FormattedMessage
|
||||
id='register_app.homepage'
|
||||
defaultMessage='Homepage URL'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-7'>
|
||||
<input
|
||||
ref='homepage'
|
||||
className='form-control'
|
||||
type='text'
|
||||
placeholder={formatMessage(holders.required)}
|
||||
/>
|
||||
{homepageError}
|
||||
</div>
|
||||
</div>
|
||||
<div className='row padding-top x2'>
|
||||
<label className='col-sm-4 control-label'>
|
||||
<FormattedMessage
|
||||
id='register_app.description'
|
||||
defaultMessage='Description'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-7'>
|
||||
<input
|
||||
ref='desc'
|
||||
className='form-control'
|
||||
type='text'
|
||||
placeholder={formatMessage(holders.optional)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='row padding-top padding-bottom x2'>
|
||||
<label className='col-sm-4 control-label'>
|
||||
<FormattedMessage
|
||||
id='register_app.callback'
|
||||
defaultMessage='Callback URL'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-7'>
|
||||
<textarea
|
||||
ref='callback'
|
||||
className='form-control'
|
||||
type='text'
|
||||
placeholder={formatMessage(holders.required)}
|
||||
rows='5'
|
||||
/>
|
||||
{callbackError}
|
||||
</div>
|
||||
</div>
|
||||
{serverError}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
footer = (
|
||||
<div>
|
||||
<button
|
||||
type='button'
|
||||
className='btn btn-default'
|
||||
onClick={() => this.updateShow(false)}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='register_app.cancel'
|
||||
defaultMessage='Cancel'
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
onClick={this.handleSubmit}
|
||||
type='submit'
|
||||
className='btn btn-primary'
|
||||
tabIndex='3'
|
||||
>
|
||||
<FormattedMessage
|
||||
id='register_app.register'
|
||||
defaultMessage='Register'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
var btnClass = ' disabled';
|
||||
if (this.state.saved) {
|
||||
btnClass = '';
|
||||
}
|
||||
|
||||
body = (
|
||||
<div className='form-horizontal user-settings'>
|
||||
<h4 className='padding-bottom x3'>
|
||||
<FormattedMessage
|
||||
id='register_app.credentialsTitle'
|
||||
defaultMessage='Your Application Credentials'
|
||||
/>
|
||||
</h4>
|
||||
<br/>
|
||||
<div className='row'>
|
||||
<label className='col-sm-4 control-label'>
|
||||
<FormattedMessage
|
||||
id='register_app.clientId'
|
||||
defaultMessage='Client ID'
|
||||
/>
|
||||
</label>
|
||||
<div className='col-sm-7'>
|
||||
<input
|
||||
className='form-control'
|
||||
type='text'
|
||||
value={this.state.clientId}
|
||||
readOnly='true'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<div className='row padding-top x2'>
|
||||
<label className='col-sm-4 control-label'>
|
||||
<FormattedMessage
|
||||
id='register_app.clientSecret'
|
||||
defaultMessage='Client Secret'
|
||||
/></label>
|
||||
<div className='col-sm-7'>
|
||||
<input
|
||||
className='form-control'
|
||||
type='text'
|
||||
value={this.state.clientSecret}
|
||||
readOnly='true'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
<br/>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='register_app.credentialsDescription'
|
||||
defaultMessage="Save these somewhere SAFE and SECURE. Treat your Client ID as your app's username and your Client Secret as the app's password."
|
||||
/>
|
||||
</strong>
|
||||
<br/>
|
||||
<br/>
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
ref='save'
|
||||
type='checkbox'
|
||||
checked={this.state.saved}
|
||||
onChange={this.save}
|
||||
/>
|
||||
<FormattedMessage
|
||||
id='register_app.credentialsSave'
|
||||
defaultMessage='I have saved both my Client Id and Client Secret somewhere safe'
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
footer = (
|
||||
<a
|
||||
className={'btn btn-sm btn-primary pull-right' + btnClass}
|
||||
href='#'
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
this.updateShow(false);
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='register_app.close'
|
||||
defaultMessage='Close'
|
||||
/>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span>
|
||||
<Modal
|
||||
show={this.state.show}
|
||||
onHide={() => this.updateShow(false)}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>
|
||||
<FormattedMessage
|
||||
id='register_app.dev'
|
||||
defaultMessage='Developer Applications'
|
||||
/>
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<form
|
||||
role='form'
|
||||
className='form-horizontal'
|
||||
>
|
||||
<Modal.Body>
|
||||
{body}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
{footer}
|
||||
</Modal.Footer>
|
||||
</form>
|
||||
</Modal>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
RegisterAppModal.propTypes = {
|
||||
intl: intlShape.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(RegisterAppModal);
|
||||
@@ -231,8 +231,13 @@ export default class SignupUserComplete extends React.Component {
|
||||
finishSignup() {
|
||||
GlobalActions.emitInitialLoad(
|
||||
() => {
|
||||
const query = this.props.location.query;
|
||||
GlobalActions.loadDefaultLocale();
|
||||
browserHistory.push('/select_team');
|
||||
if (query.redirect_to) {
|
||||
browserHistory.push(query.redirect_to);
|
||||
} else {
|
||||
browserHistory.push('/select_team');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -250,7 +255,12 @@ export default class SignupUserComplete extends React.Component {
|
||||
|
||||
GlobalActions.emitInitialLoad(
|
||||
() => {
|
||||
browserHistory.push('/select_team');
|
||||
const query = this.props.location.query;
|
||||
if (query.redirect_to) {
|
||||
browserHistory.push(query.redirect_to);
|
||||
} else {
|
||||
browserHistory.push('/select_team');
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
@@ -6,7 +6,6 @@ import * as utils from 'utils/utils.jsx';
|
||||
import NotificationsTab from './user_settings_notifications.jsx';
|
||||
import SecurityTab from './user_settings_security.jsx';
|
||||
import GeneralTab from './user_settings_general.jsx';
|
||||
import DeveloperTab from './user_settings_developer.jsx';
|
||||
import DisplayTab from './user_settings_display.jsx';
|
||||
import AdvancedTab from './user_settings_advanced.jsx';
|
||||
|
||||
@@ -77,17 +76,6 @@ export default class UserSettings extends React.Component {
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.activeTab === 'developer') {
|
||||
return (
|
||||
<div>
|
||||
<DeveloperTab
|
||||
activeSection={this.props.activeSection}
|
||||
updateSection={this.props.updateSection}
|
||||
closeModal={this.props.closeModal}
|
||||
collapseModal={this.props.collapseModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (this.props.activeTab === 'display') {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||
// See License.txt for license information.
|
||||
|
||||
import SettingItemMin from '../setting_item_min.jsx';
|
||||
import SettingItemMax from '../setting_item_max.jsx';
|
||||
import * as GlobalActions from 'actions/global_actions.jsx';
|
||||
|
||||
import {intlShape, injectIntl, defineMessages, FormattedMessage} from 'react-intl';
|
||||
|
||||
const holders = defineMessages({
|
||||
applicationsPreview: {
|
||||
id: 'user.settings.developer.applicationsPreview',
|
||||
defaultMessage: 'Applications (Preview)'
|
||||
},
|
||||
thirdParty: {
|
||||
id: 'user.settings.developer.thirdParty',
|
||||
defaultMessage: 'Open to register a new third-party application'
|
||||
}
|
||||
});
|
||||
|
||||
import React from 'react';
|
||||
|
||||
class DeveloperTab extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.register = this.register.bind(this);
|
||||
|
||||
this.state = {};
|
||||
}
|
||||
register() {
|
||||
this.props.closeModal();
|
||||
GlobalActions.showRegisterAppModal();
|
||||
}
|
||||
render() {
|
||||
var appSection;
|
||||
var self = this;
|
||||
const {formatMessage} = this.props.intl;
|
||||
if (this.props.activeSection === 'app') {
|
||||
var inputs = [];
|
||||
|
||||
inputs.push(
|
||||
<div
|
||||
key='registerbtn'
|
||||
className='form-group'
|
||||
>
|
||||
<div className='col-sm-7'>
|
||||
<a
|
||||
className='btn btn-sm btn-primary'
|
||||
onClick={this.register}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='user.settings.developer.register'
|
||||
defaultMessage='Register New Application'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
appSection = (
|
||||
<SettingItemMax
|
||||
title={formatMessage(holders.applicationsPreview)}
|
||||
inputs={inputs}
|
||||
updateSection={function updateSection(e) {
|
||||
self.props.updateSection('');
|
||||
e.preventDefault();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
appSection = (
|
||||
<SettingItemMin
|
||||
title={formatMessage(holders.applicationsPreview)}
|
||||
describe={formatMessage(holders.thirdParty)}
|
||||
updateSection={function updateSection() {
|
||||
self.props.updateSection('app');
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='modal-header'>
|
||||
<button
|
||||
type='button'
|
||||
className='close'
|
||||
data-dismiss='modal'
|
||||
aria-label='Close'
|
||||
onClick={this.props.closeModal}
|
||||
>
|
||||
<span aria-hidden='true'>{'×'}</span>
|
||||
</button>
|
||||
<h4
|
||||
className='modal-title'
|
||||
ref='title'
|
||||
>
|
||||
<div className='modal-back'>
|
||||
<i
|
||||
className='fa fa-angle-left'
|
||||
onClick={this.props.collapseModal}
|
||||
/>
|
||||
</div>
|
||||
<FormattedMessage
|
||||
id='user.settings.developer.title'
|
||||
defaultMessage='Developer Settings'
|
||||
/>
|
||||
</h4>
|
||||
</div>
|
||||
<div className='user-settings'>
|
||||
<h3 className='tab-header'>
|
||||
<FormattedMessage
|
||||
id='user.settings.developer.title'
|
||||
defaultMessage='Developer Settings'
|
||||
/>
|
||||
</h3>
|
||||
<div className='divider-dark first'/>
|
||||
{appSection}
|
||||
<div className='divider-dark'/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
DeveloperTab.defaultProps = {
|
||||
activeSection: ''
|
||||
};
|
||||
DeveloperTab.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
activeSection: React.PropTypes.string,
|
||||
updateSection: React.PropTypes.func,
|
||||
closeModal: React.PropTypes.func.isRequired,
|
||||
collapseModal: React.PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default injectIntl(DeveloperTab);
|
||||
@@ -27,10 +27,6 @@ const holders = defineMessages({
|
||||
id: 'user.settings.modal.notifications',
|
||||
defaultMessage: 'Notifications'
|
||||
},
|
||||
developer: {
|
||||
id: 'user.settings.modal.developer',
|
||||
defaultMessage: 'Developer'
|
||||
},
|
||||
display: {
|
||||
id: 'user.settings.modal.display',
|
||||
defaultMessage: 'Display'
|
||||
@@ -214,10 +210,6 @@ class UserSettingsModal extends React.Component {
|
||||
tabs.push({name: 'general', uiName: formatMessage(holders.general), icon: 'icon fa fa-gear'});
|
||||
tabs.push({name: 'security', uiName: formatMessage(holders.security), icon: 'icon fa fa-lock'});
|
||||
tabs.push({name: 'notifications', uiName: formatMessage(holders.notifications), icon: 'icon fa fa-exclamation-circle'});
|
||||
if (global.window.mm_config.EnableOAuthServiceProvider === 'true') {
|
||||
tabs.push({name: 'developer', uiName: formatMessage(holders.developer), icon: 'icon fa fa-th'});
|
||||
}
|
||||
|
||||
tabs.push({name: 'display', uiName: formatMessage(holders.display), icon: 'icon fa fa-eye'});
|
||||
tabs.push({name: 'advanced', uiName: formatMessage(holders.advanced), icon: 'icon fa fa-list-alt'});
|
||||
|
||||
|
||||
@@ -93,6 +93,17 @@
|
||||
"add_incoming_webhook.header": "Add",
|
||||
"add_incoming_webhook.name": "Name",
|
||||
"add_incoming_webhook.save": "Save",
|
||||
"add_oauth_app.callbackUrls.help": "The redirect URIs to which the service will redirect users after accepting or denying authorization of your application, and which will handle authorization codes or access tokens. Must be a valid URL and start with http:// or https://.",
|
||||
"add_oauth_app.callbackUrlsRequired": "One or more callback URLs are required",
|
||||
"add_oauth_app.description.help": "Description for your OAuth 2.0 application.",
|
||||
"add_oauth_app.descriptionRequired": "Description for the OAuth 2.0 application is required.",
|
||||
"add_oauth_app.header": "Add",
|
||||
"add_oauth_app.homepage.help": "The URL for the homepage of the OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL depending on your server configuration.",
|
||||
"add_oauth_app.homepageRequired": "Homepage for the OAuth 2.0 application is required.",
|
||||
"add_oauth_app.icon.help": "(Optional) The URL of the image used for your OAuth 2.0 application. Make sure you use HTTP or HTTPS in your URL.",
|
||||
"add_oauth_app.name.help": "Display name for your OAuth 2.0 application made of up to 64 characters.",
|
||||
"add_oauth_app.nameRequired": "Name for the OAuth 2.0 application is required.",
|
||||
"add_oauth_app.trusted.help": "When true, the OAuth 2.0 application is considered trusted by the Mattermost server and doesn't require the user to accept authorization. When false, an additional window will appear, asking the user to accept or deny the authorization.",
|
||||
"add_outgoing_webhook.callbackUrls": "Callback URLs (One Per Line)",
|
||||
"add_outgoing_webhook.callbackUrls.help": "The URL that messages will be sent to.",
|
||||
"add_outgoing_webhook.callbackUrlsRequired": "One or more callback URLs are required",
|
||||
@@ -360,8 +371,8 @@
|
||||
"admin.image.thumbWidthDescription": "Width of thumbnails generated from uploaded images. Updating this value changes how thumbnail images render in future, but does not change images created in the past.",
|
||||
"admin.image.thumbWidthExample": "Ex \"120\"",
|
||||
"admin.image.thumbWidthTitle": "Attachment Thumbnail Width:",
|
||||
"admin.integrations.custom": "Custom Integrations",
|
||||
"admin.integrations.external": "External Services",
|
||||
"admin.integrations.webhook": "Webhooks and Commands",
|
||||
"admin.ldap.baseDesc": "The Base DN is the Distinguished Name of the location where Mattermost should start its search for users in the LDAP tree.",
|
||||
"admin.ldap.baseEx": "Ex \"ou=Unit Name,dc=corp,dc=example,dc=com\"",
|
||||
"admin.ldap.baseTitle": "BaseDN:",
|
||||
@@ -459,6 +470,8 @@
|
||||
"admin.notifications.email": "Email",
|
||||
"admin.notifications.push": "Mobile Push",
|
||||
"admin.notifications.title": "Notification Settings",
|
||||
"admin.oauth.providerDescription": "When true, Mattermost can act as an OAuth 2.0 service provider allowing external applications to authorize API requests to Mattermost.",
|
||||
"admin.oauth.providerTitle": "Enable OAuth 2.0 Service Provider: ",
|
||||
"admin.password.lowercase": "At least one lowercase letter",
|
||||
"admin.password.minimumLength": "Minimum Password Length:",
|
||||
"admin.password.minimumLengthDescription": "Minimum number of characters required for a valid password. Must be a whole number greater than or equal to {min} and less than or equal to {max}.",
|
||||
@@ -625,6 +638,7 @@
|
||||
"admin.sidebar.connections": "Connections",
|
||||
"admin.sidebar.customBrand": "Custom Branding",
|
||||
"admin.sidebar.customEmoji": "Custom Emoji",
|
||||
"admin.sidebar.customIntegrations": "Custom Integrations",
|
||||
"admin.sidebar.customization": "Customization",
|
||||
"admin.sidebar.database": "Database",
|
||||
"admin.sidebar.developer": "Developer",
|
||||
@@ -666,7 +680,6 @@
|
||||
"admin.sidebar.users": "Users",
|
||||
"admin.sidebar.usersAndTeams": "Users and Teams",
|
||||
"admin.sidebar.view_statistics": "Site Statistics",
|
||||
"admin.sidebar.webhooks": "Webhooks and Commands",
|
||||
"admin.sidebarHeader.systemConsole": "System Console",
|
||||
"admin.sql.dataSource": "Data Source:",
|
||||
"admin.sql.driverName": "Driver Name:",
|
||||
@@ -861,12 +874,13 @@
|
||||
"authorize.allow": "Allow",
|
||||
"authorize.app": "The app <strong>{appName}</strong> would like the ability to access and modify your basic information.",
|
||||
"authorize.deny": "Deny",
|
||||
"authorize.title": "An application would like to connect to your {teamName} account",
|
||||
"authorize.title": "<strong>{appName}</strong> would like to connect to your <strong>Mattermost</strong> user account",
|
||||
"backstage_list.search": "Search",
|
||||
"backstage_navbar.backToMattermost": "Back to {siteName}",
|
||||
"backstage_sidebar.integrations": "Integrations",
|
||||
"backstage_sidebar.integrations.commands": "Slash Commands",
|
||||
"backstage_sidebar.integrations.incoming_webhooks": "Incoming Webhooks",
|
||||
"backstage_sidebar.integrations.oauthApps": "OAuth 2.0 Applications",
|
||||
"backstage_sidebar.integrations.outgoing_webhooks": "Outgoing Webhooks",
|
||||
"center_panel.recent": "Click here to jump to recent messages. ",
|
||||
"chanel_header.addMembers": "Add Members",
|
||||
@@ -1161,14 +1175,35 @@
|
||||
"installed_incoming_webhooks.search": "Search Incoming Webhooks",
|
||||
"installed_incoming_webhooks.unknown_channel": "A Private Webhook",
|
||||
"installed_integrations.callback_urls": "Callback URLs: {urls}",
|
||||
"installed_integrations.client_id": "Client ID: <strong>{clientId}</strong>",
|
||||
"installed_integrations.client_secret": "Client Secret: <strong>{clientSecret}</strong>",
|
||||
"installed_integrations.content_type": "Content-Type: {contentType}",
|
||||
"installed_integrations.creation": "Created by {creator} on {createAt, date, full}",
|
||||
"installed_integrations.delete": "Delete",
|
||||
"installed_integrations.hideSecret": "Hide Secret",
|
||||
"installed_integrations.regenToken": "Regenerate Token",
|
||||
"installed_integrations.showSecret": "Show Secret",
|
||||
"installed_integrations.token": "Token: {token}",
|
||||
"installed_integrations.triggerWords": "Trigger Words: {triggerWords}",
|
||||
"installed_integrations.triggerWhen": "Trigger When: {triggerWhen}",
|
||||
"installed_integrations.unnamed_oauth_app": "Unnamed OAuth 2.0 Application",
|
||||
"installed_integrations.url": "URL: {url}",
|
||||
"installed_oauth_apps.add": "Add OAuth 2.0 Application",
|
||||
"installed_oauth_apps.callbackUrls": "Callback URLs (One Per Line)",
|
||||
"installed_oauth_apps.cancel": "Cancel",
|
||||
"installed_oauth_apps.description": "Description",
|
||||
"installed_oauth_apps.empty": "No OAuth 2.0 Applications found",
|
||||
"installed_oauth_apps.header": "OAuth 2.0 Applications",
|
||||
"installed_oauth_apps.help": "OAuth 2.0 Applications are available to everyone on your server.",
|
||||
"installed_oauth_apps.homepage": "Homepage",
|
||||
"installed_oauth_apps.iconUrl": "Icon URL",
|
||||
"installed_oauth_apps.is_trusted": "Is Trusted: <strong>{isTrusted}</strong>",
|
||||
"installed_oauth_apps.name": "Display Name",
|
||||
"installed_oauth_apps.save": "Save",
|
||||
"installed_oauth_apps.search": "Search OAuth 2.0 Applications",
|
||||
"installed_oauth_apps.trusted": "Is Trusted",
|
||||
"installed_oauth_apps.trusted.no": "No",
|
||||
"installed_oauth_apps.trusted.yes": "Yes",
|
||||
"installed_outgoing_webhooks.add": "Add Outgoing Webhook",
|
||||
"installed_outgoing_webhooks.empty": "No outgoing webhooks found",
|
||||
"installed_outgoing_webhooks.header": "Outgoing Webhooks",
|
||||
@@ -1181,6 +1216,8 @@
|
||||
"integrations.header": "Integrations",
|
||||
"integrations.incomingWebhook.description": "Incoming webhooks allow external integrations to send messages",
|
||||
"integrations.incomingWebhook.title": "Incoming Webhook",
|
||||
"integrations.oauthApps.description": "OAuth 2.0 allows external applications to make authorized requests to the Mattermost API.",
|
||||
"integrations.oauthApps.title": "OAuth 2.0 Applications",
|
||||
"integrations.outgoingWebhook.description": "Outgoing webhooks allow external integrations to receive and respond to messages",
|
||||
"integrations.outgoingWebhook.title": "Outgoing Webhook",
|
||||
"intro_messages.DM": "This is the start of your direct message history with {teammate}.<br />Direct messages and files shared here are not shown to people outside this area.",
|
||||
@@ -1338,25 +1375,6 @@
|
||||
"post_info.reply": "Reply",
|
||||
"posts_view.loadMore": "Load more messages",
|
||||
"posts_view.newMsg": "New Messages",
|
||||
"register_app.callback": "Callback URL",
|
||||
"register_app.callbackError": "At least one callback URL must be filled in.",
|
||||
"register_app.cancel": "Cancel",
|
||||
"register_app.clientId": "Client ID",
|
||||
"register_app.clientSecret": "Client Secret",
|
||||
"register_app.close": "Close",
|
||||
"register_app.credentialsDescription": "Save these somewhere SAFE and SECURE. Treat your Client ID as your app's username and your Client Secret as the app's password.",
|
||||
"register_app.credentialsSave": "I have saved both my Client Id and Client Secret somewhere safe",
|
||||
"register_app.credentialsTitle": "Your Application Credentials",
|
||||
"register_app.description": "Description",
|
||||
"register_app.dev": "Developer Applications",
|
||||
"register_app.homepage": "Homepage URL",
|
||||
"register_app.homepageError": "Homepage must be filled in.",
|
||||
"register_app.name": "Application Name",
|
||||
"register_app.nameError": "Application name must be filled in.",
|
||||
"register_app.optional": "Optional",
|
||||
"register_app.register": "Register",
|
||||
"register_app.required": "Required",
|
||||
"register_app.title": "Register a New Application",
|
||||
"removed_channel.channelName": "the channel",
|
||||
"removed_channel.from": "Removed from ",
|
||||
"removed_channel.okay": "Okay",
|
||||
@@ -1581,10 +1599,6 @@
|
||||
"user.settings.custom_theme.sidebarTextHoverBg": "Sidebar Text Hover BG",
|
||||
"user.settings.custom_theme.sidebarTitle": "Sidebar Styles",
|
||||
"user.settings.custom_theme.sidebarUnreadText": "Sidebar Unread Text",
|
||||
"user.settings.developer.applicationsPreview": "Applications (Preview)",
|
||||
"user.settings.developer.register": "Register New Application",
|
||||
"user.settings.developer.thirdParty": "Open to register a new third-party application",
|
||||
"user.settings.developer.title": "Developer Settings",
|
||||
"user.settings.display.channelDisplayTitle": "Channel Display Mode",
|
||||
"user.settings.display.channeldisplaymode": "Select the width of the center channel.",
|
||||
"user.settings.display.clockDisplay": "Clock Display",
|
||||
@@ -1679,7 +1693,6 @@
|
||||
"user.settings.modal.confirmBtns": "Yes, Discard",
|
||||
"user.settings.modal.confirmMsg": "You have unsaved changes, are you sure you want to discard them?",
|
||||
"user.settings.modal.confirmTitle": "Discard Changes?",
|
||||
"user.settings.modal.developer": "Developer",
|
||||
"user.settings.modal.display": "Display",
|
||||
"user.settings.modal.general": "General",
|
||||
"user.settings.modal.notifications": "Notifications",
|
||||
|
||||
BIN
webapp/images/oauth_icon.png
Normal file
BIN
webapp/images/oauth_icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB After Width: | Height: | Size: 20 KiB |
@@ -34,20 +34,13 @@
|
||||
|
||||
<!-- CSS Should always go first -->
|
||||
<link rel='stylesheet' class='code_theme'>
|
||||
<style id='antiClickjack'>body{display:none !important;}</style>
|
||||
|
||||
<script type='text/javascript'>
|
||||
if (self === top) {
|
||||
var blocker = document.getElementById('antiClickjack');
|
||||
blocker.parentNode.removeChild(blocker);
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id='root'>
|
||||
<div
|
||||
class='loading-screen'
|
||||
style='relative'
|
||||
style='position: relative'
|
||||
>
|
||||
<div class='loading__content'>
|
||||
<div class='round round-1'></div>
|
||||
|
||||
@@ -24,7 +24,7 @@ import SessionSettings from 'components/admin_console/session_settings.jsx';
|
||||
import ConnectionSettings from 'components/admin_console/connection_settings.jsx';
|
||||
import EmailSettings from 'components/admin_console/email_settings.jsx';
|
||||
import PushSettings from 'components/admin_console/push_settings.jsx';
|
||||
import WebhookSettings from 'components/admin_console/webhook_settings.jsx';
|
||||
import CustomIntegrationsSettings from 'components/admin_console/custom_integrations_settings.jsx';
|
||||
import ExternalServiceSettings from 'components/admin_console/external_service_settings.jsx';
|
||||
import DatabaseSettings from 'components/admin_console/database_settings.jsx';
|
||||
import StorageSettings from 'components/admin_console/storage_settings.jsx';
|
||||
@@ -137,10 +137,10 @@ export default (
|
||||
/>
|
||||
</Route>
|
||||
<Route path='integrations'>
|
||||
<IndexRedirect to='webhooks'/>
|
||||
<IndexRedirect to='custom'/>
|
||||
<Route
|
||||
path='webhooks'
|
||||
component={WebhookSettings}
|
||||
path='custom'
|
||||
component={CustomIntegrationsSettings}
|
||||
/>
|
||||
<Route
|
||||
path='external'
|
||||
|
||||
@@ -61,6 +61,22 @@ export default {
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: 'oauth2-apps',
|
||||
indexRoute: {
|
||||
getComponents: (location, callback) => {
|
||||
System.import('components/integrations/components/installed_oauth_apps.jsx').then(RouteUtils.importComponentSuccess(callback));
|
||||
}
|
||||
},
|
||||
childRoutes: [
|
||||
{
|
||||
path: 'add',
|
||||
getComponents: (location, callback) => {
|
||||
System.import('components/integrations/components/add_oauth_app.jsx').then(RouteUtils.importComponentSuccess(callback));
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@@ -130,6 +130,12 @@ export default {
|
||||
System.import('components/select_team/select_team.jsx').then(RouteUtils.importComponentSuccess(callback));
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '*authorize',
|
||||
getComponents: (location, callback) => {
|
||||
System.import('components/authorize.jsx').then(RouteUtils.importComponentSuccess(callback));
|
||||
}
|
||||
},
|
||||
createTeamRoute
|
||||
]
|
||||
)
|
||||
|
||||
@@ -10,14 +10,19 @@
|
||||
|
||||
.prompt__heading {
|
||||
display: table;
|
||||
font-size: em(20px);
|
||||
font-size: em(18px);
|
||||
line-height: normal;
|
||||
margin: 1em 0;
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
|
||||
> div {
|
||||
display: table-cell;
|
||||
vertical-align: top;
|
||||
|
||||
&:first-child {
|
||||
width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
@@ -26,12 +31,12 @@
|
||||
}
|
||||
|
||||
.prompt__allow {
|
||||
font-size: em(24px);
|
||||
font-size: em(20px);
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.prompt__buttons {
|
||||
border-top: 1px solid $dark-gray;
|
||||
border-top: 1px solid $light-gray;
|
||||
padding: 1.5em 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,19 @@
|
||||
@charset 'UTF-8';
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
.prompt {
|
||||
.prompt__heading {
|
||||
display: block;
|
||||
|
||||
> div {
|
||||
&:first-child {
|
||||
display: block;
|
||||
margin: 0 0 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.scrollbar--view {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
@@ -1092,6 +1105,10 @@
|
||||
@include translate3d(260px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.integration__icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 640px) {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
.backstage-content {
|
||||
background-color: $bg--gray;
|
||||
margin: 46px auto;
|
||||
max-width: 960px;
|
||||
max-width: 1200px;
|
||||
padding-left: 135px;
|
||||
vertical-align: top;
|
||||
}
|
||||
@@ -216,6 +216,7 @@
|
||||
border-bottom: 1px solid $light-gray;
|
||||
display: flex;
|
||||
padding: 20px 15px;
|
||||
position: relative;
|
||||
|
||||
&:last-child {
|
||||
border: none;
|
||||
@@ -276,6 +277,7 @@
|
||||
background-color: $white;
|
||||
border: 1px solid $light-gray;
|
||||
padding: 40px 30px 30px;
|
||||
position: relative;
|
||||
|
||||
label {
|
||||
font-weight: normal;
|
||||
@@ -323,16 +325,27 @@
|
||||
}
|
||||
}
|
||||
|
||||
.integration__icon {
|
||||
position: absolute;
|
||||
height: 100px;
|
||||
width: 100px;
|
||||
right: 20px;
|
||||
|
||||
&.integration-list__icon {
|
||||
top: 50px;
|
||||
}
|
||||
}
|
||||
|
||||
.integration-option {
|
||||
background-color: $white;
|
||||
border: 1px solid $light-gray;
|
||||
display: inline-block;
|
||||
height: 210px;
|
||||
margin: 0 30px 30px 0;
|
||||
min-height: 230px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
width: 250px;
|
||||
width: 290px;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
@@ -346,6 +359,7 @@
|
||||
|
||||
.integration-option__image {
|
||||
height: 80px;
|
||||
margin: .5em 0 .7em;
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ class IntegrationStore extends EventEmitter {
|
||||
this.outgoingWebhooks = new Map();
|
||||
|
||||
this.commands = new Map();
|
||||
|
||||
this.oauthApps = new Map();
|
||||
}
|
||||
|
||||
addChangeListener(callback) {
|
||||
@@ -149,6 +151,35 @@ class IntegrationStore extends EventEmitter {
|
||||
this.setCommands(teamId, commands);
|
||||
}
|
||||
|
||||
hasReceivedOAuthApps(userId) {
|
||||
return this.oauthApps.has(userId);
|
||||
}
|
||||
|
||||
getOAuthApps(userId) {
|
||||
return this.oauthApps.get(userId) || [];
|
||||
}
|
||||
|
||||
setOAuthApps(userId, oauthApps) {
|
||||
this.oauthApps.set(userId, oauthApps);
|
||||
}
|
||||
|
||||
addOAuthApp(oauthApp) {
|
||||
const userId = oauthApp.creator_id;
|
||||
const oauthApps = this.getOAuthApps(userId);
|
||||
|
||||
oauthApps.push(oauthApp);
|
||||
|
||||
this.setOAuthApps(userId, oauthApps);
|
||||
}
|
||||
|
||||
removeOAuthApp(userId, id) {
|
||||
let apps = this.getOAuthApps(userId);
|
||||
|
||||
apps = apps.filter((app) => app.id !== id);
|
||||
|
||||
this.setOAuthApps(userId, apps);
|
||||
}
|
||||
|
||||
handleEventPayload(payload) {
|
||||
const action = payload.action;
|
||||
|
||||
@@ -197,6 +228,18 @@ class IntegrationStore extends EventEmitter {
|
||||
this.removeCommand(action.teamId, action.id);
|
||||
this.emitChange();
|
||||
break;
|
||||
case ActionTypes.RECEIVED_OAUTHAPPS:
|
||||
this.setOAuthApps(action.userId, action.oauthApps);
|
||||
this.emitChange();
|
||||
break;
|
||||
case ActionTypes.RECEIVED_OAUTHAPP:
|
||||
this.addOAuthApp(action.oauthApp);
|
||||
this.emitChange();
|
||||
break;
|
||||
case ActionTypes.REMOVED_OAUTHAPP:
|
||||
this.removeOAuthApp(action.userId, action.id);
|
||||
this.emitChange();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ class ModalStoreClass extends EventEmitter {
|
||||
case ActionTypes.TOGGLE_DELETE_POST_MODAL:
|
||||
case ActionTypes.TOGGLE_GET_POST_LINK_MODAL:
|
||||
case ActionTypes.TOGGLE_GET_TEAM_INVITE_LINK_MODAL:
|
||||
case ActionTypes.TOGGLE_REGISTER_APP_MODAL:
|
||||
case ActionTypes.TOGGLE_GET_PUBLIC_LINK_MODAL:
|
||||
this.emit(type, value, args);
|
||||
break;
|
||||
|
||||
@@ -106,6 +106,9 @@ export const ActionTypes = keyMirror({
|
||||
RECEIVED_COMMAND: null,
|
||||
UPDATED_COMMAND: null,
|
||||
REMOVED_COMMAND: null,
|
||||
RECEIVED_OAUTHAPPS: null,
|
||||
RECEIVED_OAUTHAPP: null,
|
||||
REMOVED_OAUTHAPP: null,
|
||||
|
||||
RECEIVED_CUSTOM_EMOJIS: null,
|
||||
RECEIVED_CUSTOM_EMOJI: null,
|
||||
@@ -138,7 +141,6 @@ export const ActionTypes = keyMirror({
|
||||
TOGGLE_DELETE_POST_MODAL: null,
|
||||
TOGGLE_GET_POST_LINK_MODAL: null,
|
||||
TOGGLE_GET_TEAM_INVITE_LINK_MODAL: null,
|
||||
TOGGLE_REGISTER_APP_MODAL: null,
|
||||
TOGGLE_GET_PUBLIC_LINK_MODAL: null,
|
||||
|
||||
SUGGESTION_PRETEXT_CHANGED: null,
|
||||
|
||||
Reference in New Issue
Block a user