MM-20698: Add server support for getting SAML metadata and updating S… (#13311)

* MM-20698: Add server support for getting SAMl metadata and updating SAMl settings
Этот коммит содержится в:
catalintomai
2020-01-09 21:25:20 -08:00
коммит произвёл GitHub
родитель c41e9d970a
Коммит 37ce413b7d
9 изменённых файлов: 397 добавлений и 5 удалений

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

@@ -4,6 +4,8 @@
package api4
import (
"io/ioutil"
"mime"
"mime/multipart"
"net/http"
@@ -22,6 +24,8 @@ func (api *API) InitSaml() {
api.BaseRoutes.SAML.Handle("/certificate/idp", api.ApiSessionRequired(removeSamlIdpCertificate)).Methods("DELETE")
api.BaseRoutes.SAML.Handle("/certificate/status", api.ApiSessionRequired(getSamlCertificateStatus)).Methods("GET")
api.BaseRoutes.SAML.Handle("/metadatafromidp", api.ApiHandler(getSamlMetadataFromIdp)).Methods("POST")
}
func getSamlMetadata(c *Context, w http.ResponseWriter, r *http.Request) {
@@ -100,16 +104,44 @@ func addSamlIdpCertificate(c *Context, w http.ResponseWriter, r *http.Request) {
return
}
fileData, err := parseSamlCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
v := r.Header.Get("Content-Type")
if v == "" {
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error", nil, "", http.StatusBadRequest)
return
}
d, _, err := mime.ParseMediaType(v)
if err != nil {
c.Err = err
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
if err := c.App.AddSamlIdpCertificate(fileData); err != nil {
c.Err = err
if d == "application/x-pem-file" {
body, err := ioutil.ReadAll(r.Body)
if err != nil {
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
if err := c.App.SetSamlIdpCertificateFromMetadata(body); err != nil {
c.Err = err
return
}
} else if d == "multipart/form-data" {
fileData, err := parseSamlCertificateRequest(r, *c.App.Config().FileSettings.MaxFileSize)
if err != nil {
c.Err = err
return
}
if err := c.App.AddSamlIdpCertificate(fileData); err != nil {
c.Err = err
return
}
} else {
c.Err = model.NewAppError("addSamlIdpCertificate", "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error", nil, "", http.StatusBadRequest)
return
}
ReturnStatusOK(w)
}
@@ -164,3 +196,25 @@ func getSamlCertificateStatus(c *Context, w http.ResponseWriter, r *http.Request
status := c.App.GetSamlCertificateStatus()
w.Write([]byte(status.ToJson()))
}
func getSamlMetadataFromIdp(c *Context, w http.ResponseWriter, r *http.Request) {
if !c.App.SessionHasPermissionTo(c.App.Session, model.PERMISSION_MANAGE_SYSTEM) {
c.SetPermissionError(model.PERMISSION_MANAGE_SYSTEM)
return
}
props := model.MapFromJson(r.Body)
url := props["saml_metadata_url"]
if url == "" {
c.SetInvalidParam("saml_metadata_url")
return
}
metadata, err := c.App.GetSamlMetadataFromIdp(url)
if err != nil {
c.Err = model.NewAppError("getSamlMetadataFromIdp", "api.admin.saml.failure_get_metadata_from_idp.app_error", nil, err.Error(), http.StatusBadRequest)
return
}
w.Write([]byte(metadata.ToJson()))
}

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

@@ -4282,6 +4282,7 @@ func TestLoginErrorMessage(t *testing.T) {
*cfg.SamlSettings.Encrypt = false
*cfg.SamlSettings.IdpUrl = "https://localhost/adfs/ls"
*cfg.SamlSettings.IdpDescriptorUrl = "https://localhost/adfs/services/trust"
*cfg.SamlSettings.IdpMetadataUrl = "https://localhost/adfs/metadata"
*cfg.SamlSettings.AssertionConsumerServiceURL = "https://localhost/login/sso/saml"
*cfg.SamlSettings.IdpCertificateFile = app.SamlIdpCertificateName
*cfg.SamlSettings.PrivateKeyFile = app.SamlPrivateKeyName

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

@@ -4,9 +4,14 @@
package app
import (
"crypto/x509"
"encoding/pem"
"encoding/xml"
"fmt"
"io/ioutil"
"mime/multipart"
"net/http"
"strings"
"github.com/mattermost/mattermost-server/v5/model"
)
@@ -172,3 +177,108 @@ func (a *App) GetSamlCertificateStatus() *model.SamlCertificateStatus {
return status
}
func (a *App) GetSamlMetadataFromIdp(idpMetadataUrl string) (*model.SamlMetadataResponse, *model.AppError) {
if a.Saml == nil {
err := model.NewAppError("GetSamlMetadataFromIdp", "api.admin.saml.not_available.app_error", nil, "", http.StatusNotImplemented)
return nil, err
}
if !strings.HasPrefix(idpMetadataUrl, "http://") && !strings.HasPrefix(idpMetadataUrl, "https://") {
idpMetadataUrl = "https://" + idpMetadataUrl
}
idpMetadataRaw, err := a.FetchSamlMetadataFromIdp(idpMetadataUrl)
if err != nil {
return nil, err
}
data, err := a.BuildSamlMetadataObject(idpMetadataRaw)
if err != nil {
return nil, err
}
return data, nil
}
func (a *App) FetchSamlMetadataFromIdp(url string) ([]byte, *model.AppError) {
resp, err := a.HTTPService.MakeClient(false).Get(url)
if err != nil {
return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.invalid_response_from_idp.app_error", nil, err.Error(), http.StatusBadRequest)
}
if resp.StatusCode != http.StatusOK {
return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.invalid_response_from_idp.app_error", nil, fmt.Sprintf("status_code=%d", resp.StatusCode), http.StatusBadRequest)
}
defer resp.Body.Close()
bodyXML, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, model.NewAppError("FetchSamlMetadataFromIdp", "app.admin.saml.failure_read_response_body_from_idp.app_error", nil, err.Error(), http.StatusInternalServerError)
}
return bodyXML, nil
}
func (a *App) BuildSamlMetadataObject(idpMetadata []byte) (*model.SamlMetadataResponse, *model.AppError) {
entityDescriptor := model.EntityDescriptor{}
err := xml.Unmarshal(idpMetadata, &entityDescriptor)
if err != nil {
return nil, model.NewAppError("BuildSamlMetadataObject", "app.admin.saml.failure_decode_metadata_xml_from_idp.app_error", nil, err.Error(), http.StatusInternalServerError)
}
data := &model.SamlMetadataResponse{}
data.IdpDescriptorUrl = entityDescriptor.EntityID
if entityDescriptor.IDPSSODescriptors == nil || len(entityDescriptor.IDPSSODescriptors) == 0 {
err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error", nil, "", http.StatusInternalServerError)
return nil, err
}
idpSSODescriptor := entityDescriptor.IDPSSODescriptors[0]
if idpSSODescriptor.SingleSignOnServices == nil || len(idpSSODescriptor.SingleSignOnServices) == 0 {
err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_ssoservices.app_error", nil, "", http.StatusInternalServerError)
return nil, err
}
data.IdpUrl = idpSSODescriptor.SingleSignOnServices[0].Location
if idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors == nil || len(idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors) == 0 {
err := model.NewAppError("BuildSamlMetadataObject", "api.admin.saml.invalid_xml_missing_keydescriptor.app_error", nil, "", http.StatusInternalServerError)
return nil, err
}
keyDescriptor := idpSSODescriptor.SSODescriptor.RoleDescriptor.KeyDescriptors[0]
data.IdpPublicCertificate = keyDescriptor.KeyInfo.X509Data.X509Certificate.Cert
return data, nil
}
func (a *App) SetSamlIdpCertificateFromMetadata(data []byte) *model.AppError {
const certPrefix = "-----BEGIN CERTIFICATE-----\n"
const certSuffix = "\n-----END CERTIFICATE-----"
fixedCertTxt := certPrefix + string(data) + certSuffix
block, _ := pem.Decode([]byte(fixedCertTxt))
if _, e := x509.ParseCertificate(block.Bytes); e != nil {
return model.NewAppError("SetSamlIdpCertificateFromMetadata", "api.admin.saml.failure_parse_idp_certificate.app_error", nil, e.Error(), http.StatusInternalServerError)
}
data = pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: block.Bytes,
})
if err := a.Srv.configStore.SetFile(SamlIdpCertificateName, data); err != nil {
return model.NewAppError("SetSamlIdpCertificateFromMetadata", "api.admin.saml.failure_save_idp_certificate_file.app_error", nil, err.Error(), http.StatusInternalServerError)
}
cfg := a.Config().Clone()
*cfg.SamlSettings.IdpCertificateFile = SamlIdpCertificateName
if err := cfg.IsValid(); err != nil {
return err
}
a.UpdateConfig(func(dest *model.Config) { *dest = *cfg })
return nil
}

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

@@ -83,6 +83,30 @@
"id": "api.admin.remove_certificate.delete.app_error",
"translation": "An error occurred while deleting the certificate."
},
{
"id": "api.admin.saml.failure_get_metadata_from_idp.app_error",
"translation": "Failed to obtain metadata from Identity Provider URL."
},
{
"id": "api.admin.saml.failure_parse_idp_certificate.app_error",
"translation": "Failure encountered while parsing the metadata information received from the Identity Provider to a certificate."
},
{
"id": "api.admin.saml.failure_save_idp_certificate_file.app_error",
"translation": "Could not save certificate file."
},
{
"id": "api.admin.saml.invalid_xml_missing_idpssodescriptors.app_error",
"translation": "Missing Identity Provider SSO Descriptors node in the XML."
},
{
"id": "api.admin.saml.invalid_xml_missing_keydescriptor.app_error",
"translation": "Missing Identity Provider Key Descriptors node in the XML."
},
{
"id": "api.admin.saml.invalid_xml_missing_ssoservices.app_error",
"translation": "Missing Identity Provider SSO Services node in the XML."
},
{
"id": "api.admin.saml.metadata.app_error",
"translation": "An error occurred while building Service Provider Metadata."
@@ -91,6 +115,18 @@
"id": "api.admin.saml.not_available.app_error",
"translation": "SAML 2.0 is not configured or supported on this server."
},
{
"id": "api.admin.saml.set_certificate_from_metadata.invalid_body.app_error",
"translation": "Invalid certificate text."
},
{
"id": "api.admin.saml.set_certificate_from_metadata.invalid_content_type.app_error",
"translation": "Invalid content type."
},
{
"id": "api.admin.saml.set_certificate_from_metadata.missing_content_type.app_error",
"translation": "Missing content type."
},
{
"id": "api.admin.test_email.body",
"translation": "It appears your Mattermost email is setup correctly!"
@@ -2838,6 +2874,18 @@
"id": "api.websocket_handler.server_busy.app_error",
"translation": "Server is busy, non-critical services are temporarily unavailable"
},
{
"id": "app.admin.saml.failure_decode_metadata_xml_from_idp.app_error",
"translation": "Could not decode the XML metadata information received from the Identity Provider."
},
{
"id": "app.admin.saml.failure_read_response_body_from_idp.app_error",
"translation": "Failure encountered when reading the response payload received from the Identity Provider."
},
{
"id": "app.admin.saml.invalid_response_from_idp.app_error",
"translation": "Could not read the response received from the Identity Provider."
},
{
"id": "app.admin.test_email.failure",
"translation": "Connection unsuccessful: {{.Error}}"

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

@@ -3463,6 +3463,18 @@ func (c *Client4) GetSamlCertificateStatus() (*SamlCertificateStatus, *Response)
return SamlCertificateStatusFromJson(r.Body), BuildResponse(r)
}
func (c *Client4) GetSamlMetadataFromIdp(samlMetadataURL string) (*SamlMetadataResponse, *Response) {
requestBody := make(map[string]string)
requestBody["saml_metadata_url"] = samlMetadataURL
r, err := c.DoApiPost(c.GetSamlRoute()+"/metadatafromidp", MapToJson(requestBody))
if err != nil {
return nil, BuildErrorResponse(r, err)
}
defer closeBody(r)
return SamlMetadataResponseFromJson(r.Body), BuildResponse(r)
}
// Compliance Section
// CreateComplianceReport creates an incoming webhook for a channel.

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

@@ -1913,6 +1913,7 @@ type SamlSettings struct {
IdpUrl *string
IdpDescriptorUrl *string
IdpMetadataUrl *string
AssertionConsumerServiceURL *string
SignatureAlgorithm *string
@@ -1984,6 +1985,10 @@ func (s *SamlSettings) SetDefaults() {
s.IdpDescriptorUrl = NewString("")
}
if s.IdpMetadataUrl == nil {
s.IdpMetadataUrl = NewString("")
}
if s.IdpCertificateFile == nil {
s.IdpCertificateFile = NewString("")
}

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

@@ -157,6 +157,7 @@ func TestConfigIsValidFakeAlgorithm(t *testing.T) {
*c1.SamlSettings.IdpUrl = "http://test.url.com"
*c1.SamlSettings.IdpDescriptorUrl = "http://test.url.com"
*c1.SamlSettings.IdpMetadataUrl = "http://test.url.com"
*c1.SamlSettings.IdpCertificateFile = "certificatefile"
*c1.SamlSettings.EmailAttribute = "Email"
*c1.SamlSettings.UsernameAttribute = "Username"

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

@@ -5,7 +5,9 @@ package model
import (
"encoding/json"
"encoding/xml"
"io"
"time"
)
const (
@@ -25,6 +27,153 @@ type SamlCertificateStatus struct {
PublicCertificateFile bool `json:"public_certificate_file"`
}
type SamlMetadataResponse struct {
IdpDescriptorUrl string `json:"idp_descriptor_url"`
IdpUrl string `json:"idp_url"`
IdpPublicCertificate string `json:"idp_public_certificate"`
}
type NameIDFormat struct {
XMLName xml.Name
Format string `xml:",attr,omitempty"`
Value string `xml:",innerxml"`
}
type NameID struct {
NameQualifier string `xml:",attr"`
SPNameQualifier string `xml:",attr"`
Format string `xml:",attr,omitempty"`
SPProvidedID string `xml:",attr"`
Value string `xml:",chardata"`
}
type AttributeValue struct {
Type string `xml:"http://www.w3.org/2001/XMLSchema-instance type,attr"`
Value string `xml:",chardata"`
NameID *NameID
}
type Attribute struct {
XMLName xml.Name
FriendlyName string `xml:",attr"`
Name string `xml:",attr"`
NameFormat string `xml:",attr"`
Values []AttributeValue `xml:"AttributeValue"`
}
type Endpoint struct {
XMLName xml.Name
Binding string `xml:"Binding,attr"`
Location string `xml:"Location,attr"`
ResponseLocation string `xml:"ResponseLocation,attr,omitempty"`
}
type IndexedEndpoint struct {
XMLName xml.Name
Binding string `xml:"Binding,attr"`
Location string `xml:"Location,attr"`
ResponseLocation *string `xml:"ResponseLocation,attr,omitempty"`
Index int `xml:"index,attr"`
IsDefault *bool `xml:"isDefault,attr"`
}
type IDPSSODescriptor struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata IDPSSODescriptor"`
SSODescriptor
WantAuthnRequestsSigned *bool `xml:",attr"`
SingleSignOnServices []Endpoint `xml:"SingleSignOnService"`
NameIDMappingServices []Endpoint `xml:"NameIDMappingService"`
AssertionIDRequestServices []Endpoint `xml:"AssertionIDRequestService"`
AttributeProfiles []string `xml:"AttributeProfile"`
Attributes []Attribute `xml:"Attribute"`
}
type SSODescriptor struct {
XMLName xml.Name
RoleDescriptor
ArtifactResolutionServices []IndexedEndpoint `xml:"ArtifactResolutionService"`
SingleLogoutServices []Endpoint `xml:"SingleLogoutService"`
ManageNameIDServices []Endpoint `xml:"ManageNameIDService"`
NameIDFormats []NameIDFormat `xml:"NameIDFormat"`
}
type X509Certificate struct {
XMLName xml.Name
Cert string `xml:",innerxml"`
}
type X509Data struct {
XMLName xml.Name
X509Certificate X509Certificate `xml:"X509Certificate"`
}
type KeyInfo struct {
XMLName xml.Name
DS string `xml:"xmlns:ds,attr"`
X509Data X509Data `xml:"X509Data"`
}
type EncryptionMethod struct {
Algorithm string `xml:"Algorithm,attr"`
}
type KeyDescriptor struct {
XMLName xml.Name
Use string `xml:"use,attr,omitempty"`
KeyInfo KeyInfo `xml:"http://www.w3.org/2000/09/xmldsig# KeyInfo,omitempty"`
}
type RoleDescriptor struct {
XMLName xml.Name
ID string `xml:",attr,omitempty"`
ValidUntil time.Time `xml:"validUntil,attr,omitempty"`
CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"`
ProtocolSupportEnumeration string `xml:"protocolSupportEnumeration,attr"`
ErrorURL string `xml:"errorURL,attr,omitempty"`
KeyDescriptors []KeyDescriptor `xml:"KeyDescriptor,omitempty"`
Organization *Organization `xml:"Organization,omitempty"`
ContactPersons []ContactPerson `xml:"ContactPerson,omitempty"`
}
type ContactPerson struct {
XMLName xml.Name
ContactType string `xml:"contactType,attr"`
Company string
GivenName string
SurName string
EmailAddresses []string `xml:"EmailAddress"`
TelephoneNumbers []string `xml:"TelephoneNumber"`
}
type LocalizedName struct {
Lang string `xml:"xml lang,attr"`
Value string `xml:",chardata"`
}
type LocalizedURI struct {
Lang string `xml:"xml lang,attr"`
Value string `xml:",chardata"`
}
type Organization struct {
XMLName xml.Name
OrganizationNames []LocalizedName `xml:"OrganizationName"`
OrganizationDisplayNames []LocalizedName `xml:"OrganizationDisplayName"`
OrganizationURLs []LocalizedURI `xml:"OrganizationURL"`
}
type EntityDescriptor struct {
XMLName xml.Name `xml:"urn:oasis:names:tc:SAML:2.0:metadata EntityDescriptor"`
EntityID string `xml:"entityID,attr"`
ID string `xml:",attr,omitempty"`
ValidUntil time.Time `xml:"validUntil,attr,omitempty"`
CacheDuration time.Duration `xml:"cacheDuration,attr,omitempty"`
RoleDescriptors []RoleDescriptor `xml:"RoleDescriptor"`
IDPSSODescriptors []IDPSSODescriptor `xml:"IDPSSODescriptor"`
Organization Organization `xml:"Organization"`
ContactPerson ContactPerson `xml:"ContactPerson"`
}
func (s *SamlCertificateStatus) ToJson() string {
b, _ := json.Marshal(s)
return string(b)
@@ -35,3 +184,14 @@ func SamlCertificateStatusFromJson(data io.Reader) *SamlCertificateStatus {
json.NewDecoder(data).Decode(&status)
return status
}
func (s *SamlMetadataResponse) ToJson() string {
b, _ := json.Marshal(s)
return string(b)
}
func SamlMetadataResponseFromJson(data io.Reader) *SamlMetadataResponse {
var status *SamlMetadataResponse
json.NewDecoder(data).Decode(&status)
return status
}

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

@@ -289,6 +289,7 @@
"Encrypt": true,
"IdpUrl": "",
"IdpDescriptorUrl": "",
"IdpMetadataUrl": "",
"AssertionConsumerServiceURL": "",
"ScopingIDPProviderId": "",
"ScopingIDPName": "",
@@ -401,4 +402,4 @@
"CustomUrlSchemes": [],
"ExperimentalTimezone": false
}
}
}