Skip to content

Commit 9373d01

Browse files
committed
Add Microsoft Graph Delegated Authorization Realm Plugin
1 parent 3f5f899 commit 9373d01

File tree

18 files changed

+542
-0
lines changed

18 files changed

+542
-0
lines changed
+21
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+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
provides org.elasticsearch.xpack.core.security.SecurityExtension with MicrosoftGraphAuthzPlugin;
18+
}
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
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,158 @@
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.authc.saml;
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.junit.BeforeClass;
24+
import org.junit.ClassRule;
25+
import org.junit.rules.RuleChain;
26+
import org.junit.rules.TestRule;
27+
28+
import java.io.FileNotFoundException;
29+
import java.io.IOException;
30+
import java.net.URISyntaxException;
31+
import java.net.URL;
32+
import java.nio.charset.StandardCharsets;
33+
import java.nio.file.Path;
34+
import java.security.cert.CertificateException;
35+
import java.util.Base64;
36+
import java.util.HashMap;
37+
import java.util.List;
38+
import java.util.Map;
39+
40+
import static org.hamcrest.Matchers.empty;
41+
import static org.hamcrest.Matchers.equalTo;
42+
43+
public class MicrosoftGraphAuthzPluginIT extends ESRestTestCase {
44+
public static ElasticsearchCluster cluster = initTestCluster();
45+
private static Path caPath;
46+
47+
@ClassRule
48+
public static TestRule ruleChain = RuleChain.outerRule(cluster);
49+
50+
private static final String IDP_ENTITY_ID = "https://idp.example.org/";
51+
52+
private static ElasticsearchCluster initTestCluster() {
53+
return ElasticsearchCluster.local()
54+
.setting("xpack.security.enabled", "true")
55+
.setting("xpack.license.self_generated.type", "trial")
56+
.setting("xpack.security.authc.token.enabled", "true")
57+
.setting("xpack.security.authc.api_key.enabled", "true")
58+
.setting("xpack.security.http.ssl.enabled", "true")
59+
.setting("xpack.security.http.ssl.certificate", "node.crt")
60+
.setting("xpack.security.http.ssl.key", "node.key")
61+
.setting("xpack.security.http.ssl.certificate_authorities", "ca.crt")
62+
.setting("xpack.security.transport.ssl.enabled", "true")
63+
.setting("xpack.security.transport.ssl.certificate", "node.crt")
64+
.setting("xpack.security.transport.ssl.key", "node.key")
65+
.setting("xpack.security.transport.ssl.certificate_authorities", "ca.crt")
66+
.setting("xpack.security.transport.ssl.verification_mode", "certificate")
67+
.plugin("microsoft-graph-authz")
68+
.keystore("bootstrap.password", "x-pack-test-password")
69+
.user("test_admin", "x-pack-test-password", User.ROOT_USER_ROLE, true)
70+
.user("rest_test", "rest_password", User.ROOT_USER_ROLE, true)
71+
.configFile("node.key", Resource.fromClasspath("ssl/node.key"))
72+
.configFile("node.crt", Resource.fromClasspath("ssl/node.crt"))
73+
.configFile("ca.crt", Resource.fromClasspath("ssl/ca.crt"))
74+
.configFile("metadata.xml", Resource.fromString(getIDPMetadata()))
75+
.setting("xpack.security.authc.realms.saml.saml1.order", "1")
76+
.setting("xpack.security.authc.realms.saml.saml1.idp.entity_id", IDP_ENTITY_ID)
77+
.setting("xpack.security.authc.realms.saml.saml1.idp.metadata.path", "metadata.xml")
78+
.setting("xpack.security.authc.realms.saml.saml1.attributes.principal", "urn:oid:2.5.4.3")
79+
.setting("xpack.security.authc.realms.saml.saml1.ssl.certificate_authorities", "ca.crt")
80+
.setting("xpack.security.authc.realms.saml.saml1.sp.entity_id", "https://sp/default.example.org/")
81+
.setting("xpack.security.authc.realms.saml.saml1.sp.acs", "https://acs/default")
82+
.setting("xpack.security.authc.realms.saml.saml1.sp.logout", "https://logout/default")
83+
.setting("xpack.security.authc.realms.saml.saml1.authorization_realms", "microsoft_graph1")
84+
.setting("xpack.security.authc.realms.microsoft_graph.microsoft_graph1.order", "2")
85+
.build();
86+
}
87+
88+
private static String getIDPMetadata() {
89+
try {
90+
var signingCert = PathUtils.get(MicrosoftGraphAuthzPluginIT.class.getResource("/saml/signing.crt").toURI());
91+
return new SamlIdpMetadataBuilder().entityId(IDP_ENTITY_ID).idpUrl(IDP_ENTITY_ID).sign(signingCert).asString();
92+
} catch (URISyntaxException | CertificateException | IOException exception) {
93+
fail(exception);
94+
}
95+
return null;
96+
}
97+
98+
@BeforeClass
99+
public static void loadCertificateAuthority() throws Exception {
100+
URL resource = MicrosoftGraphAuthzPluginIT.class.getResource("/ssl/ca.crt");
101+
if (resource == null) {
102+
throw new FileNotFoundException("Cannot find classpath resource /ssl/ca.crt");
103+
}
104+
caPath = PathUtils.get(resource.toURI());
105+
}
106+
107+
@Override
108+
protected String getTestRestCluster() {
109+
return cluster.getHttpAddresses();
110+
}
111+
112+
@Override
113+
protected String getProtocol() {
114+
return "https";
115+
}
116+
117+
@Override
118+
protected Settings restClientSettings() {
119+
final String token = basicAuthHeaderValue("rest_test", new SecureString("rest_password".toCharArray()));
120+
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).put(CERTIFICATE_AUTHORITIES, caPath).build();
121+
}
122+
123+
@Override
124+
protected boolean shouldConfigureProjects() {
125+
return false;
126+
}
127+
128+
public void testAuthenticationSuccessful() throws Exception {
129+
final String username = randomAlphaOfLengthBetween(4, 12);
130+
samlAuthWithMicrosoftGraphAuthz(username, getSamlAssertionJsonBodyString(username));
131+
}
132+
133+
private String getSamlAssertionJsonBodyString(String username) throws Exception {
134+
var message = new SamlResponseBuilder().spEntityId("https://sp/default.example.org/")
135+
.idpEntityId(IDP_ENTITY_ID)
136+
.acs(new URL("https://acs/default"))
137+
.attribute("urn:oid:2.5.4.3", username)
138+
.sign(getDataPath("/saml/signing.crt"), getDataPath("/saml/signing.key"), new char[0])
139+
.asString();
140+
141+
final Map<String, Object> body = new HashMap<>();
142+
body.put("content", Base64.getEncoder().encodeToString(message.getBytes(StandardCharsets.UTF_8)));
143+
body.put("realm", "saml1");
144+
return Strings.toString(JsonXContent.contentBuilder().map(body));
145+
}
146+
147+
private void samlAuthWithMicrosoftGraphAuthz(String username, String samlAssertion) throws Exception {
148+
var req = new Request("POST", "_security/saml/authenticate");
149+
req.setJsonEntity(samlAssertion);
150+
var resp = entityAsMap(client().performRequest(req));
151+
List<String> roles = new XContentTestUtils.JsonMapView(entityAsMap(client().performRequest(req))).get("authentication.roles");
152+
assertThat(resp.get("username"), equalTo(username));
153+
// TODO add check for mapped groups and roles when available
154+
assertThat(roles, empty());
155+
assertThat(ObjectPath.evaluate(resp, "authentication.authentication_realm.name"), equalTo("saml1"));
156+
}
157+
158+
}
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+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
-----BEGIN CERTIFICATE-----
2+
MIIDAjCCAeqgAwIBAgIVAIySNaWg1+64cquJnza2djHgwNuPMA0GCSqGSIb3DQEB
3+
CwUAMBIxEDAOBgNVBAMTB3NpZ25pbmcwIBcNMjMwMjA4MDc0MjQ3WhgPMjA1MDA2
4+
MjUwNzQyNDdaMBIxEDAOBgNVBAMTB3NpZ25pbmcwggEiMA0GCSqGSIb3DQEBAQUA
5+
A4IBDwAwggEKAoIBAQC/EPQ4xX+8QyIUf+XAJM+U4jZQT3Pa+0pGZahXqx29VVGC
6+
cbjTs4hGJ0OK0eDSN7Ess319D0ucIo9P631G/RQiEAjP7AuUOfBF5VvppEucncvU
7+
dJlPTycW3kno9KYzRqIxtAcX44JsC3qm1M/D37dlscen+ZjdTEDeP1c87hSDJgi1
8+
GAWUIBeT/7vHItpQcBv2xcJsaT26R2r62FTO/H0XNo8ElESKEcxN0pkfSCxik2g7
9+
Yn4d+Lvx63otlC0dLPL1rdJQrCQKyAzhbGI6/35uAyes58D54hYImeiCm5p6IalL
10+
ipST+CDTrYmpZm/EzypBH8hasCXDFTvgjBmQO1d1AgMBAAGjTTBLMB0GA1UdDgQW
11+
BBTlR7Un5DDAwMJJ8D4pMJ4c7+JWgTAfBgNVHSMEGDAWgBTlR7Un5DDAwMJJ8D4p
12+
MJ4c7+JWgTAJBgNVHRMEAjAAMA0GCSqGSIb3DQEBCwUAA4IBAQAAeXo5MMlC/Rzw
13+
TI6+U3rbr17n6s4M6a+PKJLY0tTfVscNwd40+WVQ8rvIIso6V1CREjvgMBuELK+6
14+
2FTURf+EP58+TMrKEOSYWjee9fR/E29uXherZIBtHHSXOrv7P1g1FxYBxHwh4iOI
15+
ZQIqrMAeJUQr00Tn1B2Fuqlyod1KA+1eTQw036lW5iBdpim5wFOE4HaGNYgP7J8j
16+
FBIqIFVDFRH27h9sDfs/Uqk3OGF2sWnovwB/x76yGjDAVejY5wG0ZultYexMRnPo
17+
KshiiSzcortBA7CEzQtYbxP6X0DUsWD3U8Z4EsiTIWHLxRGZzWurZIdOrbi8OMFQ
18+
0a27kQsa
19+
-----END CERTIFICATE-----
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
-----BEGIN RSA PRIVATE KEY-----
2+
MIIEpAIBAAKCAQEAvxD0OMV/vEMiFH/lwCTPlOI2UE9z2vtKRmWoV6sdvVVRgnG4
3+
07OIRidDitHg0jexLLN9fQ9LnCKPT+t9Rv0UIhAIz+wLlDnwReVb6aRLnJ3L1HSZ
4+
T08nFt5J6PSmM0aiMbQHF+OCbAt6ptTPw9+3ZbHHp/mY3UxA3j9XPO4UgyYItRgF
5+
lCAXk/+7xyLaUHAb9sXCbGk9ukdq+thUzvx9FzaPBJREihHMTdKZH0gsYpNoO2J+
6+
Hfi78et6LZQtHSzy9a3SUKwkCsgM4WxiOv9+bgMnrOfA+eIWCJnogpuaeiGpS4qU
7+
k/gg062JqWZvxM8qQR/IWrAlwxU74IwZkDtXdQIDAQABAoIBAADRqN5VGGOn8lgz
8+
USU2SIPV8WTS5lymBEQ80NB+uFv/UYq1LpU3uTSQcVrBugyPS7g06muJZSn9lZmC
9+
g/u0f4GC30Ahk04hbnJ6QHSGAq317jGnsKA1UbtTHMQock2Yy/6vC8hng6paD+lG
10+
/b9URj65GPGTIWYyGrq+e5hUWUGo+5SNY1B8iq1NYsP9WQ3D8s87wKBTcXDyolid
11+
BgpVExBD0LM+K3X8pOP/8Hh7qSWZBW7DpvcykcZ5OHmQjAkYizkjVV/mR9epTQHT
12+
gS52PniKFNT6t4W2/HwtNCQbdHm3T95s8kyyQrkpdnL5Dxn/UGgDy62o7CIuAZS7
13+
xygf5eECgYEA/AG4zGycGqdhccjEZnaQ23qcutakWLK82RSZLwjstqlwtRLVW1Cp
14+
Mw2k9lmcLcgXyTlrnD1weRvmG7HFHoAcy+CWnv0boFn5W3TDu9GkFHoI9/+n6hUG
15+
nUQi+OL9G+KslOt+d4nMBCSYm1t2kcC9qpEOTZz+X1F8K4moZZEEGJUCgYEAwhgG
16+
HPFFt/WZI5rIH8GRki9AKvp7EEOK6Nrl7XrtKiKMjA0NLF2wZj0RViRAax3ca1W6
17+
EjyxmSSMcNRWS1APyTRTtvL6FLdviUmQHSOiZ6TzPauvn9KMKIAYyE5ZijTtN9nz
18+
TZacH6PmlMBzLceLK2V2FAhA4tQx7M7asKsZK2ECgYEAsY4JBUc0yXbLKl85ObQq
19+
JemCygV3L+NnOU/RChmwppZFid7mInt3azge1U+XwY3sbGOflSqYt0vX2gVrjCzZ
20+
nS/1D7nnoBgkn7JqQkfX4nGFJi6jwULlMSMTvOY5TU9tJ1Ow/EpDS1v5heRwawsw
21+
1x9yw25srv37jbVkx4LgLu0CgYBDq16OPqxA+9adbDxznegj4Gdt1JCNVg8bKh5Z
22+
0q7XLt5zgaVjH3L94jKmJtNyxSFxJp1N+G0u6GgyekVv0oT+cEjzkvkPufigE86z
23+
6hWYLxFDIhWEEkMdZ7O8OlzLa7J883b5SRY7jcg5enNttZFW2vP0/f+pVbPmTSQ/
24+
zhdjYQKBgQDPwQ5gq6et0D6R35mFsOUIl2p2Py4Rp6UHLGbyooeVy0HCwz7fB/da
25+
fBx7HYDeYqOS3yQnyWkzzn+GXU3lzboBCQ9B17NlxVIbIyqAHhtPTxNWEwqf1nsa
26+
Q/a61ry0tRnMTIM6bkjXVoHY5popvkN9dQA1VCD58VYerSdh/3lRTQ==
27+
-----END RSA PRIVATE KEY-----

0 commit comments

Comments
 (0)