Skip to content

Commit

Permalink
backport #35584 to branch/v14
Browse files Browse the repository at this point in the history
  • Loading branch information
flyinghermit committed Dec 18, 2023
1 parent ad0365b commit be48b3a
Show file tree
Hide file tree
Showing 4 changed files with 2,004 additions and 1,547 deletions.
14 changes: 14 additions & 0 deletions api/proto/teleport/legacy/types/types.proto
Original file line number Diff line number Diff line change
Expand Up @@ -5765,6 +5765,20 @@ message SAMLIdPServiceProviderSpecV1 {
string EntityID = 2 [(gogoproto.jsontag) = "entity_id"];
// ACSURL is the endpoint where SAML authentication response will be redirected.
string ACSURL = 3 [(gogoproto.jsontag) = "acs_url"];
// AttributeMapping is used to map Service Provider requested attributes to
// username, role and traits in Teleport.
repeated SAMLAttributeMapping AttributeMapping = 4 [(gogoproto.jsontag) = "attribute_mapping"];
}

// SAMLAttributeMapping represents SAML Service Provider requested attribute
// name, format and its values.
message SAMLAttributeMapping {
// name is an attribute name.
string name = 1 [(gogoproto.jsontag) = "name"];
// name_format is an attribute name format.
string name_format = 2 [(gogoproto.jsontag) = "name_format"];
// value is an attribute value definable with predicate expression.
string value = 3 [(gogoproto.jsontag) = "value"];
}

// IdPOptions specify options related to access Teleport IdPs.
Expand Down
53 changes: 53 additions & 0 deletions api/types/saml_idp_service_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,20 @@ import (
"github.com/gravitational/teleport/api/utils"
)

const (
unspecifiedNameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified"
uriNameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
basicNameFormat = "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"
)

var (
// ErrMissingEntityDescriptorAndEntityID is returned when both entity descriptor and entity ID is empty.
ErrEmptyEntityDescriptorAndEntityID = &trace.BadParameterError{Message: "either entity_descriptor or entity_id must be provided"}
// ErrMissingEntityDescriptorAndACSURL is returned when both entity descriptor and ACS URL is empty.
ErrEmptyEntityDescriptorAndACSURL = &trace.BadParameterError{Message: "either entity_descriptor or acs_url must be provided"}
// ErrDuplicateAttributeName is returned when attribute mapping declares two or more
// attributes with the same name.
ErrDuplicateAttributeName = &trace.BadParameterError{Message: "duplicate attribute name not allowed"}
)

