Skip to content

Commit 8efeec2

Browse files
authored
Add "extension" attribute validation to IdP SPs (#129396)
This extends the change from #128176 to validate the "custom attributes" on a per Service Provider basis. Each Service Provider (whether registered or wildcard based) has a field "attributes.extensions" which is a list of attribute names that may be provided by the caller of "/_idp/saml/init". Service Providers that have not be configured with extension attributes will reject any custom attributes in SAML init. This necessitates a new field in the service provider index (but only if the new `extensions` attribute is set). The template has been updated, but there is no data migration because the `saml-service-provider` index does not exist in any of the environments into which we wish to deploy this change. Backport of: #128805, #129233
1 parent 5d2b59e commit 8efeec2

File tree

13 files changed

+165
-17
lines changed

13 files changed

+165
-17
lines changed

docs/changelog/128805.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 128805
2+
summary: Add "extension" attribute validation to IdP SPs
3+
area: IdentityProvider
4+
type: enhancement
5+
issues: []

server/src/main/java/org/elasticsearch/TransportVersions.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ static TransportVersion def(int id) {
240240
public static final TransportVersion ML_INFERENCE_MISTRAL_CHAT_COMPLETION_ADDED_8_19 = def(8_841_0_47);
241241
public static final TransportVersion ML_INFERENCE_ELASTIC_RERANK_ADDED_8_19 = def(8_841_0_48);
242242
public static final TransportVersion NONE_CHUNKING_STRATEGY_8_19 = def(8_841_0_49);
243+
public static final TransportVersion IDP_CUSTOM_SAML_ATTRIBUTES_ALLOW_LIST_8_19 = def(8_841_0_50);
243244

244245
/*
245246
* STOP! READ THIS FIRST! No, really,

x-pack/plugin/core/template-resources/src/main/resources/idp/saml-service-provider-template.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@
7575
},
7676
"roles": {
7777
"type": "keyword"
78+
},
79+
"extensions": {
80+
"type": "keyword"
7881
}
7982
}
8083
},

x-pack/plugin/identity-provider/qa/idp-rest-tests/src/javaRestTest/java/org/elasticsearch/xpack/idp/IdentityProviderAuthenticationIT.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ public void testCustomAttributesInIdpInitiatedSso() throws Exception {
147147
Map.entry("principal", "https://idp.test.es.elasticsearch.org/attribute/principal"),
148148
Map.entry("name", "https://idp.test.es.elasticsearch.org/attribute/name"),
149149
Map.entry("email", "https://idp.test.es.elasticsearch.org/attribute/email"),
150-
Map.entry("roles", "https://idp.test.es.elasticsearch.org/attribute/roles")
150+
Map.entry("roles", "https://idp.test.es.elasticsearch.org/attribute/roles"),
151+
Map.entry("extensions", List.of("department", "region"))
151152
)
152153
)
153154
);

x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/authn/SuccessfulAuthenticationResponseMessageBuilder.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import java.util.Map;
4949
import java.util.Set;
5050

51+
import static org.elasticsearch.common.Strings.collectionToCommaDelimitedString;
5152
import static org.opensaml.saml.saml2.core.NameIDType.TRANSIENT;
5253

5354
/**
@@ -214,28 +215,42 @@ private AttributeStatement buildAttributes(
214215
final SamlServiceProvider serviceProvider = user.getServiceProvider();
215216
final AttributeStatement statement = samlFactory.object(AttributeStatement.class, AttributeStatement.DEFAULT_ELEMENT_NAME);
216217
final List<Attribute> attributes = new ArrayList<>();
217-
final Attribute roles = buildAttribute(serviceProvider.getAttributeNames().roles, "roles", user.getRoles());
218+
final SamlServiceProvider.AttributeNames attributeNames = serviceProvider.getAttributeNames();
219+
final Attribute roles = buildAttribute(attributeNames.roles, "roles", user.getRoles());
218220
if (roles != null) {
219221
attributes.add(roles);
220222
}
221-
final Attribute principal = buildAttribute(serviceProvider.getAttributeNames().principal, "principal", user.getPrincipal());
223+
final Attribute principal = buildAttribute(attributeNames.principal, "principal", user.getPrincipal());
222224
if (principal != null) {
223225
attributes.add(principal);
224226
}
225-
final Attribute email = buildAttribute(serviceProvider.getAttributeNames().email, "email", user.getEmail());
227+
final Attribute email = buildAttribute(attributeNames.email, "email", user.getEmail());
226228
if (email != null) {
227229
attributes.add(email);
228230
}
229-
final Attribute name = buildAttribute(serviceProvider.getAttributeNames().name, "name", user.getName());
231+
final Attribute name = buildAttribute(attributeNames.name, "name", user.getName());
230232
if (name != null) {
231233
attributes.add(name);
232234
}
233235
// Add custom attributes if provided
234236
if (customAttributes != null && customAttributes.getAttributes().isEmpty() == false) {
235237
for (Map.Entry<String, List<String>> entry : customAttributes.getAttributes().entrySet()) {
236-
Attribute attribute = buildAttribute(entry.getKey(), null, entry.getValue());
237-
if (attribute != null) {
238-
attributes.add(attribute);
238+
final String attributeName = entry.getKey();
239+
if (attributeNames.isAllowedExtension(attributeName)) {
240+
Attribute attribute = buildAttribute(attributeName, null, entry.getValue());
241+
if (attribute != null) {
242+
attributes.add(attribute);
243+
}
244+
} else {
245+
throw new IllegalArgumentException(
246+
"the custom attribute ["
247+
+ attributeName
248+
+ "] is not permitted for the Service Provider ["
249+
+ serviceProvider.getName()
250+
+ "], allowed attribute names are ["
251+
+ collectionToCommaDelimitedString(attributeNames.allowedExtensions)
252+
+ "]"
253+
);
239254
}
240255
}
241256
}

x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProvider.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,18 @@ class AttributeNames {
3636
public final String name;
3737
public final String email;
3838
public final String roles;
39+
public final Set<String> allowedExtensions;
3940

40-
public AttributeNames(String principal, String name, String email, String roles) {
41+
public AttributeNames(String principal, String name, String email, String roles, Set<String> allowedExtensions) {
4142
this.principal = principal;
4243
this.name = name;
4344
this.email = email;
4445
this.roles = roles;
46+
this.allowedExtensions = allowedExtensions == null ? Set.of() : Set.copyOf(allowedExtensions);
47+
}
48+
49+
public boolean isAllowedExtension(String attributeName) {
50+
return this.allowedExtensions.contains(attributeName);
4551
}
4652
}
4753

x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderDocument.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
import java.util.TreeSet;
4343
import java.util.function.BiConsumer;
4444

45+
import static org.elasticsearch.TransportVersions.IDP_CUSTOM_SAML_ATTRIBUTES_ALLOW_LIST_8_19;
46+
4547
/**
4648
* This class models the storage of a {@link SamlServiceProvider} as an Elasticsearch document.
4749
*/
@@ -87,6 +89,12 @@ public static class AttributeNames {
8789
@Nullable
8890
public String roles;
8991

92+
/**
93+
* Extensions are attributes that are provided at runtime (by the trusted client that initiates
94+
* the SAML SSO. They are sourced from the rest request rather than the user object itself.
95+
*/
96+
public Set<String> extensions = Set.of();
97+
9098
public void setPrincipal(String principal) {
9199
this.principal = principal;
92100
}
@@ -103,6 +111,10 @@ public void setRoles(String roles) {
103111
this.roles = roles;
104112
}
105113

114+
public void setExtensions(Collection<String> names) {
115+
this.extensions = names == null ? Set.of() : Set.copyOf(names);
116+
}
117+
106118
@Override
107119
public boolean equals(Object o) {
108120
if (this == o) return true;
@@ -111,7 +123,8 @@ public boolean equals(Object o) {
111123
return Objects.equals(principal, that.principal)
112124
&& Objects.equals(email, that.email)
113125
&& Objects.equals(name, that.name)
114-
&& Objects.equals(roles, that.roles);
126+
&& Objects.equals(roles, that.roles)
127+
&& Objects.equals(extensions, that.extensions);
115128
}
116129

117130
@Override
@@ -263,6 +276,10 @@ public SamlServiceProviderDocument(StreamInput in) throws IOException {
263276
attributeNames.name = in.readOptionalString();
264277
attributeNames.roles = in.readOptionalString();
265278

279+
if (in.getTransportVersion().onOrAfter(IDP_CUSTOM_SAML_ATTRIBUTES_ALLOW_LIST_8_19)) {
280+
attributeNames.extensions = in.readCollectionAsImmutableSet(StreamInput::readString);
281+
}
282+
266283
certificates.serviceProviderSigning = in.readStringCollectionAsList();
267284
certificates.identityProviderSigning = in.readStringCollectionAsList();
268285
certificates.identityProviderMetadataSigning = in.readStringCollectionAsList();
@@ -288,6 +305,10 @@ public void writeTo(StreamOutput out) throws IOException {
288305
out.writeOptionalString(attributeNames.name);
289306
out.writeOptionalString(attributeNames.roles);
290307

308+
if (out.getTransportVersion().onOrAfter(IDP_CUSTOM_SAML_ATTRIBUTES_ALLOW_LIST_8_19)) {
309+
out.writeStringCollection(attributeNames.extensions);
310+
}
311+
291312
out.writeStringCollection(certificates.serviceProviderSigning);
292313
out.writeStringCollection(certificates.identityProviderSigning);
293314
out.writeStringCollection(certificates.identityProviderMetadataSigning);
@@ -426,6 +447,7 @@ public int hashCode() {
426447
ATTRIBUTES_PARSER.declareStringOrNull(AttributeNames::setEmail, Fields.Attributes.EMAIL);
427448
ATTRIBUTES_PARSER.declareStringOrNull(AttributeNames::setName, Fields.Attributes.NAME);
428449
ATTRIBUTES_PARSER.declareStringOrNull(AttributeNames::setRoles, Fields.Attributes.ROLES);
450+
ATTRIBUTES_PARSER.declareStringArray(AttributeNames::setExtensions, Fields.Attributes.EXTENSIONS);
429451

430452
DOC_PARSER.declareObject(NULL_CONSUMER, (p, doc) -> CERTIFICATES_PARSER.parse(p, doc.certificates, null), Fields.CERTIFICATES);
431453
CERTIFICATES_PARSER.declareStringArray(Certificates::setServiceProviderSigning, Fields.Certificates.SP_SIGNING);
@@ -516,6 +538,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
516538
builder.field(Fields.Attributes.EMAIL.getPreferredName(), attributeNames.email);
517539
builder.field(Fields.Attributes.NAME.getPreferredName(), attributeNames.name);
518540
builder.field(Fields.Attributes.ROLES.getPreferredName(), attributeNames.roles);
541+
if (attributeNames.extensions != null && attributeNames.extensions.isEmpty() == false) {
542+
builder.field(Fields.Attributes.EXTENSIONS.getPreferredName(), attributeNames.extensions);
543+
}
519544
builder.endObject();
520545

521546
builder.startObject(Fields.CERTIFICATES.getPreferredName());
@@ -553,6 +578,7 @@ interface Attributes {
553578
ParseField EMAIL = new ParseField("email");
554579
ParseField NAME = new ParseField("name");
555580
ParseField ROLES = new ParseField("roles");
581+
ParseField EXTENSIONS = new ParseField("extensions");
556582
}
557583

558584
interface Certificates {

x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderFactory.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ SamlServiceProvider buildServiceProvider(SamlServiceProviderDocument document) {
3838
document.attributeNames.principal,
3939
document.attributeNames.name,
4040
document.attributeNames.email,
41-
document.attributeNames.roles
41+
document.attributeNames.roles,
42+
document.attributeNames.extensions
4243
);
4344
final Set<X509Credential> credentials = document.certificates.getServiceProviderX509SigningCertificates()
4445
.stream()

x-pack/plugin/identity-provider/src/main/java/org/elasticsearch/xpack/idp/saml/sp/SamlServiceProviderIndex.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,26 @@ public class SamlServiceProviderIndex implements Closeable {
8080
static final String TEMPLATE_VERSION_STRING_DEPRECATED = "idp.template.version_deprecated";
8181
static final String FINAL_TEMPLATE_VERSION_STRING_DEPRECATED = "8.14.0";
8282

83-
static final int CURRENT_TEMPLATE_VERSION = 1;
83+
/**
84+
* The object in the index mapping metadata that contains a version field
85+
*/
86+
private static final String INDEX_META_FIELD = "_meta";
87+
/**
88+
* The field in the {@link #INDEX_META_FIELD} metadata that contains the version number
89+
*/
90+
private static final String TEMPLATE_VERSION_META_FIELD = "idp-template-version";
91+
92+
/**
93+
* The first version of this template (since it was moved to use {@link org.elasticsearch.xpack.core.template.IndexTemplateRegistry}
94+
*/
95+
private static final int VERSION_ORIGINAL = 1;
96+
/**
97+
* The version that added the {@code attributes.extensions} field to the SAML SP document
98+
*/
99+
private static final int VERSION_EXTENSION_ATTRIBUTES = 2;
100+
static final int CURRENT_TEMPLATE_VERSION = VERSION_EXTENSION_ATTRIBUTES;
101+
102+
private volatile boolean indexUpToDate = false;
84103

85104
public static final class DocumentVersion {
86105
public final String id;

x-pack/plugin/identity-provider/src/test/java/org/elasticsearch/xpack/idp/action/TransportSamlInitiateSingleSignOnActionTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,8 @@ private TransportSamlInitiateSingleSignOnAction setupTransportAction(boolean wit
190190
"https://saml.elasticsearch.org/attributes/principal",
191191
"https://saml.elasticsearch.org/attributes/name",
192192
"https://saml.elasticsearch.org/attributes/email",
193-
"https://saml.elasticsearch.org/attributes/roles"
193+
"https://saml.elasticsearch.org/attributes/roles",
194+
Set.of()
194195
),
195196
null,
196197
false,

0 commit comments

Comments
 (0)