SAML support (#3494)
* PLT-3073: Implement SAML/Okta Server side (EE) (#3422) * PLT-3137 Support for SAML configuration * PLT-3410 SAML Database Store * PLT-3411 CLI to add Identity Provider Certificate and Service Provider Private Key * PLT-3409 SAML Interface for EE * PLT-3139 Handle SAML authentication server side * Add localization messages * PLT-3443 SAML Obtain SP metadata * PLT-3142 Login & Switch to/from SAML * Remove Certs for Database & Clean SAML Request * Make required Username, FirstName and LastName * PLT-3140 Add SAML to System Console (#3476) * PLT-3140 Add SAML to System Console * Move web_client functions to client.jsx * Fix issues found by PM * update package.json mattermost driver * Fix text messages for SAML
Этот коммит содержится в:
77
api/admin.go
77
api/admin.go
@@ -5,6 +5,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -41,6 +42,9 @@ func InitAdmin() {
|
|||||||
BaseRoutes.Admin.Handle("/reset_mfa", ApiAdminSystemRequired(adminResetMfa)).Methods("POST")
|
BaseRoutes.Admin.Handle("/reset_mfa", ApiAdminSystemRequired(adminResetMfa)).Methods("POST")
|
||||||
BaseRoutes.Admin.Handle("/reset_password", ApiAdminSystemRequired(adminResetPassword)).Methods("POST")
|
BaseRoutes.Admin.Handle("/reset_password", ApiAdminSystemRequired(adminResetPassword)).Methods("POST")
|
||||||
BaseRoutes.Admin.Handle("/ldap_sync_now", ApiAdminSystemRequired(ldapSyncNow)).Methods("POST")
|
BaseRoutes.Admin.Handle("/ldap_sync_now", ApiAdminSystemRequired(ldapSyncNow)).Methods("POST")
|
||||||
|
BaseRoutes.Admin.Handle("/saml_metadata", ApiAppHandler(samlMetadata)).Methods("GET")
|
||||||
|
BaseRoutes.Admin.Handle("/add_certificate", ApiAdminSystemRequired(addCertificate)).Methods("POST")
|
||||||
|
BaseRoutes.Admin.Handle("/remove_certificate", ApiAdminSystemRequired(removeCertificate)).Methods("POST")
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
|
func getLogs(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -582,3 +586,76 @@ func ldapSyncNow(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
rdata["status"] = "ok"
|
rdata["status"] = "ok"
|
||||||
w.Write([]byte(model.MapToJson(rdata)))
|
w.Write([]byte(model.MapToJson(rdata)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func samlMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
samlInterface := einterfaces.GetSamlInterface()
|
||||||
|
|
||||||
|
if samlInterface == nil {
|
||||||
|
c.Err = model.NewLocAppError("loginWithSaml", "api.admin.saml.not_available.app_error", nil, "")
|
||||||
|
c.Err.StatusCode = http.StatusFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if result, err := samlInterface.GetMetadata(); err != nil {
|
||||||
|
c.Err = model.NewLocAppError("loginWithSaml", "api.admin.saml.metadata.app_error", nil, "err="+err.Message)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Content-Type", "application/xml")
|
||||||
|
w.Header().Set("Content-Disposition", "attachment; filename=\"metadata.xml\"")
|
||||||
|
w.Write([]byte(result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
err := r.ParseMultipartForm(*utils.Cfg.FileSettings.MaxFileSize)
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
m := r.MultipartForm
|
||||||
|
|
||||||
|
fileArray, ok := m.File["certificate"]
|
||||||
|
if !ok {
|
||||||
|
c.Err = model.NewLocAppError("addCertificate", "api.admin.add_certificate.no_file.app_error", nil, "")
|
||||||
|
c.Err.StatusCode = http.StatusBadRequest
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fileArray) <= 0 {
|
||||||
|
c.Err = model.NewLocAppError("addCertificate", "api.admin.add_certificate.array.app_error", nil, "")
|
||||||
|
c.Err.StatusCode = http.StatusBadRequest
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fileData := fileArray[0]
|
||||||
|
|
||||||
|
file, err := fileData.Open()
|
||||||
|
defer file.Close()
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewLocAppError("addCertificate", "api.admin.add_certificate.open.app_error", nil, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(utils.FindDir("config") + fileData.Filename)
|
||||||
|
if err != nil {
|
||||||
|
c.Err = model.NewLocAppError("addCertificate", "api.admin.add_certificate.saving.app_error", nil, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
io.Copy(out, file)
|
||||||
|
ReturnStatusOK(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
props := model.MapFromJson(r.Body)
|
||||||
|
|
||||||
|
filename := props["filename"]
|
||||||
|
if err := os.Remove(utils.FindConfigFile(filename)); err != nil {
|
||||||
|
c.Err = model.NewLocAppError("removeCertificate", "api.admin.remove_certificate.delete.app_error",
|
||||||
|
map[string]interface{}{"Filename": filename}, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ReturnStatusOK(w)
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"github.com/mattermost/platform/utils"
|
"github.com/mattermost/platform/utils"
|
||||||
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
func checkPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
|
func checkPasswordAndAllCriteria(user *model.User, password string, mfaToken string) *model.AppError {
|
||||||
@@ -145,7 +146,11 @@ func authenticateUser(user *model.User, password, mfaToken string) (*model.User,
|
|||||||
return ldapUser, nil
|
return ldapUser, nil
|
||||||
}
|
}
|
||||||
} else if user.AuthService != "" {
|
} else if user.AuthService != "" {
|
||||||
err := model.NewLocAppError("login", "api.user.login.use_auth_service.app_error", map[string]interface{}{"AuthService": user.AuthService}, "")
|
authService := user.AuthService
|
||||||
|
if authService == model.USER_AUTH_SERVICE_SAML || authService == model.USER_AUTH_SERVICE_LDAP {
|
||||||
|
authService = strings.ToUpper(authService)
|
||||||
|
}
|
||||||
|
err := model.NewLocAppError("login", "api.user.login.use_auth_service.app_error", map[string]interface{}{"AuthService": authService}, "")
|
||||||
err.StatusCode = http.StatusBadRequest
|
err.StatusCode = http.StatusBadRequest
|
||||||
return user, err
|
return user, err
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -477,6 +477,11 @@ func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request)
|
|||||||
link := "/"
|
link := "/"
|
||||||
linkMessage := T("api.templates.error.link")
|
linkMessage := T("api.templates.error.link")
|
||||||
|
|
||||||
|
status := http.StatusTemporaryRedirect
|
||||||
|
if err.StatusCode != http.StatusInternalServerError {
|
||||||
|
status = err.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
http.Redirect(
|
http.Redirect(
|
||||||
w,
|
w,
|
||||||
r,
|
r,
|
||||||
@@ -485,7 +490,7 @@ func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request)
|
|||||||
"&details="+url.QueryEscape(details)+
|
"&details="+url.QueryEscape(details)+
|
||||||
"&link="+url.QueryEscape(link)+
|
"&link="+url.QueryEscape(link)+
|
||||||
"&linkmessage="+url.QueryEscape(linkMessage),
|
"&linkmessage="+url.QueryEscape(linkMessage),
|
||||||
http.StatusTemporaryRedirect)
|
status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Handle404(w http.ResponseWriter, r *http.Request) {
|
func Handle404(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
96
api/user.go
96
api/user.go
@@ -5,6 +5,7 @@ package api
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
b64 "encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"hash/fnv"
|
"hash/fnv"
|
||||||
"html/template"
|
"html/template"
|
||||||
@@ -71,6 +72,9 @@ func InitUser() {
|
|||||||
BaseRoutes.NeedUser.Handle("/sessions", ApiUserRequired(getSessions)).Methods("GET")
|
BaseRoutes.NeedUser.Handle("/sessions", ApiUserRequired(getSessions)).Methods("GET")
|
||||||
BaseRoutes.NeedUser.Handle("/audits", ApiUserRequired(getAudits)).Methods("GET")
|
BaseRoutes.NeedUser.Handle("/audits", ApiUserRequired(getAudits)).Methods("GET")
|
||||||
BaseRoutes.NeedUser.Handle("/image", ApiUserRequiredTrustRequester(getProfileImage)).Methods("GET")
|
BaseRoutes.NeedUser.Handle("/image", ApiUserRequiredTrustRequester(getProfileImage)).Methods("GET")
|
||||||
|
|
||||||
|
BaseRoutes.Root.Handle("/login/sso/saml", AppHandlerIndependent(loginWithSaml)).Methods("GET")
|
||||||
|
BaseRoutes.Root.Handle("/login/sso/saml", AppHandlerIndependent(completeSaml)).Methods("POST")
|
||||||
}
|
}
|
||||||
|
|
||||||
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
func createUser(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -2005,6 +2009,9 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
stateProps["email"] = email
|
stateProps["email"] = email
|
||||||
|
|
||||||
m := map[string]string{}
|
m := map[string]string{}
|
||||||
|
if service == model.USER_AUTH_SERVICE_SAML {
|
||||||
|
m["follow_link"] = c.GetSiteURL() + "/login/sso/saml?action=" + model.OAUTH_ACTION_EMAIL_TO_SSO + "&email=" + email
|
||||||
|
} else {
|
||||||
if authUrl, err := GetAuthorizationCode(c, service, stateProps, ""); err != nil {
|
if authUrl, err := GetAuthorizationCode(c, service, stateProps, ""); err != nil {
|
||||||
c.LogAuditWithUserId(user.Id, "fail - oauth issue")
|
c.LogAuditWithUserId(user.Id, "fail - oauth issue")
|
||||||
c.Err = err
|
c.Err = err
|
||||||
@@ -2012,6 +2019,7 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
} else {
|
} else {
|
||||||
m["follow_link"] = authUrl
|
m["follow_link"] = authUrl
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
c.LogAuditWithUserId(user.Id, "success")
|
c.LogAuditWithUserId(user.Id, "success")
|
||||||
w.Write([]byte(model.MapToJson(m)))
|
w.Write([]byte(model.MapToJson(m)))
|
||||||
@@ -2419,3 +2427,91 @@ func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
w.Write([]byte(model.MapToJson(rdata)))
|
w.Write([]byte(model.MapToJson(rdata)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loginWithSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
samlInterface := einterfaces.GetSamlInterface()
|
||||||
|
|
||||||
|
if samlInterface == nil {
|
||||||
|
c.Err = model.NewLocAppError("loginWithSaml", "api.user.saml.not_available.app_error", nil, "")
|
||||||
|
c.Err.StatusCode = http.StatusFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
teamId, err := getTeamIdFromQuery(r.URL.Query())
|
||||||
|
if err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
action := r.URL.Query().Get("action")
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
relayState = b64.StdEncoding.EncodeToString([]byte(model.MapToJson(relayProps)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if data, err := samlInterface.BuildRequest(relayState); err != nil {
|
||||||
|
c.Err = err
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
|
||||||
|
http.Redirect(w, r, data.URL, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func completeSaml(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||||
|
samlInterface := einterfaces.GetSamlInterface()
|
||||||
|
|
||||||
|
if samlInterface == nil {
|
||||||
|
c.Err = model.NewLocAppError("completeSaml", "api.user.saml.not_available.app_error", nil, "")
|
||||||
|
c.Err.StatusCode = http.StatusFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Validate that the user is with SAML and all that
|
||||||
|
encodedXML := r.FormValue("SAMLResponse")
|
||||||
|
relayState := r.FormValue("RelayState")
|
||||||
|
|
||||||
|
relayProps := make(map[string]string)
|
||||||
|
if len(relayState) > 0 {
|
||||||
|
stateStr := ""
|
||||||
|
if b, err := b64.StdEncoding.DecodeString(relayState); err != nil {
|
||||||
|
c.Err = model.NewLocAppError("completeSaml", "api.user.authorize_oauth_user.invalid_state.app_error", nil, err.Error())
|
||||||
|
c.Err.StatusCode = http.StatusFound
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
stateStr = string(b)
|
||||||
|
}
|
||||||
|
relayProps = model.MapFromJson(strings.NewReader(stateStr))
|
||||||
|
}
|
||||||
|
|
||||||
|
if user, err := samlInterface.DoLogin(encodedXML, relayProps); err != nil {
|
||||||
|
c.Err = err
|
||||||
|
c.Err.StatusCode = http.StatusFound
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
if err := checkUserAdditionalAuthenticationCriteria(user, ""); err != nil {
|
||||||
|
c.Err = err
|
||||||
|
c.Err.StatusCode = http.StatusFound
|
||||||
|
return
|
||||||
|
}
|
||||||
|
action := relayProps["action"]
|
||||||
|
switch action {
|
||||||
|
case model.OAUTH_ACTION_SIGNUP:
|
||||||
|
teamId := relayProps["team_id"]
|
||||||
|
go addDirectChannels(teamId, user)
|
||||||
|
break
|
||||||
|
case model.OAUTH_ACTION_EMAIL_TO_SSO:
|
||||||
|
RevokeAllSession(c, user.Id)
|
||||||
|
go sendSignInChangeEmail(c, user.Email, c.GetSiteURL(), strings.Title(model.USER_AUTH_SERVICE_SAML)+" SSO")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
doLogin(c, w, r, user, "")
|
||||||
|
http.Redirect(w, r, GetProtocol(r)+"://"+r.Host, http.StatusFound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -166,5 +166,23 @@
|
|||||||
"DefaultServerLocale": "en",
|
"DefaultServerLocale": "en",
|
||||||
"DefaultClientLocale": "en",
|
"DefaultClientLocale": "en",
|
||||||
"AvailableLocales": ""
|
"AvailableLocales": ""
|
||||||
|
},
|
||||||
|
"SamlSettings": {
|
||||||
|
"Enable": false,
|
||||||
|
"Verify": false,
|
||||||
|
"Encrypt": false,
|
||||||
|
"IdpUrl": "",
|
||||||
|
"IdpDescriptorUrl": "",
|
||||||
|
"AssertionConsumerServiceURL": "",
|
||||||
|
"IdpCertificateFile": "",
|
||||||
|
"PublicCertificateFile": "",
|
||||||
|
"PrivateKeyFile": "",
|
||||||
|
"FirstNameAttribute": "",
|
||||||
|
"LastNameAttribute": "",
|
||||||
|
"EmailAttribute": "",
|
||||||
|
"UsernameAttribute": "",
|
||||||
|
"NicknameAttribute": "",
|
||||||
|
"LocaleAttribute": "",
|
||||||
|
"LoginButtonText": ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
25
einterfaces/saml.go
Обычный файл
25
einterfaces/saml.go
Обычный файл
@@ -0,0 +1,25 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package einterfaces
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mattermost/platform/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SamlInterface interface {
|
||||||
|
ConfigureSP() *model.AppError
|
||||||
|
BuildRequest(relayState string) (*model.SamlAuthRequest, *model.AppError)
|
||||||
|
DoLogin(encodedXML string, relayState map[string]string) (*model.User, *model.AppError)
|
||||||
|
GetMetadata() (string, *model.AppError)
|
||||||
|
}
|
||||||
|
|
||||||
|
var theSamlInterface SamlInterface
|
||||||
|
|
||||||
|
func RegisterSamlInterface(newInterface SamlInterface) {
|
||||||
|
theSamlInterface = newInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetSamlInterface() SamlInterface {
|
||||||
|
return theSamlInterface
|
||||||
|
}
|
||||||
2
glide.lock
сгенерированный
2
glide.lock
сгенерированный
@@ -42,6 +42,8 @@ imports:
|
|||||||
version: 9c19ed558d5df4da88e2ade9c8940d742aef0e7e
|
version: 9c19ed558d5df4da88e2ade9c8940d742aef0e7e
|
||||||
- name: github.com/gorilla/websocket
|
- name: github.com/gorilla/websocket
|
||||||
version: 1f512fc3f05332ba7117626cdfb4e07474e58e60
|
version: 1f512fc3f05332ba7117626cdfb4e07474e58e60
|
||||||
|
- name: github.com/kardianos/osext
|
||||||
|
version: 29ae4ffbc9a6fe9fb2bc5029050ce6996ea1d3bc
|
||||||
- name: github.com/lib/pq
|
- name: github.com/lib/pq
|
||||||
version: ee1442bda7bd1b6a84e913bdb421cb1874ec629d
|
version: ee1442bda7bd1b6a84e913bdb421cb1874ec629d
|
||||||
subpackages:
|
subpackages:
|
||||||
|
|||||||
@@ -39,3 +39,4 @@ import:
|
|||||||
- package: gopkg.in/throttled/throttled.v1
|
- package: gopkg.in/throttled/throttled.v1
|
||||||
subpackages:
|
subpackages:
|
||||||
- store
|
- store
|
||||||
|
- package: github.com/kardianos/osext
|
||||||
|
|||||||
148
i18n/en.json
148
i18n/en.json
@@ -47,6 +47,22 @@
|
|||||||
"id": "September",
|
"id": "September",
|
||||||
"translation": "September"
|
"translation": "September"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.admin.add_certificate.array.app_error",
|
||||||
|
"translation": "Empty array under 'certificate' in request"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.admin.add_certificate.no_file.app_error",
|
||||||
|
"translation": "No file under 'certificate' in request"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.admin.add_certificate.open.app_error",
|
||||||
|
"translation": "Could not open certificate file"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.admin.add_certificate.saving.app_error",
|
||||||
|
"translation": "Could not save certificate file"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.admin.file_read_error",
|
"id": "api.admin.file_read_error",
|
||||||
"translation": "Error reading log file"
|
"translation": "Error reading log file"
|
||||||
@@ -71,6 +87,14 @@
|
|||||||
"id": "api.admin.recycle_db_start.warn",
|
"id": "api.admin.recycle_db_start.warn",
|
||||||
"translation": "Attempting to recycle the database connection"
|
"translation": "Attempting to recycle the database connection"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.admin.remove_certificate.delete.app_error",
|
||||||
|
"translation": "An error occurred while deleting the certificate. Make sure the file config/{{.Filename}} exists."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "api.admin.saml.metadata.app_error",
|
||||||
|
"translation": "An error occurred while building Service Provider Metadata"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.admin.test_email.body",
|
"id": "api.admin.test_email.body",
|
||||||
"translation": "<br/><br/><br/>It appears your Mattermost email is setup correctly!"
|
"translation": "<br/><br/><br/>It appears your Mattermost email is setup correctly!"
|
||||||
@@ -1099,6 +1123,10 @@
|
|||||||
"id": "api.preference.save_preferences.set_details.app_error",
|
"id": "api.preference.save_preferences.set_details.app_error",
|
||||||
"translation": "session.user_id={{.SessionUserId}}, preference.user_id={{.PreferenceUserId}}"
|
"translation": "session.user_id={{.SessionUserId}}, preference.user_id={{.PreferenceUserId}}"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.saml.save_certificate.app_error",
|
||||||
|
"translation": "Certificate did not save properly."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.server.new_server.init.info",
|
"id": "api.server.new_server.init.info",
|
||||||
"translation": "Server is initializing..."
|
"translation": "Server is initializing..."
|
||||||
@@ -1803,6 +1831,10 @@
|
|||||||
"id": "api.user.reset_password.wrong_team.app_error",
|
"id": "api.user.reset_password.wrong_team.app_error",
|
||||||
"translation": "Trying to reset password for user on wrong team."
|
"translation": "Trying to reset password for user on wrong team."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "api.user.saml.not_available.app_error",
|
||||||
|
"translation": "SAML is not configured or supported on this server."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "api.user.send_email_change_email_and_forget.error",
|
"id": "api.user.send_email_change_email_and_forget.error",
|
||||||
"translation": "Failed to send email change notification email successfully err=%v"
|
"translation": "Failed to send email change notification email successfully err=%v"
|
||||||
@@ -2171,6 +2203,74 @@
|
|||||||
"id": "ent.mfa.validate_token.authenticate.app_error",
|
"id": "ent.mfa.validate_token.authenticate.app_error",
|
||||||
"translation": "Error trying to authenticate MFA token"
|
"translation": "Error trying to authenticate MFA token"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.build_request.app_error",
|
||||||
|
"translation": "An error occurred while initiating the request to the Identity Provider. Please contact your System Administrator."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.build_request.encoding.app_error",
|
||||||
|
"translation": "An error occurred while encoding the request for the Identity Provider. Please contact your System Administrator."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.build_request.encoding_signed.app_error",
|
||||||
|
"translation": "An error occurred while encoding the signed request for the Identity Provider. Please contact your System Administrator."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.configure.app_error",
|
||||||
|
"translation": "An error occurred while configuring SAML Service Provider, err=%v"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.configure.encryption_not_enabled.app_error",
|
||||||
|
"translation": "SAML login was unsuccessful because encryption is not enabled. Please contact your System Administrator."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.configure.load_idp_cert.app_error",
|
||||||
|
"translation": "Identity Provider Public Certificate File was not found. Please contact your System Administrator."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.configure.load_private_key.app_error",
|
||||||
|
"translation": "SAML login was unsuccessful because the Service Provider Private Key was not found. Please contact your System Administrator."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.configure.load_public_cert.app_error",
|
||||||
|
"translation": "Service Provider Public Certificate File was not found. Please contact your System Administrator."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.configure.not_encrypted_response.app_error",
|
||||||
|
"translation": "SAML login was unsuccessful as the Identity Provider response is not encrypted. Please contact your System Administrator."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.do_login.decrypt.app_error",
|
||||||
|
"translation": "SAML login was unsuccessful because an error occurred while decrypting the response from the Identity Provider. Please contact your System Administrator."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.do_login.empty_response.app_error",
|
||||||
|
"translation": "We received an empty response from the Identity Provider"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.do_login.parse.app_error",
|
||||||
|
"translation": "An error occurred while parsing the response from the Identity Provider. Please contact your System Administrator."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.do_login.validate.app_error",
|
||||||
|
"translation": "An error occurred while validating the response from the Identity Provider. Please contact your System Administrator."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.license_disable.app_error",
|
||||||
|
"translation": "Your license does not support SAML authentication."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.metadata.app_error",
|
||||||
|
"translation": "An error occurred while building Service Provider Metadata."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.service_disable.app_error",
|
||||||
|
"translation": "SAML is not configured or supported on this server."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "ent.saml.update_saml_user.unable_error",
|
||||||
|
"translation": "Unable to update existing SAML user. Allowing login anyway. err=%v"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "error.generic.link_message",
|
"id": "error.generic.link_message",
|
||||||
"translation": "Back to Mattermost"
|
"translation": "Back to Mattermost"
|
||||||
@@ -2571,6 +2671,46 @@
|
|||||||
"id": "model.config.is_valid.restrict_direct_message.app_error",
|
"id": "model.config.is_valid.restrict_direct_message.app_error",
|
||||||
"translation": "Invalid direct message restriction. Must be 'any', or 'team'"
|
"translation": "Invalid direct message restriction. Must be 'any', or 'team'"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.saml_assertion_consumer_service_url.app_error",
|
||||||
|
"translation": "Service Provider Login URL must be a valid URL and start with http:// or https://."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.saml_email_attribute.app_error",
|
||||||
|
"translation": "Invalid Email attribute. Must be set."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.saml_first_name_attribute.app_error",
|
||||||
|
"translation": "Invalid First Name attribute. Must be set."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.saml_idp_cert.app_error",
|
||||||
|
"translation": "Identity Provider Public Certificate missing. Did you forget to upload it?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.saml_idp_descriptor_url.app_error",
|
||||||
|
"translation": "Identity Provider Issuer URL must be a valid URL and start with http:// or https://."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.saml_idp_url.app_error",
|
||||||
|
"translation": "SAML SSO URL must be a valid URL and start with http:// or https://."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.saml_last_name_attribute.app_error",
|
||||||
|
"translation": "Invalid Last Name attribute. Must be set."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.saml_private_key.app_error",
|
||||||
|
"translation": "Service Provider Private Key missing. Did you forget to upload it?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.saml_public_cert.app_error",
|
||||||
|
"translation": "Service Provider Public Certificate missing. Did you forget to upload it?"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "model.config.is_valid.saml_username_attribute.app_error",
|
||||||
|
"translation": "Invalid Username attribute. Must be set."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "model.config.is_valid.sql_data_src.app_error",
|
"id": "model.config.is_valid.sql_data_src.app_error",
|
||||||
"translation": "Invalid data source for SQL settings. Must be set."
|
"translation": "Invalid data source for SQL settings. Must be set."
|
||||||
@@ -3795,6 +3935,10 @@
|
|||||||
"id": "store.sql_user.save.email_exists.ldap_app_error",
|
"id": "store.sql_user.save.email_exists.ldap_app_error",
|
||||||
"translation": "This account does not use LDAP authentication. Please sign in using email and password."
|
"translation": "This account does not use LDAP authentication. Please sign in using email and password."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_user.save.email_exists.saml_app_error",
|
||||||
|
"translation": "This account does not use SAML authentication. Please sign in using email and password."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql_user.save.existing.app_error",
|
"id": "store.sql_user.save.existing.app_error",
|
||||||
"translation": "Must call update for exisiting user"
|
"translation": "Must call update for exisiting user"
|
||||||
@@ -3815,6 +3959,10 @@
|
|||||||
"id": "store.sql_user.save.username_exists.ldap_app_error",
|
"id": "store.sql_user.save.username_exists.ldap_app_error",
|
||||||
"translation": "An account with that username already exists. Please contact your Administrator."
|
"translation": "An account with that username already exists. Please contact your Administrator."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "store.sql_user.save.username_exists.saml_app_error",
|
||||||
|
"translation": "An account with that username already exists. Please contact your Administrator."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "store.sql_user.update.app_error",
|
"id": "store.sql_user.update.app_error",
|
||||||
"translation": "We couldn't update the account"
|
"translation": "We couldn't update the account"
|
||||||
|
|||||||
156
model/config.go
156
model/config.go
@@ -227,6 +227,31 @@ type LocalizationSettings struct {
|
|||||||
AvailableLocales *string
|
AvailableLocales *string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SamlSettings struct {
|
||||||
|
// Basic
|
||||||
|
Enable *bool
|
||||||
|
Verify *bool
|
||||||
|
Encrypt *bool
|
||||||
|
|
||||||
|
IdpUrl *string
|
||||||
|
IdpDescriptorUrl *string
|
||||||
|
AssertionConsumerServiceURL *string
|
||||||
|
|
||||||
|
IdpCertificateFile *string
|
||||||
|
PublicCertificateFile *string
|
||||||
|
PrivateKeyFile *string
|
||||||
|
|
||||||
|
// User Mapping
|
||||||
|
FirstNameAttribute *string
|
||||||
|
LastNameAttribute *string
|
||||||
|
EmailAttribute *string
|
||||||
|
UsernameAttribute *string
|
||||||
|
NicknameAttribute *string
|
||||||
|
LocaleAttribute *string
|
||||||
|
|
||||||
|
LoginButtonText *string
|
||||||
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ServiceSettings ServiceSettings
|
ServiceSettings ServiceSettings
|
||||||
TeamSettings TeamSettings
|
TeamSettings TeamSettings
|
||||||
@@ -242,6 +267,7 @@ type Config struct {
|
|||||||
LdapSettings LdapSettings
|
LdapSettings LdapSettings
|
||||||
ComplianceSettings ComplianceSettings
|
ComplianceSettings ComplianceSettings
|
||||||
LocalizationSettings LocalizationSettings
|
LocalizationSettings LocalizationSettings
|
||||||
|
SamlSettings SamlSettings
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Config) ToJson() string {
|
func (o *Config) ToJson() string {
|
||||||
@@ -627,6 +653,86 @@ func (o *Config) SetDefaults() {
|
|||||||
o.LocalizationSettings.AvailableLocales = new(string)
|
o.LocalizationSettings.AvailableLocales = new(string)
|
||||||
*o.LocalizationSettings.AvailableLocales = ""
|
*o.LocalizationSettings.AvailableLocales = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.Enable == nil {
|
||||||
|
o.SamlSettings.Enable = new(bool)
|
||||||
|
*o.SamlSettings.Enable = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.Verify == nil {
|
||||||
|
o.SamlSettings.Verify = new(bool)
|
||||||
|
*o.SamlSettings.Verify = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.Encrypt == nil {
|
||||||
|
o.SamlSettings.Encrypt = new(bool)
|
||||||
|
*o.SamlSettings.Encrypt = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.IdpUrl == nil {
|
||||||
|
o.SamlSettings.IdpUrl = new(string)
|
||||||
|
*o.SamlSettings.IdpUrl = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.IdpDescriptorUrl == nil {
|
||||||
|
o.SamlSettings.IdpDescriptorUrl = new(string)
|
||||||
|
*o.SamlSettings.IdpDescriptorUrl = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.IdpCertificateFile == nil {
|
||||||
|
o.SamlSettings.IdpCertificateFile = new(string)
|
||||||
|
*o.SamlSettings.IdpCertificateFile = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.PublicCertificateFile == nil {
|
||||||
|
o.SamlSettings.PublicCertificateFile = new(string)
|
||||||
|
*o.SamlSettings.PublicCertificateFile = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.PrivateKeyFile == nil {
|
||||||
|
o.SamlSettings.PrivateKeyFile = new(string)
|
||||||
|
*o.SamlSettings.PrivateKeyFile = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.AssertionConsumerServiceURL == nil {
|
||||||
|
o.SamlSettings.AssertionConsumerServiceURL = new(string)
|
||||||
|
*o.SamlSettings.AssertionConsumerServiceURL = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.LoginButtonText == nil || *o.SamlSettings.LoginButtonText == "" {
|
||||||
|
o.SamlSettings.LoginButtonText = new(string)
|
||||||
|
*o.SamlSettings.LoginButtonText = USER_AUTH_SERVICE_SAML_TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.FirstNameAttribute == nil {
|
||||||
|
o.SamlSettings.FirstNameAttribute = new(string)
|
||||||
|
*o.SamlSettings.FirstNameAttribute = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.LastNameAttribute == nil {
|
||||||
|
o.SamlSettings.LastNameAttribute = new(string)
|
||||||
|
*o.SamlSettings.LastNameAttribute = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.EmailAttribute == nil {
|
||||||
|
o.SamlSettings.EmailAttribute = new(string)
|
||||||
|
*o.SamlSettings.EmailAttribute = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.UsernameAttribute == nil {
|
||||||
|
o.SamlSettings.UsernameAttribute = new(string)
|
||||||
|
*o.SamlSettings.UsernameAttribute = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.NicknameAttribute == nil {
|
||||||
|
o.SamlSettings.NicknameAttribute = new(string)
|
||||||
|
*o.SamlSettings.NicknameAttribute = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if o.SamlSettings.LocaleAttribute == nil {
|
||||||
|
o.SamlSettings.LocaleAttribute = new(string)
|
||||||
|
*o.SamlSettings.LocaleAttribute = ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *Config) IsValid() *AppError {
|
func (o *Config) IsValid() *AppError {
|
||||||
@@ -749,6 +855,56 @@ func (o *Config) IsValid() *AppError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *o.SamlSettings.Enable {
|
||||||
|
if len(*o.SamlSettings.IdpUrl) == 0 {
|
||||||
|
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_idp_url.app_error", nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*o.SamlSettings.IdpDescriptorUrl) == 0 || !IsValidHttpUrl(*o.SamlSettings.IdpDescriptorUrl) {
|
||||||
|
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_idp_descriptor_url.app_error", nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*o.SamlSettings.IdpCertificateFile) == 0 {
|
||||||
|
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_idp_cert.app_error", nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*o.SamlSettings.EmailAttribute) == 0 {
|
||||||
|
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*o.SamlSettings.UsernameAttribute) == 0 {
|
||||||
|
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_username_attribute.app_error", nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*o.SamlSettings.FirstNameAttribute) == 0 {
|
||||||
|
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_first_name_attribute.app_error", nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*o.SamlSettings.LastNameAttribute) == 0 {
|
||||||
|
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_last_name_attribute.app_error", nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.SamlSettings.Verify {
|
||||||
|
if len(*o.SamlSettings.AssertionConsumerServiceURL) == 0 || !IsValidHttpUrl(*o.SamlSettings.AssertionConsumerServiceURL) {
|
||||||
|
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_assertion_consumer_service_url.app_error", nil, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *o.SamlSettings.Encrypt {
|
||||||
|
if len(*o.SamlSettings.PrivateKeyFile) == 0 {
|
||||||
|
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_private_key.app_error", nil, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*o.SamlSettings.PublicCertificateFile) == 0 {
|
||||||
|
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_public_cert.app_error", nil, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(*o.SamlSettings.EmailAttribute) == 0 {
|
||||||
|
return NewLocAppError("Config.IsValid", "model.config.is_valid.saml_email_attribute.app_error", nil, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ type Features struct {
|
|||||||
Compliance *bool `json:"compliance"`
|
Compliance *bool `json:"compliance"`
|
||||||
CustomBrand *bool `json:"custom_brand"`
|
CustomBrand *bool `json:"custom_brand"`
|
||||||
MHPNS *bool `json:"mhpns"`
|
MHPNS *bool `json:"mhpns"`
|
||||||
|
SAML *bool `json:"saml"`
|
||||||
FutureFeatures *bool `json:"future_features"`
|
FutureFeatures *bool `json:"future_features"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +83,11 @@ func (f *Features) SetDefaults() {
|
|||||||
f.MHPNS = new(bool)
|
f.MHPNS = new(bool)
|
||||||
*f.MHPNS = *f.FutureFeatures
|
*f.MHPNS = *f.FutureFeatures
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if f.SAML == nil {
|
||||||
|
f.SAML = new(bool)
|
||||||
|
*f.SAML = *f.FutureFeatures
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *License) IsExpired() bool {
|
func (l *License) IsExpired() bool {
|
||||||
|
|||||||
18
model/saml.go
Обычный файл
18
model/saml.go
Обычный файл
@@ -0,0 +1,18 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
package model
|
||||||
|
|
||||||
|
const (
|
||||||
|
USER_AUTH_SERVICE_SAML = "saml"
|
||||||
|
USER_AUTH_SERVICE_SAML_TEXT = "With SAML"
|
||||||
|
SAML_IDP_CERTIFICATE = 1
|
||||||
|
SAML_PRIVATE_KEY = 2
|
||||||
|
SAML_PUBLIC_CERT = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
type SamlAuthRequest struct {
|
||||||
|
Base64AuthRequest string
|
||||||
|
URL string
|
||||||
|
RelayState string
|
||||||
|
}
|
||||||
@@ -192,6 +192,10 @@ func LoadConfig(fileName string) {
|
|||||||
// This restarts the job if nessisary (works for config reloads)
|
// This restarts the job if nessisary (works for config reloads)
|
||||||
ldapI.StartLdapSyncJob()
|
ldapI.StartLdapSyncJob()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if samlI := einterfaces.GetSamlInterface(); samlI != nil {
|
||||||
|
samlI.ConfigureSP()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getClientConfig(c *model.Config) map[string]string {
|
func getClientConfig(c *model.Config) map[string]string {
|
||||||
@@ -277,6 +281,11 @@ func getClientConfig(c *model.Config) map[string]string {
|
|||||||
if *License.Features.Compliance {
|
if *License.Features.Compliance {
|
||||||
props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable)
|
props["EnableCompliance"] = strconv.FormatBool(*c.ComplianceSettings.Enable)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *License.Features.SAML {
|
||||||
|
props["EnableSaml"] = strconv.FormatBool(*c.SamlSettings.Enable)
|
||||||
|
props["SamlLoginButtonText"] = *c.SamlSettings.LoginButtonText
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return props
|
return props
|
||||||
|
|||||||
@@ -121,6 +121,7 @@ func getClientLicense(l *model.License) map[string]string {
|
|||||||
props["Users"] = strconv.Itoa(*l.Features.Users)
|
props["Users"] = strconv.Itoa(*l.Features.Users)
|
||||||
props["LDAP"] = strconv.FormatBool(*l.Features.LDAP)
|
props["LDAP"] = strconv.FormatBool(*l.Features.LDAP)
|
||||||
props["MFA"] = strconv.FormatBool(*l.Features.MFA)
|
props["MFA"] = strconv.FormatBool(*l.Features.MFA)
|
||||||
|
props["SAML"] = strconv.FormatBool(*l.Features.SAML)
|
||||||
props["GoogleSSO"] = strconv.FormatBool(*l.Features.GoogleSSO)
|
props["GoogleSSO"] = strconv.FormatBool(*l.Features.GoogleSSO)
|
||||||
props["Compliance"] = strconv.FormatBool(*l.Features.Compliance)
|
props["Compliance"] = strconv.FormatBool(*l.Features.Compliance)
|
||||||
props["CustomBrand"] = strconv.FormatBool(*l.Features.CustomBrand)
|
props["CustomBrand"] = strconv.FormatBool(*l.Features.CustomBrand)
|
||||||
|
|||||||
27
vendor/github.com/kardianos/osext/LICENSE
сгенерированный
поставляемый
Обычный файл
27
vendor/github.com/kardianos/osext/LICENSE
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,27 @@
|
|||||||
|
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||||
|
|
||||||
|
Redistribution and use in source and binary forms, with or without
|
||||||
|
modification, are permitted provided that the following conditions are
|
||||||
|
met:
|
||||||
|
|
||||||
|
* Redistributions of source code must retain the above copyright
|
||||||
|
notice, this list of conditions and the following disclaimer.
|
||||||
|
* Redistributions in binary form must reproduce the above
|
||||||
|
copyright notice, this list of conditions and the following disclaimer
|
||||||
|
in the documentation and/or other materials provided with the
|
||||||
|
distribution.
|
||||||
|
* Neither the name of Google Inc. nor the names of its
|
||||||
|
contributors may be used to endorse or promote products derived from
|
||||||
|
this software without specific prior written permission.
|
||||||
|
|
||||||
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
|
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||||
|
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||||
|
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||||
|
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||||
|
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||||
|
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||||
|
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||||
16
vendor/github.com/kardianos/osext/README.md
сгенерированный
поставляемый
Обычный файл
16
vendor/github.com/kardianos/osext/README.md
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,16 @@
|
|||||||
|
### Extensions to the "os" package.
|
||||||
|
|
||||||
|
## Find the current Executable and ExecutableFolder.
|
||||||
|
|
||||||
|
There is sometimes utility in finding the current executable file
|
||||||
|
that is running. This can be used for upgrading the current executable
|
||||||
|
or finding resources located relative to the executable file. Both
|
||||||
|
working directory and the os.Args[0] value are arbitrary and cannot
|
||||||
|
be relied on; os.Args[0] can be "faked".
|
||||||
|
|
||||||
|
Multi-platform and supports:
|
||||||
|
* Linux
|
||||||
|
* OS X
|
||||||
|
* Windows
|
||||||
|
* Plan 9
|
||||||
|
* BSDs.
|
||||||
33
vendor/github.com/kardianos/osext/osext.go
сгенерированный
поставляемый
Обычный файл
33
vendor/github.com/kardianos/osext/osext.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// Extensions to the standard "os" package.
|
||||||
|
package osext // import "github.com/kardianos/osext"
|
||||||
|
|
||||||
|
import "path/filepath"
|
||||||
|
|
||||||
|
var cx, ce = executableClean()
|
||||||
|
|
||||||
|
func executableClean() (string, error) {
|
||||||
|
p, err := executable()
|
||||||
|
return filepath.Clean(p), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Executable returns an absolute path that can be used to
|
||||||
|
// re-invoke the current program.
|
||||||
|
// It may not be valid after the current program exits.
|
||||||
|
func Executable() (string, error) {
|
||||||
|
return cx, ce
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns same path as Executable, returns just the folder
|
||||||
|
// path. Excludes the executable name and any trailing slash.
|
||||||
|
func ExecutableFolder() (string, error) {
|
||||||
|
p, err := Executable()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Dir(p), nil
|
||||||
|
}
|
||||||
20
vendor/github.com/kardianos/osext/osext_plan9.go
сгенерированный
поставляемый
Обычный файл
20
vendor/github.com/kardianos/osext/osext_plan9.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package osext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
func executable() (string, error) {
|
||||||
|
f, err := os.Open("/proc/" + strconv.Itoa(os.Getpid()) + "/text")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return syscall.Fd2path(int(f.Fd()))
|
||||||
|
}
|
||||||
36
vendor/github.com/kardianos/osext/osext_procfs.go
сгенерированный
поставляемый
Обычный файл
36
vendor/github.com/kardianos/osext/osext_procfs.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build linux netbsd solaris dragonfly
|
||||||
|
|
||||||
|
package osext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func executable() (string, error) {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "linux":
|
||||||
|
const deletedTag = " (deleted)"
|
||||||
|
execpath, err := os.Readlink("/proc/self/exe")
|
||||||
|
if err != nil {
|
||||||
|
return execpath, err
|
||||||
|
}
|
||||||
|
execpath = strings.TrimSuffix(execpath, deletedTag)
|
||||||
|
execpath = strings.TrimPrefix(execpath, deletedTag)
|
||||||
|
return execpath, nil
|
||||||
|
case "netbsd":
|
||||||
|
return os.Readlink("/proc/curproc/exe")
|
||||||
|
case "dragonfly":
|
||||||
|
return os.Readlink("/proc/curproc/file")
|
||||||
|
case "solaris":
|
||||||
|
return os.Readlink(fmt.Sprintf("/proc/%d/path/a.out", os.Getpid()))
|
||||||
|
}
|
||||||
|
return "", errors.New("ExecPath not implemented for " + runtime.GOOS)
|
||||||
|
}
|
||||||
126
vendor/github.com/kardianos/osext/osext_sysctl.go
сгенерированный
поставляемый
Обычный файл
126
vendor/github.com/kardianos/osext/osext_sysctl.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,126 @@
|
|||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build darwin freebsd openbsd
|
||||||
|
|
||||||
|
package osext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var initCwd, initCwdErr = os.Getwd()
|
||||||
|
|
||||||
|
func executable() (string, error) {
|
||||||
|
var mib [4]int32
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "freebsd":
|
||||||
|
mib = [4]int32{1 /* CTL_KERN */, 14 /* KERN_PROC */, 12 /* KERN_PROC_PATHNAME */, -1}
|
||||||
|
case "darwin":
|
||||||
|
mib = [4]int32{1 /* CTL_KERN */, 38 /* KERN_PROCARGS */, int32(os.Getpid()), -1}
|
||||||
|
case "openbsd":
|
||||||
|
mib = [4]int32{1 /* CTL_KERN */, 55 /* KERN_PROC_ARGS */, int32(os.Getpid()), 1 /* KERN_PROC_ARGV */}
|
||||||
|
}
|
||||||
|
|
||||||
|
n := uintptr(0)
|
||||||
|
// Get length.
|
||||||
|
_, _, errNum := syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, 0, uintptr(unsafe.Pointer(&n)), 0, 0)
|
||||||
|
if errNum != 0 {
|
||||||
|
return "", errNum
|
||||||
|
}
|
||||||
|
if n == 0 { // This shouldn't happen.
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
buf := make([]byte, n)
|
||||||
|
_, _, errNum = syscall.Syscall6(syscall.SYS___SYSCTL, uintptr(unsafe.Pointer(&mib[0])), 4, uintptr(unsafe.Pointer(&buf[0])), uintptr(unsafe.Pointer(&n)), 0, 0)
|
||||||
|
if errNum != 0 {
|
||||||
|
return "", errNum
|
||||||
|
}
|
||||||
|
if n == 0 { // This shouldn't happen.
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var execPath string
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "openbsd":
|
||||||
|
// buf now contains **argv, with pointers to each of the C-style
|
||||||
|
// NULL terminated arguments.
|
||||||
|
var args []string
|
||||||
|
argv := uintptr(unsafe.Pointer(&buf[0]))
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
argp := *(**[1 << 20]byte)(unsafe.Pointer(argv))
|
||||||
|
if argp == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for i := 0; uintptr(i) < n; i++ {
|
||||||
|
// we don't want the full arguments list
|
||||||
|
if string(argp[i]) == " " {
|
||||||
|
break Loop
|
||||||
|
}
|
||||||
|
if argp[i] != 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
args = append(args, string(argp[:i]))
|
||||||
|
n -= uintptr(i)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if n < unsafe.Sizeof(argv) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
argv += unsafe.Sizeof(argv)
|
||||||
|
n -= unsafe.Sizeof(argv)
|
||||||
|
}
|
||||||
|
execPath = args[0]
|
||||||
|
// There is no canonical way to get an executable path on
|
||||||
|
// OpenBSD, so check PATH in case we are called directly
|
||||||
|
if execPath[0] != '/' && execPath[0] != '.' {
|
||||||
|
execIsInPath, err := exec.LookPath(execPath)
|
||||||
|
if err == nil {
|
||||||
|
execPath = execIsInPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
for i, v := range buf {
|
||||||
|
if v == 0 {
|
||||||
|
buf = buf[:i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
execPath = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
// execPath will not be empty due to above checks.
|
||||||
|
// Try to get the absolute path if the execPath is not rooted.
|
||||||
|
if execPath[0] != '/' {
|
||||||
|
execPath, err = getAbs(execPath)
|
||||||
|
if err != nil {
|
||||||
|
return execPath, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// For darwin KERN_PROCARGS may return the path to a symlink rather than the
|
||||||
|
// actual executable.
|
||||||
|
if runtime.GOOS == "darwin" {
|
||||||
|
if execPath, err = filepath.EvalSymlinks(execPath); err != nil {
|
||||||
|
return execPath, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return execPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAbs(execPath string) (string, error) {
|
||||||
|
if initCwdErr != nil {
|
||||||
|
return execPath, initCwdErr
|
||||||
|
}
|
||||||
|
// The execPath may begin with a "../" or a "./" so clean it first.
|
||||||
|
// Join the two paths, trailing and starting slashes undetermined, so use
|
||||||
|
// the generic Join function.
|
||||||
|
return filepath.Join(initCwd, filepath.Clean(execPath)), nil
|
||||||
|
}
|
||||||
203
vendor/github.com/kardianos/osext/osext_test.go
сгенерированный
поставляемый
Обычный файл
203
vendor/github.com/kardianos/osext/osext_test.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,203 @@
|
|||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
// +build darwin linux freebsd netbsd windows openbsd
|
||||||
|
|
||||||
|
package osext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
executableEnvVar = "OSTEST_OUTPUT_EXECUTABLE"
|
||||||
|
|
||||||
|
executableEnvValueMatch = "match"
|
||||||
|
executableEnvValueDelete = "delete"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPrintExecutable(t *testing.T) {
|
||||||
|
ef, err := Executable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Executable failed: %v", err)
|
||||||
|
}
|
||||||
|
t.Log("Executable:", ef)
|
||||||
|
}
|
||||||
|
func TestPrintExecutableFolder(t *testing.T) {
|
||||||
|
ef, err := ExecutableFolder()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExecutableFolder failed: %v", err)
|
||||||
|
}
|
||||||
|
t.Log("Executable Folder:", ef)
|
||||||
|
}
|
||||||
|
func TestExecutableFolder(t *testing.T) {
|
||||||
|
ef, err := ExecutableFolder()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ExecutableFolder failed: %v", err)
|
||||||
|
}
|
||||||
|
if ef[len(ef)-1] == filepath.Separator {
|
||||||
|
t.Fatal("ExecutableFolder ends with a trailing slash.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func TestExecutableMatch(t *testing.T) {
|
||||||
|
ep, err := Executable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Executable failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fullpath to be of the form "dir/prog".
|
||||||
|
dir := filepath.Dir(filepath.Dir(ep))
|
||||||
|
fullpath, err := filepath.Rel(dir, ep)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("filepath.Rel: %v", err)
|
||||||
|
}
|
||||||
|
// Make child start with a relative program path.
|
||||||
|
// Alter argv[0] for child to verify getting real path without argv[0].
|
||||||
|
cmd := &exec.Cmd{
|
||||||
|
Dir: dir,
|
||||||
|
Path: fullpath,
|
||||||
|
Env: []string{fmt.Sprintf("%s=%s", executableEnvVar, executableEnvValueMatch)},
|
||||||
|
}
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("exec(self) failed: %v", err)
|
||||||
|
}
|
||||||
|
outs := string(out)
|
||||||
|
if !filepath.IsAbs(outs) {
|
||||||
|
t.Fatalf("Child returned %q, want an absolute path", out)
|
||||||
|
}
|
||||||
|
if !sameFile(outs, ep) {
|
||||||
|
t.Fatalf("Child returned %q, not the same file as %q", out, ep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExecutableDelete(t *testing.T) {
|
||||||
|
if runtime.GOOS != "linux" {
|
||||||
|
t.Skip()
|
||||||
|
}
|
||||||
|
fpath, err := Executable()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Executable failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, w := io.Pipe()
|
||||||
|
stderrBuff := &bytes.Buffer{}
|
||||||
|
stdoutBuff := &bytes.Buffer{}
|
||||||
|
cmd := &exec.Cmd{
|
||||||
|
Path: fpath,
|
||||||
|
Env: []string{fmt.Sprintf("%s=%s", executableEnvVar, executableEnvValueDelete)},
|
||||||
|
Stdin: r,
|
||||||
|
Stderr: stderrBuff,
|
||||||
|
Stdout: stdoutBuff,
|
||||||
|
}
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("exec(self) start failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tempPath := fpath + "_copy"
|
||||||
|
_ = os.Remove(tempPath)
|
||||||
|
|
||||||
|
err = copyFile(tempPath, fpath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("copy file failed: %v", err)
|
||||||
|
}
|
||||||
|
err = os.Remove(fpath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("remove running test file failed: %v", err)
|
||||||
|
}
|
||||||
|
err = os.Rename(tempPath, fpath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("rename copy to previous name failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write([]byte{0})
|
||||||
|
w.Close()
|
||||||
|
|
||||||
|
err = cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("exec wait failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
childPath := stderrBuff.String()
|
||||||
|
if !filepath.IsAbs(childPath) {
|
||||||
|
t.Fatalf("Child returned %q, want an absolute path", childPath)
|
||||||
|
}
|
||||||
|
if !sameFile(childPath, fpath) {
|
||||||
|
t.Fatalf("Child returned %q, not the same file as %q", childPath, fpath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameFile(fn1, fn2 string) bool {
|
||||||
|
fi1, err := os.Stat(fn1)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
fi2, err := os.Stat(fn2)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return os.SameFile(fi1, fi2)
|
||||||
|
}
|
||||||
|
func copyFile(dest, src string) error {
|
||||||
|
df, err := os.Create(dest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer df.Close()
|
||||||
|
|
||||||
|
sf, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer sf.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(df, sf)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
env := os.Getenv(executableEnvVar)
|
||||||
|
switch env {
|
||||||
|
case "":
|
||||||
|
os.Exit(m.Run())
|
||||||
|
case executableEnvValueMatch:
|
||||||
|
// First chdir to another path.
|
||||||
|
dir := "/"
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
dir = filepath.VolumeName(".")
|
||||||
|
}
|
||||||
|
os.Chdir(dir)
|
||||||
|
if ep, err := Executable(); err != nil {
|
||||||
|
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(os.Stderr, ep)
|
||||||
|
}
|
||||||
|
case executableEnvValueDelete:
|
||||||
|
bb := make([]byte, 1)
|
||||||
|
var err error
|
||||||
|
n, err := os.Stdin.Read(bb)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
if n != 1 {
|
||||||
|
fmt.Fprint(os.Stderr, "ERROR: n != 1, n == ", n)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
if ep, err := Executable(); err != nil {
|
||||||
|
fmt.Fprint(os.Stderr, "ERROR: ", err)
|
||||||
|
} else {
|
||||||
|
fmt.Fprint(os.Stderr, ep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
os.Exit(0)
|
||||||
|
}
|
||||||
34
vendor/github.com/kardianos/osext/osext_windows.go
сгенерированный
поставляемый
Обычный файл
34
vendor/github.com/kardianos/osext/osext_windows.go
сгенерированный
поставляемый
Обычный файл
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright 2012 The Go Authors. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package osext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel = syscall.MustLoadDLL("kernel32.dll")
|
||||||
|
getModuleFileNameProc = kernel.MustFindProc("GetModuleFileNameW")
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetModuleFileName() with hModule = NULL
|
||||||
|
func executable() (exePath string, err error) {
|
||||||
|
return getModuleFileName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getModuleFileName() (string, error) {
|
||||||
|
var n uint32
|
||||||
|
b := make([]uint16, syscall.MAX_PATH)
|
||||||
|
size := uint32(len(b))
|
||||||
|
|
||||||
|
r0, _, e1 := getModuleFileNameProc.Call(0, uintptr(unsafe.Pointer(&b[0])), uintptr(size))
|
||||||
|
n = uint32(r0)
|
||||||
|
if n == 0 {
|
||||||
|
return "", e1
|
||||||
|
}
|
||||||
|
return string(utf16.Decode(b[0:n])), nil
|
||||||
|
}
|
||||||
@@ -176,6 +176,7 @@ export default class AdminSidebar extends React.Component {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
let ldapSettings = null;
|
let ldapSettings = null;
|
||||||
|
let samlSettings = null;
|
||||||
let complianceSettings = null;
|
let complianceSettings = null;
|
||||||
|
|
||||||
let license = null;
|
let license = null;
|
||||||
@@ -198,6 +199,20 @@ export default class AdminSidebar extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (global.window.mm_license.SAML === 'true') {
|
||||||
|
samlSettings = (
|
||||||
|
<AdminSidebarSection
|
||||||
|
name='saml'
|
||||||
|
title={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.sidebar.saml'
|
||||||
|
defaultMessage='SAML'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (global.window.mm_license.Compliance === 'true') {
|
if (global.window.mm_license.Compliance === 'true') {
|
||||||
complianceSettings = (
|
complianceSettings = (
|
||||||
<AdminSidebarSection
|
<AdminSidebarSection
|
||||||
@@ -391,6 +406,7 @@ export default class AdminSidebar extends React.Component {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{ldapSettings}
|
{ldapSettings}
|
||||||
|
{samlSettings}
|
||||||
</AdminSidebarSection>
|
</AdminSidebarSection>
|
||||||
<AdminSidebarSection
|
<AdminSidebarSection
|
||||||
name='security'
|
name='security'
|
||||||
|
|||||||
124
webapp/components/admin_console/file_upload_setting.jsx
Обычный файл
124
webapp/components/admin_console/file_upload_setting.jsx
Обычный файл
@@ -0,0 +1,124 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import $ from 'jquery';
|
||||||
|
import React from 'react';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import Setting from './setting.jsx';
|
||||||
|
|
||||||
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
|
||||||
|
export default class FileUploadSetting extends Setting {
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
id: React.PropTypes.string.isRequired,
|
||||||
|
label: React.PropTypes.node.isRequired,
|
||||||
|
helpText: React.PropTypes.node,
|
||||||
|
uploadingText: React.PropTypes.node,
|
||||||
|
onSubmit: React.PropTypes.func.isRequired,
|
||||||
|
disabled: React.PropTypes.bool,
|
||||||
|
fileType: React.PropTypes.string.isRequired
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.handleChange = this.handleChange.bind(this);
|
||||||
|
this.handleSubmit = this.handleSubmit.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
fileName: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleChange() {
|
||||||
|
const files = this.refs.fileInput.files;
|
||||||
|
if (files && files.length > 0) {
|
||||||
|
this.setState({fileSelected: true, fileName: files[0].name});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleSubmit(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
$(this.refs.upload_button).button('loading');
|
||||||
|
this.props.onSubmit(this.props.id, this.refs.fileInput.files[0], (error) => {
|
||||||
|
$(this.refs.upload_button).button('reset');
|
||||||
|
if (error) {
|
||||||
|
Utils.clearFileInput(this.refs.fileInput);
|
||||||
|
}
|
||||||
|
this.setState({fileSelected: false, fileName: null, serverError: error});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let serverError;
|
||||||
|
if (this.state.serverError) {
|
||||||
|
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
var btnClass = 'btn';
|
||||||
|
if (this.state.fileSelected) {
|
||||||
|
btnClass = 'btn btn-primary';
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileName;
|
||||||
|
if (this.state.fileName) {
|
||||||
|
fileName = this.state.fileName;
|
||||||
|
} else {
|
||||||
|
fileName = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.file_upload.noFile'
|
||||||
|
defaultMessage='No file uploaded'
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Setting
|
||||||
|
label={this.props.label}
|
||||||
|
helpText={this.props.helpText}
|
||||||
|
inputId={this.props.id}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className='file__upload'>
|
||||||
|
<button
|
||||||
|
className='btn btn-default'
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.file_upload.chooseFile'
|
||||||
|
defaultMessage='Choose File'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref='fileInput'
|
||||||
|
type='file'
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
accept={this.props.fileType}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className={btnClass}
|
||||||
|
disabled={!this.state.fileSelected}
|
||||||
|
onClick={this.handleSubmit}
|
||||||
|
ref='upload_button'
|
||||||
|
data-loading-text={`<span class=\'glyphicon glyphicon-refresh glyphicon-refresh-animate\'></span> ${this.props.uploadingText}`}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.file_upload.uploadFile'
|
||||||
|
defaultMessage='Upload'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
<div className='help-text no-margin'>
|
||||||
|
{fileName}
|
||||||
|
</div>
|
||||||
|
{serverError}
|
||||||
|
</div>
|
||||||
|
</Setting>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
webapp/components/admin_console/remove_file_setting.jsx
Обычный файл
72
webapp/components/admin_console/remove_file_setting.jsx
Обычный файл
@@ -0,0 +1,72 @@
|
|||||||
|
// Copyright (c) 2016 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import $ from 'jquery';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import Setting from './setting.jsx';
|
||||||
|
|
||||||
|
export default class RemoveFileSetting extends Setting {
|
||||||
|
static get propTypes() {
|
||||||
|
return {
|
||||||
|
id: React.PropTypes.string.isRequired,
|
||||||
|
label: React.PropTypes.node.isRequired,
|
||||||
|
helpText: React.PropTypes.node,
|
||||||
|
removeButtonText: React.PropTypes.node.isRequired,
|
||||||
|
removingText: React.PropTypes.node,
|
||||||
|
fileName: React.PropTypes.string.isRequired,
|
||||||
|
onSubmit: React.PropTypes.func.isRequired,
|
||||||
|
disabled: React.PropTypes.bool
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.handleRemove = this.handleRemove.bind(this);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
serverError: null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
handleRemove(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
$(this.refs.remove_button).button('loading');
|
||||||
|
this.props.onSubmit(this.props.id, (error) => {
|
||||||
|
$(this.refs.remove_button).button('reset');
|
||||||
|
this.setState({serverError: error});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let serverError;
|
||||||
|
if (this.state.serverError) {
|
||||||
|
serverError = <div className='form-group has-error'><label className='control-label'>{this.state.serverError}</label></div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Setting
|
||||||
|
label={this.props.label}
|
||||||
|
helpText={this.props.helpText}
|
||||||
|
inputId={this.props.id}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className='help-text remove-filename'>
|
||||||
|
{this.props.fileName}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className='btn btn-danger'
|
||||||
|
onClick={this.handleRemove}
|
||||||
|
ref='remove_button'
|
||||||
|
disabled={this.props.disabled}
|
||||||
|
data-loading-text={`<span class='glyphicon glyphicon-refresh glyphicon-refresh-animate'></span> ${this.props.removingText}`}
|
||||||
|
>
|
||||||
|
{this.props.removeButtonText}
|
||||||
|
</button>
|
||||||
|
{serverError}
|
||||||
|
</div>
|
||||||
|
</Setting>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
518
webapp/components/admin_console/saml_settings.jsx
Обычный файл
518
webapp/components/admin_console/saml_settings.jsx
Обычный файл
@@ -0,0 +1,518 @@
|
|||||||
|
// Copyright (c) 2015 Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See License.txt for license information.
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import AdminSettings from './admin_settings.jsx';
|
||||||
|
import BooleanSetting from './boolean_setting.jsx';
|
||||||
|
import TextSetting from './text_setting.jsx';
|
||||||
|
import FileUploadSetting from './file_upload_setting.jsx';
|
||||||
|
import RemoveFileSetting from './remove_file_setting.jsx';
|
||||||
|
|
||||||
|
import {FormattedMessage, FormattedHTMLMessage} from 'react-intl';
|
||||||
|
import SettingsGroup from './settings_group.jsx';
|
||||||
|
|
||||||
|
import Client from 'utils/web_client.jsx';
|
||||||
|
import * as Utils from 'utils/utils.jsx';
|
||||||
|
|
||||||
|
export default class SamlSettings extends AdminSettings {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.getConfigFromState = this.getConfigFromState.bind(this);
|
||||||
|
|
||||||
|
this.renderSettings = this.renderSettings.bind(this);
|
||||||
|
this.uploadCertificate = this.uploadCertificate.bind(this);
|
||||||
|
this.removeCertificate = this.removeCertificate.bind(this);
|
||||||
|
|
||||||
|
const settings = props.config.SamlSettings;
|
||||||
|
|
||||||
|
this.state = Object.assign(this.state, {
|
||||||
|
enable: settings.Enable,
|
||||||
|
verify: settings.Verify,
|
||||||
|
encrypt: settings.Encrypt,
|
||||||
|
idpUrl: settings.IdpUrl,
|
||||||
|
idpDescriptorUrl: settings.IdpDescriptorUrl,
|
||||||
|
assertionConsumerServiceURL: settings.AssertionConsumerServiceURL,
|
||||||
|
idpCertificateFile: settings.IdpCertificateFile,
|
||||||
|
publicCertificateFile: settings.PublicCertificateFile,
|
||||||
|
privateKeyFile: settings.PrivateKeyFile,
|
||||||
|
firstNameAttribute: settings.FirstNameAttribute,
|
||||||
|
lastNameAttribute: settings.LastNameAttribute,
|
||||||
|
emailAttribute: settings.EmailAttribute,
|
||||||
|
usernameAttribute: settings.UsernameAttribute,
|
||||||
|
nicknameAttribute: settings.NicknameAttribute,
|
||||||
|
localeAttribute: settings.LocaleAttribute,
|
||||||
|
loginButtonText: settings.LoginButtonText
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getConfigFromState(config) {
|
||||||
|
config.SamlSettings.Enable = this.state.enable;
|
||||||
|
config.SamlSettings.Verify = this.state.verify;
|
||||||
|
config.SamlSettings.Encrypt = this.state.encrypt;
|
||||||
|
config.SamlSettings.IdpUrl = this.state.idpUrl;
|
||||||
|
config.SamlSettings.IdpDescriptorUrl = this.state.idpDescriptorUrl;
|
||||||
|
config.SamlSettings.AssertionConsumerServiceURL = this.state.assertionConsumerServiceURL;
|
||||||
|
config.SamlSettings.IdpCertificateFile = this.state.idpCertificateFile;
|
||||||
|
config.SamlSettings.PublicCertificateFile = this.state.publicCertificateFile;
|
||||||
|
config.SamlSettings.PrivateKeyFile = this.state.privateKeyFile;
|
||||||
|
config.SamlSettings.FirstNameAttribute = this.state.firstNameAttribute;
|
||||||
|
config.SamlSettings.LastNameAttribute = this.state.lastNameAttribute;
|
||||||
|
config.SamlSettings.EmailAttribute = this.state.emailAttribute;
|
||||||
|
config.SamlSettings.UsernameAttribute = this.state.usernameAttribute;
|
||||||
|
config.SamlSettings.NicknameAttribute = this.state.nicknameAttribute;
|
||||||
|
config.SamlSettings.LocaleAttribute = this.state.localeAttribute;
|
||||||
|
config.SamlSettings.LoginButtonText = this.state.loginButtonText;
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
uploadCertificate(id, file, callback) {
|
||||||
|
Client.uploadCertificateFile(
|
||||||
|
file,
|
||||||
|
() => {
|
||||||
|
const fileName = file.name;
|
||||||
|
this.handleChange(id, fileName);
|
||||||
|
this.setState({[id]: fileName});
|
||||||
|
if (callback && typeof callback === 'function') {
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (callback && typeof callback === 'function') {
|
||||||
|
callback(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeCertificate(id, callback) {
|
||||||
|
Client.removeCertificateFile(
|
||||||
|
this.state[id],
|
||||||
|
() => {
|
||||||
|
this.handleChange(id, '');
|
||||||
|
this.setState({[id]: null});
|
||||||
|
},
|
||||||
|
(error) => {
|
||||||
|
if (callback && typeof callback === 'function') {
|
||||||
|
callback(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderTitle() {
|
||||||
|
return (
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.authentication.saml'
|
||||||
|
defaultMessage='SAML'
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderSettings() {
|
||||||
|
const licenseEnabled = global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.SAML === 'true';
|
||||||
|
if (!licenseEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let idpCert;
|
||||||
|
let privKey;
|
||||||
|
let pubCert;
|
||||||
|
|
||||||
|
if (this.state.idpCertificateFile) {
|
||||||
|
idpCert = (
|
||||||
|
<RemoveFileSetting
|
||||||
|
id='idpCertificateFile'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.idpCertificateFileTitle'
|
||||||
|
defaultMessage='Identity Provider Public Certificate:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.idpCertificateFileRemoveDesc'
|
||||||
|
defaultMessage='Remove the public authentication certificate issued by your Identity Provider.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
removeButtonText={Utils.localizeMessage('admin.saml.remove.idp_certificate', 'Remove Identity Provider Certificate')}
|
||||||
|
removingText={Utils.localizeMessage('admin.saml.removing.certificate', 'Removing Certificate...')}
|
||||||
|
fileName={this.state.idpCertificateFile}
|
||||||
|
onSubmit={this.removeCertificate}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
idpCert = (
|
||||||
|
<FileUploadSetting
|
||||||
|
id='idpCertificateFile'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.idpCertificateFileTitle'
|
||||||
|
defaultMessage='Identity Provider Public Certificate:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.idpCertificateFileDesc'
|
||||||
|
defaultMessage='The public authentication certificate issued by your Identity Provider.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
uploadingText={Utils.localizeMessage('admin.saml.uploading.certificate', 'Uploading Certificate...')}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
fileType='.crt'
|
||||||
|
onSubmit={this.uploadCertificate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.privateKeyFile) {
|
||||||
|
privKey = (
|
||||||
|
<RemoveFileSetting
|
||||||
|
id='privateKeyFile'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.privateKeyFileTitle'
|
||||||
|
defaultMessage='Service Provider Private Key:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.privateKeyFileFileRemoveDesc'
|
||||||
|
defaultMessage='Remove the private key used to decrypt SAML Assertions from the Identity Provider.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
removeButtonText={Utils.localizeMessage('admin.saml.remove.privKey', 'Remove Service Provider Private Key')}
|
||||||
|
removingText={Utils.localizeMessage('admin.saml.removing.privKey', 'Removing Private Key...')}
|
||||||
|
fileName={this.state.privateKeyFile}
|
||||||
|
onSubmit={this.removeCertificate}
|
||||||
|
disabled={!this.state.enable || !this.state.encrypt}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
privKey = (
|
||||||
|
<FileUploadSetting
|
||||||
|
id='privateKeyFile'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.privateKeyFileTitle'
|
||||||
|
defaultMessage='Service Provider Private Key:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.privateKeyFileFileDesc'
|
||||||
|
defaultMessage='The private key used to decrypt SAML Assertions from the Identity Provider.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
uploadingText={Utils.localizeMessage('admin.saml.uploading.privateKey', 'Uploading Private Key...')}
|
||||||
|
disabled={!this.state.enable || !this.state.encrypt}
|
||||||
|
fileType='.key'
|
||||||
|
onSubmit={this.uploadCertificate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.state.publicCertificateFile) {
|
||||||
|
pubCert = (
|
||||||
|
<RemoveFileSetting
|
||||||
|
id='publicCertificateFile'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.publicCertificateFileTitle'
|
||||||
|
defaultMessage='Service Provider Public Certificate:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.publicCertificateFileRemoveDesc'
|
||||||
|
defaultMessage='Remove the certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
removeButtonText={Utils.localizeMessage('admin.saml.remove.sp_certificate', 'Remove Service Provider Certificate')}
|
||||||
|
removingText={Utils.localizeMessage('admin.saml.removing.certificate', 'Removing Certificate...')}
|
||||||
|
fileName={this.state.publicCertificateFile}
|
||||||
|
onSubmit={this.removeCertificate}
|
||||||
|
disabled={!this.state.enable || !this.state.encrypt}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
pubCert = (
|
||||||
|
<FileUploadSetting
|
||||||
|
id='publicCertificateFile'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.publicCertificateFileTitle'
|
||||||
|
defaultMessage='Service Provider Public Certificate:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.publicCertificateFileDesc'
|
||||||
|
defaultMessage='The certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
uploadingText={Utils.localizeMessage('admin.saml.uploading.certificate', 'Uploading Certificate...')}
|
||||||
|
disabled={!this.state.enable || !this.state.encrypt}
|
||||||
|
fileType='.crt'
|
||||||
|
onSubmit={this.uploadCertificate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SettingsGroup>
|
||||||
|
<BooleanSetting
|
||||||
|
id='enable'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.enableTitle'
|
||||||
|
defaultMessage='Enable Login With SAML:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedHTMLMessage
|
||||||
|
id='admin.saml.enableDescription'
|
||||||
|
defaultMessage='When true, Mattermost allows login using SAML. Please see <a href="http://docs.mattermost.com/deployment/sso-saml.html" target="_blank">documentation</a> to learn more about configuring SAML for Mattermost.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.enable}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
<TextSetting
|
||||||
|
id='idpUrl'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.idpUrlTitle'
|
||||||
|
defaultMessage='SAML SSO URL:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={Utils.localizeMessage('admin.saml.idpUrlEx', 'Ex "https://idp.example.org/SAML2/SSO/Login"')}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.idpUrlDesc'
|
||||||
|
defaultMessage='The URL where Mattermost sends a SAML request to start login sequence.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.idpUrl}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
/>
|
||||||
|
<TextSetting
|
||||||
|
id='idpDescriptorUrl'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.idpDescriptorUrlTitle'
|
||||||
|
defaultMessage='Identity Provider Issuer URL:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={Utils.localizeMessage('admin.saml.idpDescriptorUrlEx', 'Ex "https://idp.example.org/SAML2/issuer"')}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.idpDescriptorUrlDesc'
|
||||||
|
defaultMessage='The issuer URL for the Identity Provider you use for SAML requests.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.idpDescriptorUrl}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
/>
|
||||||
|
{idpCert}
|
||||||
|
<BooleanSetting
|
||||||
|
id='verify'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.verifyTitle'
|
||||||
|
defaultMessage='Verify Signature:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.verifyDescription'
|
||||||
|
defaultMessage='When true, Mattermost verifies that the signature sent from the SAML Response matches the Service Provider Login URL'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.verify}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
<TextSetting
|
||||||
|
id='assertionConsumerServiceURL'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.assertionConsumerServiceURLTitle'
|
||||||
|
defaultMessage='Service Provider Login URL:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={Utils.localizeMessage('admin.saml.assertionConsumerServiceURLEx', 'Ex "https://<your-mattermost-url>/login/sso/saml"')}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.assertionConsumerServiceURLDesc'
|
||||||
|
defaultMessage='Enter https://<your-mattermost-url>/login/sso/saml. Make sure you use HTTP or HTTPS in your URL depending on your server configuration. This field is also known as the Assertion Consumer Service URL.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.assertionConsumerServiceURL}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={!this.state.enable || !this.state.verify}
|
||||||
|
/>
|
||||||
|
<BooleanSetting
|
||||||
|
id='encrypt'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.encryptTitle'
|
||||||
|
defaultMessage='Enable Encryption:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.encryptDescription'
|
||||||
|
defaultMessage='When true, Mattermost will decrypt SAML Assertions encrypted with your Service Provider Public Certificate.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.encrypt}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
/>
|
||||||
|
{privKey}
|
||||||
|
{pubCert}
|
||||||
|
<TextSetting
|
||||||
|
id='emailAttribute'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.emailAttrTitle'
|
||||||
|
defaultMessage='Email Attribute:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={Utils.localizeMessage('admin.saml.emailAttrEx', 'Ex "Email" or "PrimaryEmail"')}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.emailAttrDesc'
|
||||||
|
defaultMessage='The attribute in the SAML Assertion that will be used to populate the email addresses of users in Mattermost.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.emailAttribute}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
/>
|
||||||
|
<TextSetting
|
||||||
|
id='usernameAttribute'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.usernameAttrTitle'
|
||||||
|
defaultMessage='Username Attribute:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={Utils.localizeMessage('admin.saml.usernameAttrEx', 'Ex "Username"')}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.usernameAttrDesc'
|
||||||
|
defaultMessage='The attribute in the SAML Assertion that will be used to populate the username field in Mattermost.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.usernameAttribute}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
/>
|
||||||
|
<TextSetting
|
||||||
|
id='firstNameAttribute'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.firstnameAttrTitle'
|
||||||
|
defaultMessage='First Name Attribute:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={Utils.localizeMessage('admin.saml.firstnameAttrEx', 'Ex "FirstName"')}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.firstnameAttrDesc'
|
||||||
|
defaultMessage='The attribute in the SAML Assertion that will be used to populate the first name of users in Mattermost.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.firstNameAttribute}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
/>
|
||||||
|
<TextSetting
|
||||||
|
id='lastNameAttribute'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.lastnameAttrTitle'
|
||||||
|
defaultMessage='Last Name Attribute:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={Utils.localizeMessage('admin.saml.lastnameAttrEx', 'Ex "LastName"')}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.lastnameAttrDesc'
|
||||||
|
defaultMessage='The attribute in the SAML Assertion that will be used to populate the last name of users in Mattermost.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.lastNameAttribute}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
/>
|
||||||
|
<TextSetting
|
||||||
|
id='nicknameAttribute'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.nicknameAttrTitle'
|
||||||
|
defaultMessage='Nickname Attribute:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={Utils.localizeMessage('admin.saml.nicknameAttrEx', 'Ex "Nickname"')}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.nicknameAttrDesc'
|
||||||
|
defaultMessage='(Optional) The attribute in the SAML Assertion that will be used to populate the nickname of users in Mattermost.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.nicknameAttribute}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
/>
|
||||||
|
<TextSetting
|
||||||
|
id='localeAttribute'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.localeAttrTitle'
|
||||||
|
defaultMessage='Preferred Language Attribute:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={Utils.localizeMessage('admin.saml.localeAttrEx', 'Ex "Locale" or "PrimaryLanguage"')}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.localeAttrDesc'
|
||||||
|
defaultMessage='(Optional) The attribute in the SAML Assertion that will be used to populate the language of users in Mattermost.'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.localeAttribute}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
/>
|
||||||
|
<TextSetting
|
||||||
|
id='loginButtonText'
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.loginButtonTextTitle'
|
||||||
|
defaultMessage='Login Button Text:'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
placeholder={Utils.localizeMessage('admin.saml.loginButtonTextEx', 'Ex "With OKTA"')}
|
||||||
|
helpText={
|
||||||
|
<FormattedMessage
|
||||||
|
id='admin.saml.loginButtonTextDesc'
|
||||||
|
defaultMessage='(Optional) The text that appears in the login button on the login page. Defaults to "With SAML".'
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
value={this.state.loginButtonText}
|
||||||
|
onChange={this.handleChange}
|
||||||
|
disabled={!this.state.enable}
|
||||||
|
/>
|
||||||
|
</SettingsGroup>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// See License.txt for license information.
|
// See License.txt for license information.
|
||||||
|
|
||||||
import Client from 'utils/web_client.jsx';
|
import Client from 'utils/web_client.jsx';
|
||||||
|
import Constants from 'utils/constants.jsx';
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import UserStore from 'stores/user_store.jsx';
|
import UserStore from 'stores/user_store.jsx';
|
||||||
import ConfirmModal from '../confirm_modal.jsx';
|
import ConfirmModal from '../confirm_modal.jsx';
|
||||||
@@ -374,12 +375,13 @@ export default class UserItem extends React.Component {
|
|||||||
let authServiceText;
|
let authServiceText;
|
||||||
let passwordReset;
|
let passwordReset;
|
||||||
if (user.auth_service) {
|
if (user.auth_service) {
|
||||||
|
const service = (user.auth_service === Constants.LDAP_SERVICE || user.auth_service === Constants.SAML_SERVICE) ? user.auth_service.toUpperCase() : Utils.toTitleCase(user.auth_service);
|
||||||
authServiceText = (
|
authServiceText = (
|
||||||
<FormattedHTMLMessage
|
<FormattedHTMLMessage
|
||||||
id='admin.user_item.authServiceNotEmail'
|
id='admin.user_item.authServiceNotEmail'
|
||||||
defaultMessage=', <strong>Sign-in Method:</strong> {service}'
|
defaultMessage=', <strong>Sign-in Method:</strong> {service}'
|
||||||
values={{
|
values={{
|
||||||
service: Utils.toTitleCase(user.auth_service)
|
service
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import Client from 'utils/web_client.jsx';
|
import Client from 'utils/web_client.jsx';
|
||||||
|
import Constants from 'utils/constants.jsx';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
@@ -55,7 +56,8 @@ export default class EmailToOAuth extends React.Component {
|
|||||||
formClass += ' has-error';
|
formClass += ' has-error';
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiType = Utils.toTitleCase(this.props.newType) + ' SSO';
|
const type = (this.props.newType === Constants.SAML_SERVICE ? Constants.SAML_SERVICE.toUpperCase() : Utils.toTitleCase(this.props.newType));
|
||||||
|
const uiType = `${type} SSO`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -74,7 +76,7 @@ export default class EmailToOAuth extends React.Component {
|
|||||||
id='claim.email_to_oauth.ssoType'
|
id='claim.email_to_oauth.ssoType'
|
||||||
defaultMessage='Upon claiming your account, you will only be able to login with {type} SSO'
|
defaultMessage='Upon claiming your account, you will only be able to login with {type} SSO'
|
||||||
values={{
|
values={{
|
||||||
type: Utils.toTitleCase(this.props.newType)
|
type
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
@@ -83,7 +85,7 @@ export default class EmailToOAuth extends React.Component {
|
|||||||
id='claim.email_to_oauth.ssoNote'
|
id='claim.email_to_oauth.ssoNote'
|
||||||
defaultMessage='You must already have a valid {type} account'
|
defaultMessage='You must already have a valid {type} account'
|
||||||
values={{
|
values={{
|
||||||
type: Utils.toTitleCase(this.props.newType)
|
type
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
import * as Utils from 'utils/utils.jsx';
|
import * as Utils from 'utils/utils.jsx';
|
||||||
import Client from 'utils/web_client.jsx';
|
import Client from 'utils/web_client.jsx';
|
||||||
|
import Constants from 'utils/constants.jsx';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
@@ -62,7 +63,7 @@ export default class OAuthToEmail extends React.Component {
|
|||||||
formClass += ' has-error';
|
formClass += ' has-error';
|
||||||
}
|
}
|
||||||
|
|
||||||
const uiType = Utils.toTitleCase(this.props.currentType) + ' SSO';
|
const uiType = `${(this.props.currentType === Constants.SAML_SERVICE ? Constants.SAML_SERVICE.toUpperCase() : Utils.toTitleCase(this.props.currentType))} SSO`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -85,7 +86,7 @@ export default class OAuthToEmail extends React.Component {
|
|||||||
<p>
|
<p>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='claim.oauth_to_email.enterNewPwd'
|
id='claim.oauth_to_email.enterNewPwd'
|
||||||
defaultMessage='Enter a new password for your {site} account'
|
defaultMessage='Enter a new password for your {site} email account'
|
||||||
values={{
|
values={{
|
||||||
site: global.window.mm_config.SiteName
|
site: global.window.mm_config.SiteName
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export default class LoginController extends React.Component {
|
|||||||
ldapEnabled: global.window.mm_license.IsLicensed === 'true' && global.window.mm_config.EnableLdap === 'true',
|
ldapEnabled: global.window.mm_license.IsLicensed === 'true' && global.window.mm_config.EnableLdap === 'true',
|
||||||
usernameSigninEnabled: global.window.mm_config.EnableSignInWithUsername === 'true',
|
usernameSigninEnabled: global.window.mm_config.EnableSignInWithUsername === 'true',
|
||||||
emailSigninEnabled: global.window.mm_config.EnableSignInWithEmail === 'true',
|
emailSigninEnabled: global.window.mm_config.EnableSignInWithEmail === 'true',
|
||||||
|
samlEnabled: global.window.mm_license.IsLicensed === 'true' && global.window.mm_config.EnableSaml === 'true',
|
||||||
loginId: '', // the browser will set a default for this
|
loginId: '', // the browser will set a default for this
|
||||||
password: '',
|
password: '',
|
||||||
showMfa: false
|
showMfa: false
|
||||||
@@ -319,6 +320,7 @@ export default class LoginController extends React.Component {
|
|||||||
const ldapEnabled = this.state.ldapEnabled;
|
const ldapEnabled = this.state.ldapEnabled;
|
||||||
const gitlabSigninEnabled = global.window.mm_config.EnableSignUpWithGitLab === 'true';
|
const gitlabSigninEnabled = global.window.mm_config.EnableSignUpWithGitLab === 'true';
|
||||||
const googleSigninEnabled = global.window.mm_config.EnableSignUpWithGoogle === 'true';
|
const googleSigninEnabled = global.window.mm_config.EnableSignUpWithGoogle === 'true';
|
||||||
|
const samlSigninEnabled = this.state.samlEnabled;
|
||||||
const usernameSigninEnabled = this.state.usernameSigninEnabled;
|
const usernameSigninEnabled = this.state.usernameSigninEnabled;
|
||||||
const emailSigninEnabled = this.state.emailSigninEnabled;
|
const emailSigninEnabled = this.state.emailSigninEnabled;
|
||||||
|
|
||||||
@@ -416,7 +418,7 @@ export default class LoginController extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((emailSigninEnabled || usernameSigninEnabled || ldapEnabled) && (gitlabSigninEnabled || googleSigninEnabled)) {
|
if ((emailSigninEnabled || usernameSigninEnabled || ldapEnabled) && (gitlabSigninEnabled || googleSigninEnabled || samlSigninEnabled)) {
|
||||||
loginControls.push(
|
loginControls.push(
|
||||||
<div
|
<div
|
||||||
key='divider'
|
key='divider'
|
||||||
@@ -475,6 +477,20 @@ export default class LoginController extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (samlSigninEnabled) {
|
||||||
|
loginControls.push(
|
||||||
|
<a
|
||||||
|
className='btn btn-custom-login saml'
|
||||||
|
key='gitlab'
|
||||||
|
href={'/login/sso/saml' + this.props.location.search}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{window.mm_config.SamlLoginButtonText}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{extraBox}
|
{extraBox}
|
||||||
|
|||||||
@@ -588,6 +588,20 @@ export default class SignupUserComplete extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (global.window.mm_config.EnableSaml === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.SAML === 'true') {
|
||||||
|
signupMessage.push(
|
||||||
|
<a
|
||||||
|
className='btn btn-custom-login saml'
|
||||||
|
key='saml'
|
||||||
|
href={`/login/sso/saml${window.location.search}${window.location.search ? '&' : '?'}action=signup`}
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
{global.window.mm_config.SamlLoginButtonText}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let ldapSignup;
|
let ldapSignup;
|
||||||
if (global.window.mm_config.EnableLdap === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP) {
|
if (global.window.mm_config.EnableLdap === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP) {
|
||||||
ldapSignup = (
|
ldapSignup = (
|
||||||
|
|||||||
@@ -412,6 +412,24 @@ class UserSettingsGeneralTab extends React.Component {
|
|||||||
{helpText}
|
{helpText}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
} else if (this.props.user.auth_service === Constants.SAML_SERVICE) {
|
||||||
|
inputs.push(
|
||||||
|
<div
|
||||||
|
key='oauthEmailInfo'
|
||||||
|
className='form-group'
|
||||||
|
>
|
||||||
|
<div className='setting-list__hint'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='user.settings.general.emailSamlCantUpdate'
|
||||||
|
defaultMessage='Login occurs through SAML. Email cannot be updated. Email address used for notifications is {email}.'
|
||||||
|
values={{
|
||||||
|
email: this.state.email
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{helpText}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
emailSection = (
|
emailSection = (
|
||||||
@@ -478,6 +496,16 @@ class UserSettingsGeneralTab extends React.Component {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (this.props.user.auth_service === Constants.SAML_SERVICE) {
|
||||||
|
describe = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='user.settings.general.loginSaml'
|
||||||
|
defaultMessage='Login done through SAML ({email})'
|
||||||
|
values={{
|
||||||
|
email: this.state.email
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
emailSection = (
|
emailSection = (
|
||||||
|
|||||||
@@ -620,6 +620,24 @@ class SecurityTab extends React.Component {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let samlOption;
|
||||||
|
if (global.window.mm_config.EnableSaml === 'true' && user.auth_service === '') {
|
||||||
|
samlOption = (
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
className='btn btn-primary'
|
||||||
|
to={'/claim/email_to_oauth?email=' + encodeURIComponent(user.email) + '&old_type=' + user.auth_service + '&new_type=' + Constants.SAML_SERVICE}
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id='user.settings.security.switchSaml'
|
||||||
|
defaultMessage='Switch to using SAML SSO'
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
<br/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const inputs = [];
|
const inputs = [];
|
||||||
inputs.push(
|
inputs.push(
|
||||||
<div key='userSignInOption'>
|
<div key='userSignInOption'>
|
||||||
@@ -627,6 +645,7 @@ class SecurityTab extends React.Component {
|
|||||||
{gitlabOption}
|
{gitlabOption}
|
||||||
<br/>
|
<br/>
|
||||||
{ldapOption}
|
{ldapOption}
|
||||||
|
{samlOption}
|
||||||
{googleOption}
|
{googleOption}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -681,6 +700,13 @@ class SecurityTab extends React.Component {
|
|||||||
defaultMessage='LDAP'
|
defaultMessage='LDAP'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
} else if (this.props.user.auth_service === Constants.SAML_SERVICE) {
|
||||||
|
describe = (
|
||||||
|
<FormattedMessage
|
||||||
|
id='user.settings.security.saml'
|
||||||
|
defaultMessage='SAML'
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -701,6 +727,7 @@ class SecurityTab extends React.Component {
|
|||||||
numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods;
|
numMethods = global.window.mm_config.EnableSignUpWithGitLab === 'true' ? numMethods + 1 : numMethods;
|
||||||
numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods;
|
numMethods = global.window.mm_config.EnableSignUpWithGoogle === 'true' ? numMethods + 1 : numMethods;
|
||||||
numMethods = global.window.mm_config.EnableLdap === 'true' ? numMethods + 1 : numMethods;
|
numMethods = global.window.mm_config.EnableLdap === 'true' ? numMethods + 1 : numMethods;
|
||||||
|
numMethods = global.window.mm_config.EnableSaml === 'true' ? numMethods + 1 : numMethods;
|
||||||
|
|
||||||
let signInSection;
|
let signInSection;
|
||||||
if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) {
|
if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) {
|
||||||
|
|||||||
@@ -102,6 +102,7 @@
|
|||||||
"admin.audits.title": "User Activity Logs",
|
"admin.audits.title": "User Activity Logs",
|
||||||
"admin.authentication.email": "Email Auth",
|
"admin.authentication.email": "Email Auth",
|
||||||
"admin.authentication.gitlab": "GitLab",
|
"admin.authentication.gitlab": "GitLab",
|
||||||
|
"admin.authentication.saml": "SAML",
|
||||||
"admin.banner.heading": "Note:",
|
"admin.banner.heading": "Note:",
|
||||||
"admin.compliance.directoryDescription": "Directory to which compliance reports are written. If blank, will be set to ./data/.",
|
"admin.compliance.directoryDescription": "Directory to which compliance reports are written. If blank, will be set to ./data/.",
|
||||||
"admin.compliance.directoryExample": "Ex \"./data/\"",
|
"admin.compliance.directoryExample": "Ex \"./data/\"",
|
||||||
@@ -217,6 +218,9 @@
|
|||||||
"admin.email.smtpUsernameTitle": "SMTP Server Username:",
|
"admin.email.smtpUsernameTitle": "SMTP Server Username:",
|
||||||
"admin.email.testing": "Testing...",
|
"admin.email.testing": "Testing...",
|
||||||
"admin.false": "false",
|
"admin.false": "false",
|
||||||
|
"admin.file_upload.chooseFile": "Choose File",
|
||||||
|
"admin.file_upload.noFile": "No file uploaded",
|
||||||
|
"admin.file_upload.uploadFile": "Upload",
|
||||||
"admin.files.images": "Images",
|
"admin.files.images": "Images",
|
||||||
"admin.files.storage": "Storage",
|
"admin.files.storage": "Storage",
|
||||||
"admin.general.configuration": "Configuration",
|
"admin.general.configuration": "Configuration",
|
||||||
@@ -431,6 +435,58 @@
|
|||||||
"admin.reset_password.submit": "Please enter at least {chars} characters.",
|
"admin.reset_password.submit": "Please enter at least {chars} characters.",
|
||||||
"admin.reset_password.titleReset": "Reset Password",
|
"admin.reset_password.titleReset": "Reset Password",
|
||||||
"admin.reset_password.titleSwitch": "Switch Account to Email/Password",
|
"admin.reset_password.titleSwitch": "Switch Account to Email/Password",
|
||||||
|
"admin.saml.assertionConsumerServiceURLDesc": "Enter https://<your-mattermost-url>/login/sso/saml. Make sure you use HTTP or HTTPS in your URL depending on your server configuration. This field is also known as the Assertion Consumer Service URL.",
|
||||||
|
"admin.saml.assertionConsumerServiceURLEx": "Ex \"https://<your-mattermost-url>/login/sso/saml\"",
|
||||||
|
"admin.saml.assertionConsumerServiceURLTitle": "Service Provider Login URL:",
|
||||||
|
"admin.saml.emailAttrDesc": "The attribute in the SAML Assertion that will be used to populate the email addresses of users in Mattermost.",
|
||||||
|
"admin.saml.emailAttrEx": "Ex \"Email\" or \"PrimaryEmail\"",
|
||||||
|
"admin.saml.emailAttrTitle": "Email Attribute:",
|
||||||
|
"admin.saml.enableDescription": "When true, Mattermost allows login using SAML. Please see <a href='http://docs.mattermost.com/deployment/sso-saml.html' target='_blank'>documentation</a> to learn more about configuring SAML for Mattermost.",
|
||||||
|
"admin.saml.enableTitle": "Enable Login With SAML:",
|
||||||
|
"admin.saml.encryptDescription": "When true, Mattermost will decrypt SAML Assertions encrypted with your Service Provider Public Certificate.",
|
||||||
|
"admin.saml.encryptTitle": "Enable Encryption:",
|
||||||
|
"admin.saml.firstnameAttrDesc": "The attribute in the SAML Assertion that will be used to populate the first name of users in Mattermost.",
|
||||||
|
"admin.saml.firstnameAttrEx": "Ex \"FirstName\"",
|
||||||
|
"admin.saml.firstnameAttrTitle": "First Name Attribute:",
|
||||||
|
"admin.saml.idpCertificateFileDesc": "The public authentication certificate issued by your Identity Provider.",
|
||||||
|
"admin.saml.idpCertificateFileRemoveDesc": "Remove the public authentication certificate issued by your Identity Provider.",
|
||||||
|
"admin.saml.idpCertificateFileTitle": "Identity Provider Public Certificate:",
|
||||||
|
"admin.saml.idpDescriptorUrlDesc": "The issuer URL for the Identity Provider you use for SAML requests.",
|
||||||
|
"admin.saml.idpDescriptorUrlEx": "Ex \"https://idp.example.org/SAML2/issuer\"",
|
||||||
|
"admin.saml.idpDescriptorUrlTitle": "Identity Provider Issuer URL:",
|
||||||
|
"admin.saml.idpUrlDesc": "The URL where Mattermost sends a SAML request to start login sequence.",
|
||||||
|
"admin.saml.idpUrlEx": "Ex \"https://idp.example.org/SAML2/SSO/Login\"",
|
||||||
|
"admin.saml.idpUrlTitle": "SAML SSO URL:",
|
||||||
|
"admin.saml.lastnameAttrDesc": "The attribute in the SAML Assertion that will be used to populate the last name of users in Mattermost.",
|
||||||
|
"admin.saml.lastnameAttrEx": "Ex \"LastName\"",
|
||||||
|
"admin.saml.lastnameAttrTitle": "Last Name Attribute:",
|
||||||
|
"admin.saml.localeAttrDesc": "(Optional) The attribute in the SAML Assertion that will be used to populate the language of users in Mattermost.",
|
||||||
|
"admin.saml.localeAttrEx": "Ex \"Locale\" or \"PrimaryLanguage\"",
|
||||||
|
"admin.saml.localeAttrTitle": "Preferred Language Attribute:",
|
||||||
|
"admin.saml.loginButtonTextDesc": "(Optional) The text that appears in the login button on the login page. Defaults to \"With SAML\".",
|
||||||
|
"admin.saml.loginButtonTextEx": "Ex \"With OKTA\"",
|
||||||
|
"admin.saml.loginButtonTextTitle": "Login Button Text:",
|
||||||
|
"admin.saml.nicknameAttrDesc": "(Optional) The attribute in the SAML Assertion that will be used to populate the nickname of users in Mattermost.",
|
||||||
|
"admin.saml.nicknameAttrEx": "Ex \"Nickname\"",
|
||||||
|
"admin.saml.nicknameAttrTitle": "Nickname Attribute:",
|
||||||
|
"admin.saml.privateKeyFileFileDesc": "The private key used to decrypt SAML Assertions from the Identity Provider.",
|
||||||
|
"admin.saml.privateKeyFileFileRemoveDesc": "Remove the private key used to decrypt SAML Assertions from the Identity Provider.",
|
||||||
|
"admin.saml.privateKeyFileTitle": "Service Provider Private Key:",
|
||||||
|
"admin.saml.publicCertificateFileDesc": "The certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.",
|
||||||
|
"admin.saml.publicCertificateFileRemoveDesc": "Remove the certificate used to generate the signature on a SAML request to the Identity Provider for a service provider initiated SAML login, when Mattermost is the Service Provider.",
|
||||||
|
"admin.saml.publicCertificateFileTitle": "Service Provider Public Certificate:",
|
||||||
|
"admin.saml.remove.idp_certificate": "Remove Identity Provider Certificate",
|
||||||
|
"admin.saml.remove.privKey": "Remove Service Provider Private Key",
|
||||||
|
"admin.saml.remove.sp_certificate": "Remove Service Provider Certificate",
|
||||||
|
"admin.saml.removing.certificate": "Removing Certificate...",
|
||||||
|
"admin.saml.removing.privKey": "Removing Private Key...",
|
||||||
|
"admin.saml.uploading.certificate": "Uploading Certificate...",
|
||||||
|
"admin.saml.uploading.privateKey": "Uploading Private Key...",
|
||||||
|
"admin.saml.usernameAttrDesc": "The attribute in the SAML Assertion that will be used to populate the username field in Mattermost.",
|
||||||
|
"admin.saml.usernameAttrEx": "Ex \"Username\"",
|
||||||
|
"admin.saml.usernameAttrTitle": "Username Attribute:",
|
||||||
|
"admin.saml.verifyDescription": "When true, Mattermost verifies that the signature sent from the SAML Response matches the Service Provider Login URL",
|
||||||
|
"admin.saml.verifyTitle": "Verify Signature:",
|
||||||
"admin.save": "Save",
|
"admin.save": "Save",
|
||||||
"admin.saving": "Saving Config...",
|
"admin.saving": "Saving Config...",
|
||||||
"admin.security.connection": "Connections",
|
"admin.security.connection": "Connections",
|
||||||
@@ -522,6 +578,7 @@
|
|||||||
"admin.sidebar.rateLimiting": "Rate Limiting",
|
"admin.sidebar.rateLimiting": "Rate Limiting",
|
||||||
"admin.sidebar.reports": "REPORTING",
|
"admin.sidebar.reports": "REPORTING",
|
||||||
"admin.sidebar.rmTeamSidebar": "Remove team from sidebar menu",
|
"admin.sidebar.rmTeamSidebar": "Remove team from sidebar menu",
|
||||||
|
"admin.sidebar.saml": "SAML",
|
||||||
"admin.sidebar.security": "Security",
|
"admin.sidebar.security": "Security",
|
||||||
"admin.sidebar.sessions": "Sessions",
|
"admin.sidebar.sessions": "Sessions",
|
||||||
"admin.sidebar.settings": "SETTINGS",
|
"admin.sidebar.settings": "SETTINGS",
|
||||||
@@ -842,7 +899,7 @@
|
|||||||
"claim.ldap_to_email.title": "Switch LDAP Account to Email/Password",
|
"claim.ldap_to_email.title": "Switch LDAP Account to Email/Password",
|
||||||
"claim.oauth_to_email.confirm": "Confirm Password",
|
"claim.oauth_to_email.confirm": "Confirm Password",
|
||||||
"claim.oauth_to_email.description": "Upon changing your account type, you will only be able to login with your email and password.",
|
"claim.oauth_to_email.description": "Upon changing your account type, you will only be able to login with your email and password.",
|
||||||
"claim.oauth_to_email.enterNewPwd": "Enter a new password for your {site} account",
|
"claim.oauth_to_email.enterNewPwd": "Enter a new password for your {site} email account",
|
||||||
"claim.oauth_to_email.enterPwd": "Please enter a password.",
|
"claim.oauth_to_email.enterPwd": "Please enter a password.",
|
||||||
"claim.oauth_to_email.newPwd": "New Password",
|
"claim.oauth_to_email.newPwd": "New Password",
|
||||||
"claim.oauth_to_email.pwdNotMatch": "Password do not match.",
|
"claim.oauth_to_email.pwdNotMatch": "Password do not match.",
|
||||||
@@ -1454,6 +1511,7 @@
|
|||||||
"user.settings.general.emailHelp4": "A verification email was sent to {email}.",
|
"user.settings.general.emailHelp4": "A verification email was sent to {email}.",
|
||||||
"user.settings.general.emailLdapCantUpdate": "Login occurs through LDAP. Email cannot be updated. Email address used for notifications is {email}.",
|
"user.settings.general.emailLdapCantUpdate": "Login occurs through LDAP. Email cannot be updated. Email address used for notifications is {email}.",
|
||||||
"user.settings.general.emailMatch": "The new emails you entered do not match.",
|
"user.settings.general.emailMatch": "The new emails you entered do not match.",
|
||||||
|
"user.settings.general.emailSamlCantUpdate": "Login occurs through SAML. Email cannot be updated. Email address used for notifications is {email}.",
|
||||||
"user.settings.general.emailUnchanged": "Your new email address is the same as your old email address.",
|
"user.settings.general.emailUnchanged": "Your new email address is the same as your old email address.",
|
||||||
"user.settings.general.emptyName": "Click 'Edit' to add your full name",
|
"user.settings.general.emptyName": "Click 'Edit' to add your full name",
|
||||||
"user.settings.general.emptyNickname": "Click 'Edit' to add a nickname",
|
"user.settings.general.emptyNickname": "Click 'Edit' to add a nickname",
|
||||||
@@ -1465,6 +1523,7 @@
|
|||||||
"user.settings.general.lastName": "Last Name",
|
"user.settings.general.lastName": "Last Name",
|
||||||
"user.settings.general.loginGitlab": "Login done through GitLab ({email})",
|
"user.settings.general.loginGitlab": "Login done through GitLab ({email})",
|
||||||
"user.settings.general.loginLdap": "Login done through LDAP ({email})",
|
"user.settings.general.loginLdap": "Login done through LDAP ({email})",
|
||||||
|
"user.settings.general.loginSaml": "Login done through SAML ({email})",
|
||||||
"user.settings.general.newAddress": "New Address: {email}<br />Check your email to verify the above address.",
|
"user.settings.general.newAddress": "New Address: {email}<br />Check your email to verify the above address.",
|
||||||
"user.settings.general.nickname": "Nickname",
|
"user.settings.general.nickname": "Nickname",
|
||||||
"user.settings.general.nicknameExtra": "Use Nickname for a name you might be called that is different from your first name and username. This is most often used when two or more people have similar sounding names and usernames.",
|
"user.settings.general.nicknameExtra": "Use Nickname for a name you might be called that is different from your first name and username. This is most often used when two or more people have similar sounding names and usernames.",
|
||||||
@@ -1552,10 +1611,12 @@
|
|||||||
"user.settings.security.passwordLengthError": "New passwords must be at least {chars} characters",
|
"user.settings.security.passwordLengthError": "New passwords must be at least {chars} characters",
|
||||||
"user.settings.security.passwordMatchError": "The new passwords you entered do not match",
|
"user.settings.security.passwordMatchError": "The new passwords you entered do not match",
|
||||||
"user.settings.security.retypePassword": "Retype New Password",
|
"user.settings.security.retypePassword": "Retype New Password",
|
||||||
|
"user.settings.security.saml": "SAML",
|
||||||
"user.settings.security.switchEmail": "Switch to using email and password",
|
"user.settings.security.switchEmail": "Switch to using email and password",
|
||||||
"user.settings.security.switchGitlab": "Switch to using GitLab SSO",
|
"user.settings.security.switchGitlab": "Switch to using GitLab SSO",
|
||||||
"user.settings.security.switchGoogle": "Switch to using Google SSO",
|
"user.settings.security.switchGoogle": "Switch to using Google SSO",
|
||||||
"user.settings.security.switchLdap": "Switch to using LDAP",
|
"user.settings.security.switchLdap": "Switch to using LDAP",
|
||||||
|
"user.settings.security.switchSaml": "Switch to using SAML SSO",
|
||||||
"user.settings.security.title": "Security Settings",
|
"user.settings.security.title": "Security Settings",
|
||||||
"user.settings.security.viewHistory": "View Access History",
|
"user.settings.security.viewHistory": "View Access History",
|
||||||
"user_list.notFound": "No users found",
|
"user_list.notFound": "No users found",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@
|
|||||||
"keymirror": "0.1.1",
|
"keymirror": "0.1.1",
|
||||||
"marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b",
|
"marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b",
|
||||||
"match-at": "0.1.0",
|
"match-at": "0.1.0",
|
||||||
"mattermost": "mattermost/mattermost-javascript#8e4c320d5af653eacb248455d77057a76ec28830",
|
"mattermost": "mattermost/mattermost-javascript#798c39c5d302d2d109e768a35575ebdbf2a8ee6a",
|
||||||
"object-assign": "4.1.0",
|
"object-assign": "4.1.0",
|
||||||
"perfect-scrollbar": "0.6.11",
|
"perfect-scrollbar": "0.6.11",
|
||||||
"react": "15.0.2",
|
"react": "15.0.2",
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import LogSettings from 'components/admin_console/log_settings.jsx';
|
|||||||
import EmailAuthenticationSettings from 'components/admin_console/email_authentication_settings.jsx';
|
import EmailAuthenticationSettings from 'components/admin_console/email_authentication_settings.jsx';
|
||||||
import GitLabSettings from 'components/admin_console/gitlab_settings.jsx';
|
import GitLabSettings from 'components/admin_console/gitlab_settings.jsx';
|
||||||
import LdapSettings from 'components/admin_console/ldap_settings.jsx';
|
import LdapSettings from 'components/admin_console/ldap_settings.jsx';
|
||||||
|
import SamlSettings from 'components/admin_console/saml_settings.jsx';
|
||||||
import SignupSettings from 'components/admin_console/signup_settings.jsx';
|
import SignupSettings from 'components/admin_console/signup_settings.jsx';
|
||||||
import LoginSettings from 'components/admin_console/login_settings.jsx';
|
import LoginSettings from 'components/admin_console/login_settings.jsx';
|
||||||
import PublicLinkSettings from 'components/admin_console/public_link_settings.jsx';
|
import PublicLinkSettings from 'components/admin_console/public_link_settings.jsx';
|
||||||
@@ -90,6 +91,10 @@ export default (
|
|||||||
path='ldap'
|
path='ldap'
|
||||||
component={LdapSettings}
|
component={LdapSettings}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path='saml'
|
||||||
|
component={SamlSettings}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
<Route path='security'>
|
<Route path='security'>
|
||||||
<IndexRedirect to='sign_up'/>
|
<IndexRedirect to='sign_up'/>
|
||||||
|
|||||||
@@ -132,6 +132,12 @@
|
|||||||
.btn {
|
.btn {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.remove-filename {
|
||||||
|
margin-bottom: 5px;
|
||||||
|
top: -2px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
.alert {
|
||||||
|
|||||||
@@ -280,6 +280,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.saml {
|
||||||
|
background: #dd4b39;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: darken(#dd4b39, 10%);
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.btn-full {
|
&.btn-full {
|
||||||
padding-left: 35px;
|
padding-left: 35px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ export default {
|
|||||||
GOOGLE_SERVICE: 'google',
|
GOOGLE_SERVICE: 'google',
|
||||||
EMAIL_SERVICE: 'email',
|
EMAIL_SERVICE: 'email',
|
||||||
LDAP_SERVICE: 'ldap',
|
LDAP_SERVICE: 'ldap',
|
||||||
|
SAML_SERVICE: 'saml',
|
||||||
USERNAME_SERVICE: 'username',
|
USERNAME_SERVICE: 'username',
|
||||||
SIGNIN_CHANGE: 'signin_change',
|
SIGNIN_CHANGE: 'signin_change',
|
||||||
PASSWORD_CHANGE: 'password_change',
|
PASSWORD_CHANGE: 'password_change',
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user