// SAMLIdPServiceProvider specifies configuration for service providers for Teleport's built in SAML IdP.
Expand All @@ -51,6 +60,10 @@ type SAMLIdPServiceProvider interface {
GetACSURL() string
// SetACSURL sets the ACS URL.
SetACSURL(string)
// GetAttributeMapping returns Attribute Mapping.
GetAttributeMapping() []*SAMLAttributeMapping
// SetAttributeMapping sets Attribute Mapping.
SetAttributeMapping([]*SAMLAttributeMapping)
// Copy returns a copy of this saml idp service provider object.
Copy() SAMLIdPServiceProvider
// CloneResource returns a copy of the SAMLIdPServiceProvider as a ResourceWithLabels
Expand Down Expand Up @@ -103,6 +116,16 @@ func (s *SAMLIdPServiceProviderV1) SetACSURL(acsURL string) {
s.Spec.ACSURL = acsURL
}

// GetAttributeMapping returns the Attribute Mapping.
func (s *SAMLIdPServiceProviderV1) GetAttributeMapping() []*SAMLAttributeMapping {
return s.Spec.AttributeMapping
}

// SetAttributeMapping sets Attribute Mapping.
func (s *SAMLIdPServiceProviderV1) SetAttributeMapping(attrMaps []*SAMLAttributeMapping) {
s.Spec.AttributeMapping = attrMaps
}

// String returns the SAML IdP service provider string representation.
func (s *SAMLIdPServiceProviderV1) String() string {
return fmt.Sprintf("SAMLIdPServiceProviderV1(Name=%v)",
Expand Down Expand Up @@ -160,6 +183,18 @@ func (s *SAMLIdPServiceProviderV1) CheckAndSetDefaults() error {
s.Spec.EntityID = ed.EntityID
}

attrNames := make(map[string]struct{})
for _, attributeMap := range s.GetAttributeMapping() {
if err := attributeMap.CheckAndSetDefaults(); err != nil {
return trace.Wrap(err)
}
// check for duplicate attribute names
if _, ok := attrNames[attributeMap.Name]; ok {
return trace.Wrap(ErrDuplicateAttributeName)
}
attrNames[attributeMap.Name] = struct{}{}
}

return nil
}

Expand All @@ -183,3 +218,21 @@ func (s SAMLIdPServiceProviders) Less(i, j int) bool { return s[i].GetName() < s

// Swap swaps two service providers.
func (s SAMLIdPServiceProviders) Swap(i, j int) { s[i], s[j] = s[j], s[i] }

// CheckAndSetDefaults check and sets SAMLAttributeMapping default values
func (am *SAMLAttributeMapping) CheckAndSetDefaults() error {
// verify name format is one of the supported
// formats - unspecifiedNameFormat, basicNameFormat or uriNameFormat
// and assign it with the URN value of that format.
switch am.NameFormat {
case "", "unspecified", unspecifiedNameFormat:
am.NameFormat = unspecifiedNameFormat
case "basic", basicNameFormat:
am.NameFormat = basicNameFormat
case "uri", uriNameFormat:
am.NameFormat = uriNameFormat
default:
return trace.BadParameter("invalid name format: %s", am.NameFormat)
}
return nil
}
63 changes: 63 additions & 0 deletions api/types/saml_idp_service_provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ func TestNewSAMLIdPServiceProvider(t *testing.T) {
acsURL string
errAssertion require.ErrorAssertionFunc
expectedEntityID string
attributeMapping []*SAMLAttributeMapping
}{
{
name: "valid entity descriptor",
Expand Down Expand Up @@ -82,6 +83,64 @@ func TestNewSAMLIdPServiceProvider(t *testing.T) {
errAssertion: require.NoError,
expectedEntityID: "IAMShowcase",
},
{
name: "duplicate attribute mapping",
entityDescriptor: testEntityDescriptor,
attributeMapping: []*SAMLAttributeMapping{
{
Name: "username",
Value: "user.traits.name",
},
{
Name: "user1",
Value: "user.traits.firstname",
},
{
Name: "username",
Value: "user.traits.givenname",
},
},
errAssertion: func(t require.TestingT, err error, i ...interface{}) {
require.ErrorIs(t, err, ErrDuplicateAttributeName)
},
},
{
name: "valid attribute mapping",
entityDescriptor: testEntityDescriptor,
entityID: "IAMShowcase",
expectedEntityID: "IAMShowcase",
attributeMapping: []*SAMLAttributeMapping{
{
Name: "username",
Value: "user.traits.name",
},
{
Name: "user1",
Value: "user.traits.givenname",
},
},
errAssertion: require.NoError,
},
{
name: "invalid attribute mapping name format",
entityDescriptor: testEntityDescriptor,
entityID: "IAMShowcase",
expectedEntityID: "IAMShowcase",
attributeMapping: []*SAMLAttributeMapping{
{
Name: "username",
Value: "user.traits.name",
NameFormat: "emailAddress",
},
{
Name: "user1",
Value: "user.traits.givenname",
},
},
errAssertion: func(t require.TestingT, err error, i ...interface{}) {
require.ErrorContains(t, err, "invalid name format")
},
},
}

for _, test := range tests {
Expand All @@ -92,11 +151,15 @@ func TestNewSAMLIdPServiceProvider(t *testing.T) {
EntityDescriptor: test.entityDescriptor,
EntityID: test.entityID,
ACSURL: test.acsURL,
AttributeMapping: test.attributeMapping,
})

test.errAssertion(t, err)
if sp != nil {
require.Equal(t, test.expectedEntityID, sp.GetEntityID())
if len(sp.GetAttributeMapping()) > 0 {
require.Equal(t, test.attributeMapping, sp.GetAttributeMapping())
}
}
})
}
Expand Down

0 comments on commit be48b3a

Please sign in to comment.