Skip to content

Commit acc8ae7

Browse files
authored
Add Microsoft Graph Delegated Authorization Realm Plugin (#127910)
* Add Microsoft Graph Delegated Authorization Realm Plugin * Update docs/changelog/127910.yaml
1 parent 20c02f4 commit acc8ae7

File tree

12 files changed

+352
-0
lines changed

12 files changed

+352
-0
lines changed

docs/changelog/127910.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 127910
2+
summary: Add Microsoft Graph Delegated Authorization Realm Plugin
3+
area: Authorization
4+
type: enhancement
5+
issues: []
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
apply plugin: "elasticsearch.internal-java-rest-test"
11+
12+
esplugin {
13+
name = "microsoft-graph-authz"
14+
description = "Microsoft Graph Delegated Authorization Realm Plugin"
15+
classname = "org.elasticsearch.xpack.security.authz.microsoft.MicrosoftGraphAuthzPlugin"
16+
extendedPlugins = ["x-pack-security"]
17+
}
18+
19+
dependencies {
20+
compileOnly project(":x-pack:plugin:core")
21+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import org.elasticsearch.xpack.security.authz.microsoft.MicrosoftGraphAuthzPlugin;
11+
12+
module org.elasticsearch.plugin.security.authz {
13+
requires org.elasticsearch.base;
14+
requires org.elasticsearch.server;
15+
requires org.elasticsearch.xcore;
16+
requires org.elasticsearch.logging;
17+
18+
provides org.elasticsearch.xpack.core.security.SecurityExtension with MicrosoftGraphAuthzPlugin;
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.xpack.security.authz.microsoft;
11+
12+
import org.elasticsearch.common.settings.Setting;
13+
import org.elasticsearch.plugins.Plugin;
14+
import org.elasticsearch.xpack.core.security.SecurityExtension;
15+
import org.elasticsearch.xpack.core.security.authc.Realm;
16+
17+
import java.util.List;
18+
import java.util.Map;
19+
20+
public class MicrosoftGraphAuthzPlugin extends Plugin implements SecurityExtension {
21+
@Override
22+
public Map<String, Realm.Factory> getRealms(SecurityComponents components) {
23+
return Map.of(MicrosoftGraphAuthzRealmSettings.REALM_TYPE, MicrosoftGraphAuthzRealm::new);
24+
}
25+
26+
@Override
27+
public List<Setting<?>> getSettings() {
28+
return MicrosoftGraphAuthzRealmSettings.getSettings();
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.xpack.security.authz.microsoft;
11+
12+
import org.elasticsearch.action.ActionListener;
13+
import org.elasticsearch.common.util.concurrent.ThreadContext;
14+
import org.elasticsearch.logging.LogManager;
15+
import org.elasticsearch.logging.Logger;
16+
import org.elasticsearch.xpack.core.security.authc.AuthenticationResult;
17+
import org.elasticsearch.xpack.core.security.authc.AuthenticationToken;
18+
import org.elasticsearch.xpack.core.security.authc.Realm;
19+
import org.elasticsearch.xpack.core.security.authc.RealmConfig;
20+
import org.elasticsearch.xpack.core.security.user.User;
21+
22+
public class MicrosoftGraphAuthzRealm extends Realm {
23+
24+
private static final Logger logger = LogManager.getLogger(MicrosoftGraphAuthzRealm.class);
25+
26+
public MicrosoftGraphAuthzRealm(RealmConfig config) {
27+
super(config);
28+
}
29+
30+
@Override
31+
public boolean supports(AuthenticationToken token) {
32+
return false;
33+
}
34+
35+
@Override
36+
public AuthenticationToken token(ThreadContext context) {
37+
return null;
38+
}
39+
40+
@Override
41+
public void authenticate(AuthenticationToken token, ActionListener<AuthenticationResult<User>> listener) {
42+
listener.onResponse(AuthenticationResult.notHandled());
43+
}
44+
45+
@Override
46+
public void lookupUser(String username, ActionListener<User> listener) {
47+
logger.info("Microsoft Graph Authz not yet implemented, returning empty roles for [{}]", username);
48+
listener.onResponse(new User(username));
49+
}
50+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
package org.elasticsearch.xpack.security.authz.microsoft;
11+
12+
import org.elasticsearch.common.settings.Setting;
13+
import org.elasticsearch.xpack.core.security.authc.RealmSettings;
14+
15+
import java.util.ArrayList;
16+
import java.util.List;
17+
18+
public class MicrosoftGraphAuthzRealmSettings {
19+
public static final String REALM_TYPE = "microsoft_graph";
20+
21+
public static List<Setting<?>> getSettings() {
22+
return new ArrayList<>(RealmSettings.getStandardSettings(REALM_TYPE));
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.elasticsearch.xpack.security.authz.microsoft.MicrosoftGraphAuthzPlugin
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apply plugin: 'elasticsearch.internal-java-rest-test'
2+
3+
dependencies {
4+
javaRestTestImplementation project(':x-pack:plugin:core')
5+
javaRestTestImplementation project(':x-pack:plugin:security')
6+
javaRestTestImplementation testArtifact(project(":x-pack:plugin:security:qa:saml-rest-tests"), "javaRestTest")
7+
clusterPlugins project(':plugins:microsoft-graph-authz')
8+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.security.authz.microsoft;
9+
10+
import org.elasticsearch.client.Request;
11+
import org.elasticsearch.common.Strings;
12+
import org.elasticsearch.common.settings.SecureString;
13+
import org.elasticsearch.common.settings.Settings;
14+
import org.elasticsearch.common.util.concurrent.ThreadContext;
15+
import org.elasticsearch.core.PathUtils;
16+
import org.elasticsearch.test.XContentTestUtils;
17+
import org.elasticsearch.test.cluster.ElasticsearchCluster;
18+
import org.elasticsearch.test.cluster.local.model.User;
19+
import org.elasticsearch.test.cluster.util.resource.Resource;
20+
import org.elasticsearch.test.rest.ESRestTestCase;
21+
import org.elasticsearch.test.rest.ObjectPath;
22+
import org.elasticsearch.xcontent.json.JsonXContent;
23+
import org.elasticsearch.xpack.security.authc.saml.SamlIdpMetadataBuilder;
24+
import org.elasticsearch.xpack.security.authc.saml.SamlResponseBuilder;
25+
import org.junit.ClassRule;
26+
import org.junit.rules.RuleChain;
27+
import org.junit.rules.TestRule;
28+
29+
import java.io.IOException;
30+
import java.net.URISyntaxException;
31+
import java.net.URL;
32+
import java.nio.charset.StandardCharsets;
33+
import java.security.cert.CertificateException;
34+
import java.util.Base64;
35+
import java.util.HashMap;
36+
import java.util.List;
37+
import java.util.Map;
38+
39+
import static org.hamcrest.Matchers.empty;
40+
import static org.hamcrest.Matchers.equalTo;
41+
42+
public class MicrosoftGraphAuthzPluginIT extends ESRestTestCase {
43+
public static ElasticsearchCluster cluster = initTestCluster();
44+
45+
@ClassRule
46+
public static TestRule ruleChain = RuleChain.outerRule(cluster);
47+
48+
private static final String IDP_ENTITY_ID = "http://idp.example.org/";
49+
50+
private static ElasticsearchCluster initTestCluster() {
51+
return ElasticsearchCluster.local()
52+
.setting("xpack.security.enabled", "true")
53+
.setting("xpack.license.self_generated.type", "trial")
54+
.setting("xpack.security.authc.token.enabled", "true")
55+
.setting("xpack.security.http.ssl.enabled", "false")
56+
.plugin("microsoft-graph-authz")
57+
.keystore("bootstrap.password", "x-pack-test-password")
58+
.user("test_admin", "x-pack-test-password", User.ROOT_USER_ROLE, true)
59+
.user("rest_test", "rest_password", User.ROOT_USER_ROLE, true)
60+
.configFile("metadata.xml", Resource.fromString(getIDPMetadata()))
61+
.setting("xpack.security.authc.realms.saml.saml1.order", "1")
62+
.setting("xpack.security.authc.realms.saml.saml1.idp.entity_id", IDP_ENTITY_ID)
63+
.setting("xpack.security.authc.realms.saml.saml1.idp.metadata.path", "metadata.xml")
64+
.setting("xpack.security.authc.realms.saml.saml1.attributes.principal", "urn:oid:2.5.4.3")
65+
.setting("xpack.security.authc.realms.saml.saml1.sp.entity_id", "http://sp/default.example.org/")
66+
.setting("xpack.security.authc.realms.saml.saml1.sp.acs", "http://acs/default")
67+
.setting("xpack.security.authc.realms.saml.saml1.sp.logout", "http://logout/default")
68+
.setting("xpack.security.authc.realms.saml.saml1.authorization_realms", "microsoft_graph1")
69+
.setting("xpack.security.authc.realms.microsoft_graph.microsoft_graph1.order", "2")
70+
.build();
71+
}
72+
73+
private static String getIDPMetadata() {
74+
try {
75+
var signingCert = PathUtils.get(MicrosoftGraphAuthzPluginIT.class.getResource("/saml/signing.crt").toURI());
76+
return new SamlIdpMetadataBuilder().entityId(IDP_ENTITY_ID).idpUrl(IDP_ENTITY_ID).sign(signingCert).asString();
77+
} catch (URISyntaxException | CertificateException | IOException exception) {
78+
fail(exception);
79+
}
80+
return null;
81+
}
82+
83+
@Override
84+
protected String getTestRestCluster() {
85+
return cluster.getHttpAddresses();
86+
}
87+
88+
@Override
89+
protected String getProtocol() {
90+
return "http";
91+
}
92+
93+
@Override
94+
protected Settings restClientSettings() {
95+
final String token = basicAuthHeaderValue("rest_test", new SecureString("rest_password".toCharArray()));
96+
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
97+
}
98+
99+
@Override
100+
protected boolean shouldConfigureProjects() {
101+
return false;
102+
}
103+
104+
public void testAuthenticationSuccessful() throws Exception {
105+
final String username = randomAlphaOfLengthBetween(4, 12);
106+
samlAuthWithMicrosoftGraphAuthz(username, getSamlAssertionJsonBodyString(username));
107+
}
108+
109+
private String getSamlAssertionJsonBodyString(String username) throws Exception {
110+
var message = new SamlResponseBuilder().spEntityId("http://sp/default.example.org/")
111+
.idpEntityId(IDP_ENTITY_ID)
112+
.acs(new URL("http://acs/default"))
113+
.attribute("urn:oid:2.5.4.3", username)
114+
.sign(getDataPath("/saml/signing.crt"), getDataPath("/saml/signing.key"), new char[0])
115+
.asString();
116+
117+
final Map<String, Object> body = new HashMap<>();
118+
body.put("content", Base64.getEncoder().encodeToString(message.getBytes(StandardCharsets.UTF_8)));
119+
body.put("realm", "saml1");
120+
return Strings.toString(JsonXContent.contentBuilder().map(body));
121+
}
122+
123+
private void samlAuthWithMicrosoftGraphAuthz(String username, String samlAssertion) throws Exception {
124+
var req = new Request("POST", "_security/saml/authenticate");
125+
req.setJsonEntity(samlAssertion);
126+
var resp = entityAsMap(client().performRequest(req));
127+
List<String> roles = new XContentTestUtils.JsonMapView(entityAsMap(client().performRequest(req))).get("authentication.roles");
128+
assertThat(resp.get("username"), equalTo(username));
129+
// TODO add check for mapped groups and roles when available
130+
assertThat(roles, empty());
131+
assertThat(ObjectPath.evaluate(resp, "authentication.authentication_realm.name"), equalTo("saml1"));
132+
}
133+
134+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#
2+
# Script / Instructions to (re)generate the certificates + keys in this directory
3+
#
4+
# You can run this with bash, provided "elasticsearch-certutil" is somewhere on your path (or aliased)
5+
6+
#
7+
# Step 1: Create a Signing Key in PEM format
8+
#
9+
elasticsearch-certutil cert --self-signed --pem --out ${PWD}/signing.zip -days 9999 -keysize 2048 -name "signing"
10+
unzip signing.zip
11+
mv signing/signing.* ./
12+
rmdir signing
13+
rm signing.zip
14+

0 commit comments

Comments
 (0)