* 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
Этот коммит содержится в:
enahum
2016-07-05 15:49:00 -04:00
коммит произвёл GitHub
родитель f91b9d4a65
Коммит 5f04dc4f45
39 изменённых файлов: 1983 добавлений и 16 удалений

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

@@ -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) {

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

@@ -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 Обычный файл
Просмотреть файл

@@ -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 сгенерированный
Просмотреть файл

@@ -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

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

@@ -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"

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

@@ -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 Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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 сгенерированный поставляемый Обычный файл
Просмотреть файл

@@ -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'

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

@@ -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>
);
}
}

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

@@ -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>
);
}
}

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

@@ -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',