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 (
|
||||
"bufio"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -41,6 +42,9 @@ func InitAdmin() {
|
||||
BaseRoutes.Admin.Handle("/reset_mfa", ApiAdminSystemRequired(adminResetMfa)).Methods("POST")
|
||||
BaseRoutes.Admin.Handle("/reset_password", ApiAdminSystemRequired(adminResetPassword)).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) {
|
||||
@@ -582,3 +586,76 @@ func ldapSyncNow(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
rdata["status"] = "ok"
|
||||
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"
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
} 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
|
||||
return user, err
|
||||
} else {
|
||||
|
||||
@@ -477,6 +477,11 @@ func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request)
|
||||
link := "/"
|
||||
linkMessage := T("api.templates.error.link")
|
||||
|
||||
status := http.StatusTemporaryRedirect
|
||||
if err.StatusCode != http.StatusInternalServerError {
|
||||
status = err.StatusCode
|
||||
}
|
||||
|
||||
http.Redirect(
|
||||
w,
|
||||
r,
|
||||
@@ -485,7 +490,7 @@ func RenderWebError(err *model.AppError, w http.ResponseWriter, r *http.Request)
|
||||
"&details="+url.QueryEscape(details)+
|
||||
"&link="+url.QueryEscape(link)+
|
||||
"&linkmessage="+url.QueryEscape(linkMessage),
|
||||
http.StatusTemporaryRedirect)
|
||||
status)
|
||||
}
|
||||
|
||||
func Handle404(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
106
api/user.go
106
api/user.go
@@ -5,6 +5,7 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
b64 "encoding/base64"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"html/template"
|
||||
@@ -71,6 +72,9 @@ func InitUser() {
|
||||
BaseRoutes.NeedUser.Handle("/sessions", ApiUserRequired(getSessions)).Methods("GET")
|
||||
BaseRoutes.NeedUser.Handle("/audits", ApiUserRequired(getAudits)).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) {
|
||||
@@ -2005,12 +2009,16 @@ func emailToOAuth(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
stateProps["email"] = email
|
||||
|
||||
m := map[string]string{}
|
||||
if authUrl, err := GetAuthorizationCode(c, service, stateProps, ""); err != nil {
|
||||
c.LogAuditWithUserId(user.Id, "fail - oauth issue")
|
||||
c.Err = err
|
||||
return
|
||||
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 {
|
||||
m["follow_link"] = authUrl
|
||||
if authUrl, err := GetAuthorizationCode(c, service, stateProps, ""); err != nil {
|
||||
c.LogAuditWithUserId(user.Id, "fail - oauth issue")
|
||||
c.Err = err
|
||||
return
|
||||
} else {
|
||||
m["follow_link"] = authUrl
|
||||
}
|
||||
}
|
||||
|
||||
c.LogAuditWithUserId(user.Id, "success")
|
||||
@@ -2419,3 +2427,91 @@ func checkMfa(c *Context, w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
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",
|
||||
"DefaultClientLocale": "en",
|
||||
"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
|
||||
- name: github.com/gorilla/websocket
|
||||
version: 1f512fc3f05332ba7117626cdfb4e07474e58e60
|
||||
- name: github.com/kardianos/osext
|
||||
version: 29ae4ffbc9a6fe9fb2bc5029050ce6996ea1d3bc
|
||||
- name: github.com/lib/pq
|
||||
version: ee1442bda7bd1b6a84e913bdb421cb1874ec629d
|
||||
subpackages:
|
||||
|
||||
@@ -39,3 +39,4 @@ import:
|
||||
- package: gopkg.in/throttled/throttled.v1
|
||||
subpackages:
|
||||
- store
|
||||
- package: github.com/kardianos/osext
|
||||
|
||||
148
i18n/en.json
148
i18n/en.json
@@ -47,6 +47,22 @@
|
||||
"id": "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",
|
||||
"translation": "Error reading log file"
|
||||
@@ -71,6 +87,14 @@
|
||||
"id": "api.admin.recycle_db_start.warn",
|
||||
"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",
|
||||
"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",
|
||||
"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",
|
||||
"translation": "Server is initializing..."
|
||||
@@ -1803,6 +1831,10 @@
|
||||
"id": "api.user.reset_password.wrong_team.app_error",
|
||||
"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",
|
||||
"translation": "Failed to send email change notification email successfully err=%v"
|
||||
@@ -2171,6 +2203,74 @@
|
||||
"id": "ent.mfa.validate_token.authenticate.app_error",
|
||||
"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",
|
||||
"translation": "Back to Mattermost"
|
||||
@@ -2571,6 +2671,46 @@
|
||||
"id": "model.config.is_valid.restrict_direct_message.app_error",
|
||||
"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",
|
||||
"translation": "Invalid data source for SQL settings. Must be set."
|
||||
@@ -3795,6 +3935,10 @@
|
||||
"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."
|
||||
},
|
||||
{
|
||||
"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",
|
||||
"translation": "Must call update for exisiting user"
|
||||
@@ -3815,6 +3959,10 @@
|
||||
"id": "store.sql_user.save.username_exists.ldap_app_error",
|
||||
"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",
|
||||
"translation": "We couldn't update the account"
|
||||
|
||||
156
model/config.go
156
model/config.go
@@ -227,6 +227,31 @@ type LocalizationSettings struct {
|
||||
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 {
|
||||
ServiceSettings ServiceSettings
|
||||
TeamSettings TeamSettings
|
||||
@@ -242,6 +267,7 @@ type Config struct {
|
||||
LdapSettings LdapSettings
|
||||
ComplianceSettings ComplianceSettings
|
||||
LocalizationSettings LocalizationSettings
|
||||
SamlSettings SamlSettings
|
||||
}
|
||||
|
||||
func (o *Config) ToJson() string {
|
||||
@@ -627,6 +653,86 @@ func (o *Config) SetDefaults() {
|
||||
o.LocalizationSettings.AvailableLocales = new(string)
|
||||
*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 {
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ type Features struct {
|
||||
Compliance *bool `json:"compliance"`
|
||||
CustomBrand *bool `json:"custom_brand"`
|
||||
MHPNS *bool `json:"mhpns"`
|
||||
SAML *bool `json:"saml"`
|
||||
FutureFeatures *bool `json:"future_features"`
|
||||
}
|
||||
|
||||
@@ -82,6 +83,11 @@ func (f *Features) SetDefaults() {
|
||||
f.MHPNS = new(bool)
|
||||
*f.MHPNS = *f.FutureFeatures
|
||||
}
|
||||
|
||||
if f.SAML == nil {
|
||||
f.SAML = new(bool)
|
||||
*f.SAML = *f.FutureFeatures
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
ldapI.StartLdapSyncJob()
|
||||
}
|
||||
|
||||
if samlI := einterfaces.GetSamlInterface(); samlI != nil {
|
||||
samlI.ConfigureSP()
|
||||
}
|
||||
}
|
||||
|
||||
func getClientConfig(c *model.Config) map[string]string {
|
||||
@@ -277,6 +281,11 @@ func getClientConfig(c *model.Config) map[string]string {
|
||||
if *License.Features.Compliance {
|
||||
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
|
||||
|
||||
@@ -121,6 +121,7 @@ func getClientLicense(l *model.License) map[string]string {
|
||||
props["Users"] = strconv.Itoa(*l.Features.Users)
|
||||
props["LDAP"] = strconv.FormatBool(*l.Features.LDAP)
|
||||
props["MFA"] = strconv.FormatBool(*l.Features.MFA)
|
||||
props["SAML"] = strconv.FormatBool(*l.Features.SAML)
|
||||
props["GoogleSSO"] = strconv.FormatBool(*l.Features.GoogleSSO)
|
||||
props["Compliance"] = strconv.FormatBool(*l.Features.Compliance)
|
||||
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() {
|
||||
let ldapSettings = null;
|
||||
let samlSettings = null;
|
||||
let complianceSettings = 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') {
|
||||
complianceSettings = (
|
||||
<AdminSidebarSection
|
||||
@@ -391,6 +406,7 @@ export default class AdminSidebar extends React.Component {
|
||||
}
|
||||
/>
|
||||
{ldapSettings}
|
||||
{samlSettings}
|
||||
</AdminSidebarSection>
|
||||
<AdminSidebarSection
|
||||
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.
|
||||
|
||||
import Client from 'utils/web_client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import UserStore from 'stores/user_store.jsx';
|
||||
import ConfirmModal from '../confirm_modal.jsx';
|
||||
@@ -374,12 +375,13 @@ export default class UserItem extends React.Component {
|
||||
let authServiceText;
|
||||
let passwordReset;
|
||||
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 = (
|
||||
<FormattedHTMLMessage
|
||||
id='admin.user_item.authServiceNotEmail'
|
||||
defaultMessage=', <strong>Sign-in Method:</strong> {service}'
|
||||
values={{
|
||||
service: Utils.toTitleCase(user.auth_service)
|
||||
service
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Client from 'utils/web_client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
@@ -55,7 +56,8 @@ export default class EmailToOAuth extends React.Component {
|
||||
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 (
|
||||
<div>
|
||||
@@ -74,7 +76,7 @@ export default class EmailToOAuth extends React.Component {
|
||||
id='claim.email_to_oauth.ssoType'
|
||||
defaultMessage='Upon claiming your account, you will only be able to login with {type} SSO'
|
||||
values={{
|
||||
type: Utils.toTitleCase(this.props.newType)
|
||||
type
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
@@ -83,7 +85,7 @@ export default class EmailToOAuth extends React.Component {
|
||||
id='claim.email_to_oauth.ssoNote'
|
||||
defaultMessage='You must already have a valid {type} account'
|
||||
values={{
|
||||
type: Utils.toTitleCase(this.props.newType)
|
||||
type
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import * as Utils from 'utils/utils.jsx';
|
||||
import Client from 'utils/web_client.jsx';
|
||||
import Constants from 'utils/constants.jsx';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
@@ -62,7 +63,7 @@ export default class OAuthToEmail extends React.Component {
|
||||
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 (
|
||||
<div>
|
||||
@@ -85,7 +86,7 @@ export default class OAuthToEmail extends React.Component {
|
||||
<p>
|
||||
<FormattedMessage
|
||||
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={{
|
||||
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',
|
||||
usernameSigninEnabled: global.window.mm_config.EnableSignInWithUsername === '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
|
||||
password: '',
|
||||
showMfa: false
|
||||
@@ -319,6 +320,7 @@ export default class LoginController extends React.Component {
|
||||
const ldapEnabled = this.state.ldapEnabled;
|
||||
const gitlabSigninEnabled = global.window.mm_config.EnableSignUpWithGitLab === 'true';
|
||||
const googleSigninEnabled = global.window.mm_config.EnableSignUpWithGoogle === 'true';
|
||||
const samlSigninEnabled = this.state.samlEnabled;
|
||||
const usernameSigninEnabled = this.state.usernameSigninEnabled;
|
||||
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(
|
||||
<div
|
||||
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 (
|
||||
<div>
|
||||
{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;
|
||||
if (global.window.mm_config.EnableLdap === 'true' && global.window.mm_license.IsLicensed === 'true' && global.window.mm_license.LDAP) {
|
||||
ldapSignup = (
|
||||
|
||||
@@ -412,6 +412,24 @@ class UserSettingsGeneralTab extends React.Component {
|
||||
{helpText}
|
||||
</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 = (
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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 = [];
|
||||
inputs.push(
|
||||
<div key='userSignInOption'>
|
||||
@@ -627,6 +645,7 @@ class SecurityTab extends React.Component {
|
||||
{gitlabOption}
|
||||
<br/>
|
||||
{ldapOption}
|
||||
{samlOption}
|
||||
{googleOption}
|
||||
</div>
|
||||
);
|
||||
@@ -681,6 +700,13 @@ class SecurityTab extends React.Component {
|
||||
defaultMessage='LDAP'
|
||||
/>
|
||||
);
|
||||
} else if (this.props.user.auth_service === Constants.SAML_SERVICE) {
|
||||
describe = (
|
||||
<FormattedMessage
|
||||
id='user.settings.security.saml'
|
||||
defaultMessage='SAML'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
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.EnableSignUpWithGoogle === '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;
|
||||
if (global.window.mm_config.EnableSignUpWithEmail === 'true' && numMethods > 0) {
|
||||
|
||||
@@ -102,6 +102,7 @@
|
||||
"admin.audits.title": "User Activity Logs",
|
||||
"admin.authentication.email": "Email Auth",
|
||||
"admin.authentication.gitlab": "GitLab",
|
||||
"admin.authentication.saml": "SAML",
|
||||
"admin.banner.heading": "Note:",
|
||||
"admin.compliance.directoryDescription": "Directory to which compliance reports are written. If blank, will be set to ./data/.",
|
||||
"admin.compliance.directoryExample": "Ex \"./data/\"",
|
||||
@@ -217,6 +218,9 @@
|
||||
"admin.email.smtpUsernameTitle": "SMTP Server Username:",
|
||||
"admin.email.testing": "Testing...",
|
||||
"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.storage": "Storage",
|
||||
"admin.general.configuration": "Configuration",
|
||||
@@ -431,6 +435,58 @@
|
||||
"admin.reset_password.submit": "Please enter at least {chars} characters.",
|
||||
"admin.reset_password.titleReset": "Reset 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.saving": "Saving Config...",
|
||||
"admin.security.connection": "Connections",
|
||||
@@ -522,6 +578,7 @@
|
||||
"admin.sidebar.rateLimiting": "Rate Limiting",
|
||||
"admin.sidebar.reports": "REPORTING",
|
||||
"admin.sidebar.rmTeamSidebar": "Remove team from sidebar menu",
|
||||
"admin.sidebar.saml": "SAML",
|
||||
"admin.sidebar.security": "Security",
|
||||
"admin.sidebar.sessions": "Sessions",
|
||||
"admin.sidebar.settings": "SETTINGS",
|
||||
@@ -842,7 +899,7 @@
|
||||
"claim.ldap_to_email.title": "Switch LDAP Account to Email/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.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.newPwd": "New Password",
|
||||
"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.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.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.emptyName": "Click 'Edit' to add your full name",
|
||||
"user.settings.general.emptyNickname": "Click 'Edit' to add a nickname",
|
||||
@@ -1465,6 +1523,7 @@
|
||||
"user.settings.general.lastName": "Last Name",
|
||||
"user.settings.general.loginGitlab": "Login done through GitLab ({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.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.",
|
||||
@@ -1552,10 +1611,12 @@
|
||||
"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.retypePassword": "Retype New Password",
|
||||
"user.settings.security.saml": "SAML",
|
||||
"user.settings.security.switchEmail": "Switch to using email and password",
|
||||
"user.settings.security.switchGitlab": "Switch to using GitLab SSO",
|
||||
"user.settings.security.switchGoogle": "Switch to using Google SSO",
|
||||
"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.viewHistory": "View Access History",
|
||||
"user_list.notFound": "No users found",
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"keymirror": "0.1.1",
|
||||
"marked": "mattermost/marked#12d2be4cdf54d4ec95fead934e18840b6a2c1a7b",
|
||||
"match-at": "0.1.0",
|
||||
"mattermost": "mattermost/mattermost-javascript#8e4c320d5af653eacb248455d77057a76ec28830",
|
||||
"mattermost": "mattermost/mattermost-javascript#798c39c5d302d2d109e768a35575ebdbf2a8ee6a",
|
||||
"object-assign": "4.1.0",
|
||||
"perfect-scrollbar": "0.6.11",
|
||||
"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 GitLabSettings from 'components/admin_console/gitlab_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 LoginSettings from 'components/admin_console/login_settings.jsx';
|
||||
import PublicLinkSettings from 'components/admin_console/public_link_settings.jsx';
|
||||
@@ -90,6 +91,10 @@ export default (
|
||||
path='ldap'
|
||||
component={LdapSettings}
|
||||
/>
|
||||
<Route
|
||||
path='saml'
|
||||
component={SamlSettings}
|
||||
/>
|
||||
</Route>
|
||||
<Route path='security'>
|
||||
<IndexRedirect to='sign_up'/>
|
||||
|
||||
@@ -132,6 +132,12 @@
|
||||
.btn {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
&.remove-filename {
|
||||
margin-bottom: 5px;
|
||||
top: -2px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
|
||||
.alert {
|
||||
|
||||
@@ -280,6 +280,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.saml {
|
||||
background: #dd4b39;
|
||||
|
||||
&:hover {
|
||||
background: darken(#dd4b39, 10%);
|
||||
}
|
||||
|
||||
span {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-full {
|
||||
padding-left: 35px;
|
||||
text-align: left;
|
||||
|
||||
@@ -227,6 +227,7 @@ export default {
|
||||
GOOGLE_SERVICE: 'google',
|
||||
EMAIL_SERVICE: 'email',
|
||||
LDAP_SERVICE: 'ldap',
|
||||
SAML_SERVICE: 'saml',
|
||||
USERNAME_SERVICE: 'username',
|
||||
SIGNIN_CHANGE: 'signin_change',
|
||||
PASSWORD_CHANGE: 'password_change',
|
||||
|
||||
Ссылка в новой задаче
Block a user