Skip to content

Commit f1df1cd

Browse files
RyanL1997stephen-crawfordpeterniedowaiskazi19
authored
[Backport 2.x] Service accounts and on-behalf-of authentication in 2.x (#11052)
* Implement on behalf of token passing for extensions (#8679) * Provide service accounts tokens to extensions (#9618) This change adds a new transport action which passes the extension a string representation of its service account auth token. This token is created by the TokenManager interface implementation. The token is expected to be an encoded basic auth credential string which can be used by the extension to interact with its own system index. * Cherry pick #10614 and #10664 Signed-off-by: Stephen Crawford <[email protected]> Signed-off-by: Stephen Crawford <[email protected]> Signed-off-by: Ryan Liang <[email protected]> Signed-off-by: Peter Nied <[email protected]> Co-authored-by: Stephen Crawford <[email protected]> Co-authored-by: Peter Nied <[email protected]> Co-authored-by: Owais Kazi <[email protected]> Co-authored-by: Peter Nied <[email protected]>
1 parent 0d30590 commit f1df1cd

22 files changed

+382
-98
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
2121
- Update the indexRandom function to create more segments for concurrent search tests ([10247](https://github.com/opensearch-project/OpenSearch/pull/10247))
2222
- Add support for query profiler with concurrent aggregation ([#9248](https://github.com/opensearch-project/OpenSearch/pull/9248))
2323
- Introduce ConcurrentQueryProfiler to profile query using concurrent segment search path and support concurrency during rewrite and create weight ([10352](https://github.com/opensearch-project/OpenSearch/pull/10352))
24+
- Implement on behalf of token passing for extensions ([#8679](https://github.com/opensearch-project/OpenSearch/pull/8679))
25+
- Provide service accounts tokens to extensions ([#9618](https://github.com/opensearch-project/OpenSearch/pull/9618))
2426

2527
### Dependencies
2628
- Bumps jetty version to 9.4.52.v20230823 to fix GMS-2023-1857 ([#9822](https://github.com/opensearch-project/OpenSearch/pull/9822))

plugins/identity-shiro/src/main/java/org/opensearch/identity/shiro/ShiroTokenManager.java

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@
1010

1111
import org.apache.logging.log4j.LogManager;
1212
import org.apache.logging.log4j.Logger;
13-
import org.apache.shiro.SecurityUtils;
1413
import org.apache.shiro.authc.AuthenticationToken;
1514
import org.apache.shiro.authc.UsernamePasswordToken;
1615
import org.opensearch.common.Randomness;
1716
import org.opensearch.identity.IdentityService;
17+
import org.opensearch.identity.Subject;
1818
import org.opensearch.identity.tokens.AuthToken;
1919
import org.opensearch.identity.tokens.BasicAuthToken;
20+
import org.opensearch.identity.tokens.OnBehalfOfClaims;
2021
import org.opensearch.identity.tokens.TokenManager;
2122

2223
import java.util.Arrays;
@@ -54,15 +55,16 @@ public Optional<AuthenticationToken> translateAuthToken(org.opensearch.identity.
5455
final BasicAuthToken basicAuthToken = (BasicAuthToken) authenticationToken;
5556
return Optional.of(new UsernamePasswordToken(basicAuthToken.getUser(), basicAuthToken.getPassword()));
5657
}
57-
5858
return Optional.empty();
5959
}
6060

6161
@Override
62-
public AuthToken issueToken(String audience) {
62+
public AuthToken issueOnBehalfOfToken(Subject subject, OnBehalfOfClaims claims) {
6363

6464
String password = generatePassword();
65-
final byte[] rawEncoded = Base64.getEncoder().encode((audience + ":" + password).getBytes(UTF_8));
65+
// Make a new ShiroSubject audience as name
66+
final byte[] rawEncoded = Base64.getUrlEncoder().encode((claims.getAudience() + ":" + password).getBytes(UTF_8));
67+
6668
final String usernamePassword = new String(rawEncoded, UTF_8);
6769
final String header = "Basic " + usernamePassword;
6870
BasicAuthToken token = new BasicAuthToken(header);
@@ -71,13 +73,17 @@ public AuthToken issueToken(String audience) {
7173
return token;
7274
}
7375

74-
public boolean validateToken(AuthToken token) {
75-
if (token instanceof BasicAuthToken) {
76-
final BasicAuthToken basicAuthToken = (BasicAuthToken) token;
77-
return basicAuthToken.getUser().equals(SecurityUtils.getSubject().toString())
78-
&& basicAuthToken.getPassword().equals(shiroTokenPasswordMap.get(basicAuthToken));
79-
}
80-
return false;
76+
@Override
77+
public AuthToken issueServiceAccountToken(String audience) {
78+
79+
String password = generatePassword();
80+
final byte[] rawEncoded = Base64.getUrlEncoder().withoutPadding().encode((audience + ":" + password).getBytes(UTF_8)); // Make a new
81+
final String usernamePassword = new String(rawEncoded, UTF_8);
82+
final String header = "Basic " + usernamePassword;
83+
84+
BasicAuthToken token = new BasicAuthToken(header);
85+
shiroTokenPasswordMap.put(token, password);
86+
return token;
8187
}
8288

8389
public String getTokenInfo(AuthToken token) {

plugins/identity-shiro/src/test/java/org/opensearch/identity/shiro/AuthTokenHandlerTests.java

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010

1111
import org.apache.shiro.authc.AuthenticationToken;
1212
import org.apache.shiro.authc.UsernamePasswordToken;
13+
import org.opensearch.identity.Subject;
14+
import org.opensearch.identity.noop.NoopSubject;
1315
import org.opensearch.identity.noop.NoopTokenManager;
1416
import org.opensearch.identity.tokens.AuthToken;
1517
import org.opensearch.identity.tokens.BasicAuthToken;
1618
import org.opensearch.identity.tokens.BearerAuthToken;
19+
import org.opensearch.identity.tokens.OnBehalfOfClaims;
1720
import org.opensearch.test.OpenSearchTestCase;
1821
import org.junit.Before;
1922

@@ -34,16 +37,15 @@
3437
public class AuthTokenHandlerTests extends OpenSearchTestCase {
3538

3639
private ShiroTokenManager shiroAuthTokenHandler;
37-
private NoopTokenManager noopTokenManager;
3840

3941
@Before
4042
public void testSetup() {
4143
shiroAuthTokenHandler = new ShiroTokenManager();
42-
noopTokenManager = new NoopTokenManager();
4344
}
4445

4546
public void testShouldExtractBasicAuthTokenSuccessfully() {
4647
final BasicAuthToken authToken = new BasicAuthToken("Basic YWRtaW46YWRtaW4="); // admin:admin
48+
assertEquals(authToken.asAuthHeaderValue(), "YWRtaW46YWRtaW4=");
4749

4850
final AuthenticationToken translatedToken = shiroAuthTokenHandler.translateAuthToken(authToken).get();
4951
assertThat(translatedToken, is(instanceOf(UsernamePasswordToken.class)));
@@ -98,18 +100,13 @@ public void testShouldFailGetTokenInfo() {
98100
assertThrows(UnsupportedAuthenticationToken.class, () -> shiroAuthTokenHandler.getTokenInfo(bearerAuthToken));
99101
}
100102

101-
public void testShouldFailValidateToken() {
102-
final BearerAuthToken bearerAuthToken = new BearerAuthToken("header.payload.signature");
103-
assertFalse(shiroAuthTokenHandler.validateToken(bearerAuthToken));
104-
}
105-
106103
public void testShoudPassMapLookupWithToken() {
107104
final BasicAuthToken authToken = new BasicAuthToken("Basic dGVzdDp0ZTpzdA==");
108105
shiroAuthTokenHandler.getShiroTokenPasswordMap().put(authToken, "te:st");
109106
assertTrue(authToken.getPassword().equals(shiroAuthTokenHandler.getShiroTokenPasswordMap().get(authToken)));
110107
}
111108

112-
public void testShouldPassThrougbResetToken(AuthToken token) {
109+
public void testShouldPassThroughResetToken() {
113110
final BearerAuthToken bearerAuthToken = new BearerAuthToken("header.payload.signature");
114111
shiroAuthTokenHandler.resetToken(bearerAuthToken);
115112
}
@@ -124,6 +121,7 @@ public void testVerifyBearerTokenObject() {
124121
assertEquals(testGoodToken.getPayload(), "payload");
125122
assertEquals(testGoodToken.getSignature(), "signature");
126123
assertEquals(testGoodToken.toString(), "Bearer auth token with header=header, payload=payload, signature=signature");
124+
assertEquals(testGoodToken.asAuthHeaderValue(), "header.payload.signature");
127125
}
128126

129127
public void testGeneratedPasswordContents() {
@@ -147,4 +145,35 @@ public void testGeneratedPasswordContents() {
147145
validator.validate(data);
148146
}
149147

148+
public void testIssueOnBehalfOfTokenFromClaims() {
149+
Subject subject = new NoopSubject();
150+
OnBehalfOfClaims claims = new OnBehalfOfClaims("test", "test");
151+
BasicAuthToken authToken = (BasicAuthToken) shiroAuthTokenHandler.issueOnBehalfOfToken(subject, claims);
152+
assertTrue(authToken instanceof BasicAuthToken);
153+
UsernamePasswordToken translatedToken = (UsernamePasswordToken) shiroAuthTokenHandler.translateAuthToken(authToken).get();
154+
assertEquals(authToken.getPassword(), new String(translatedToken.getPassword()));
155+
assertTrue(shiroAuthTokenHandler.getShiroTokenPasswordMap().containsKey(authToken));
156+
assertEquals(shiroAuthTokenHandler.getShiroTokenPasswordMap().get(authToken), new String(translatedToken.getPassword()));
157+
}
158+
159+
public void testTokenNoopIssuance() {
160+
NoopTokenManager tokenManager = new NoopTokenManager();
161+
OnBehalfOfClaims claims = new OnBehalfOfClaims("test", "test");
162+
Subject subject = new NoopSubject();
163+
AuthToken token = tokenManager.issueOnBehalfOfToken(subject, claims);
164+
assertTrue(token instanceof AuthToken);
165+
AuthToken serviceAccountToken = tokenManager.issueServiceAccountToken("test");
166+
assertTrue(serviceAccountToken instanceof AuthToken);
167+
assertEquals(serviceAccountToken.asAuthHeaderValue(), "noopToken");
168+
}
169+
170+
public void testShouldSucceedIssueServiceAccountToken() {
171+
String audience = "testExtensionName";
172+
BasicAuthToken authToken = (BasicAuthToken) shiroAuthTokenHandler.issueServiceAccountToken(audience);
173+
assertTrue(authToken instanceof BasicAuthToken);
174+
UsernamePasswordToken translatedToken = (UsernamePasswordToken) shiroAuthTokenHandler.translateAuthToken(authToken).get();
175+
assertEquals(authToken.getPassword(), new String(translatedToken.getPassword()));
176+
assertTrue(shiroAuthTokenHandler.getShiroTokenPasswordMap().containsKey(authToken));
177+
assertEquals(shiroAuthTokenHandler.getShiroTokenPasswordMap().get(authToken), new String(translatedToken.getPassword()));
178+
}
150179
}

server/src/main/java/org/opensearch/discovery/InitializeExtensionRequest.java

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,23 +25,27 @@
2525
public class InitializeExtensionRequest extends TransportRequest {
2626
private final DiscoveryNode sourceNode;
2727
private final DiscoveryExtensionNode extension;
28+
private final String serviceAccountHeader;
2829

29-
public InitializeExtensionRequest(DiscoveryNode sourceNode, DiscoveryExtensionNode extension) {
30+
public InitializeExtensionRequest(DiscoveryNode sourceNode, DiscoveryExtensionNode extension, String serviceAccountHeader) {
3031
this.sourceNode = sourceNode;
3132
this.extension = extension;
33+
this.serviceAccountHeader = serviceAccountHeader;
3234
}
3335

3436
public InitializeExtensionRequest(StreamInput in) throws IOException {
3537
super(in);
3638
sourceNode = new DiscoveryNode(in);
3739
extension = new DiscoveryExtensionNode(in);
40+
serviceAccountHeader = in.readString();
3841
}
3942

4043
@Override
4144
public void writeTo(StreamOutput out) throws IOException {
4245
super.writeTo(out);
4346
sourceNode.writeTo(out);
4447
extension.writeTo(out);
48+
out.writeString(serviceAccountHeader);
4549
}
4650

4751
public DiscoveryNode getSourceNode() {
@@ -52,6 +56,10 @@ public DiscoveryExtensionNode getExtension() {
5256
return extension;
5357
}
5458

59+
public String getServiceAccountHeader() {
60+
return serviceAccountHeader;
61+
}
62+
5563
@Override
5664
public String toString() {
5765
return "InitializeExtensionsRequest{" + "sourceNode=" + sourceNode + ", extension=" + extension + '}';
@@ -62,7 +70,9 @@ public boolean equals(Object o) {
6270
if (this == o) return true;
6371
if (o == null || getClass() != o.getClass()) return false;
6472
InitializeExtensionRequest that = (InitializeExtensionRequest) o;
65-
return Objects.equals(sourceNode, that.sourceNode) && Objects.equals(extension, that.extension);
73+
return Objects.equals(sourceNode, that.sourceNode)
74+
&& Objects.equals(extension, that.extension)
75+
&& Objects.equals(serviceAccountHeader, that.getServiceAccountHeader());
6676
}
6777

6878
@Override

server/src/main/java/org/opensearch/extensions/ExtensionsManager.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@
4040
import org.opensearch.extensions.rest.RestActionsRequestHandler;
4141
import org.opensearch.extensions.settings.CustomSettingsRequestHandler;
4242
import org.opensearch.extensions.settings.RegisterCustomSettingsRequest;
43+
import org.opensearch.identity.IdentityService;
44+
import org.opensearch.identity.tokens.AuthToken;
4345
import org.opensearch.threadpool.ThreadPool;
4446
import org.opensearch.transport.ConnectTransportException;
4547
import org.opensearch.transport.TransportException;
@@ -100,14 +102,15 @@ public static enum OpenSearchRequestType {
100102
private Settings environmentSettings;
101103
private AddSettingsUpdateConsumerRequestHandler addSettingsUpdateConsumerRequestHandler;
102104
private NodeClient client;
105+
private IdentityService identityService;
103106

104107
/**
105108
* Instantiate a new ExtensionsManager object to handle requests and responses from extensions. This is called during Node bootstrap.
106109
*
107110
* @param additionalSettings Additional settings to read in from extension initialization request
108111
* @throws IOException If the extensions discovery file is not properly retrieved.
109112
*/
110-
public ExtensionsManager(Set<Setting<?>> additionalSettings) throws IOException {
113+
public ExtensionsManager(Set<Setting<?>> additionalSettings, IdentityService identityService) throws IOException {
111114
logger.info("ExtensionsManager initialized");
112115
this.initializedExtensions = new HashMap<String, DiscoveryExtensionNode>();
113116
this.extensionIdMap = new HashMap<String, DiscoveryExtensionNode>();
@@ -122,6 +125,7 @@ public ExtensionsManager(Set<Setting<?>> additionalSettings) throws IOException
122125
}
123126
this.client = null;
124127
this.extensionTransportActionsHandler = null;
128+
this.identityService = identityService;
125129
}
126130

127131
/**
@@ -141,9 +145,15 @@ public void initializeServicesAndRestHandler(
141145
TransportService transportService,
142146
ClusterService clusterService,
143147
Settings initialEnvironmentSettings,
144-
NodeClient client
148+
NodeClient client,
149+
IdentityService identityService
145150
) {
146-
this.restActionsRequestHandler = new RestActionsRequestHandler(actionModule.getRestController(), extensionIdMap, transportService);
151+
this.restActionsRequestHandler = new RestActionsRequestHandler(
152+
actionModule.getRestController(),
153+
extensionIdMap,
154+
transportService,
155+
identityService
156+
);
147157
this.customSettingsRequestHandler = new CustomSettingsRequestHandler(settingsModule);
148158
this.transportService = transportService;
149159
this.clusterService = clusterService;
@@ -399,7 +409,7 @@ protected void doRun() throws Exception {
399409
transportService.sendRequest(
400410
extensionNode,
401411
REQUEST_EXTENSION_ACTION_NAME,
402-
new InitializeExtensionRequest(transportService.getLocalNode(), extensionNode),
412+
new InitializeExtensionRequest(transportService.getLocalNode(), extensionNode, issueServiceAccount(extensionNode)),
403413
initializeExtensionResponseHandler
404414
);
405415
}
@@ -442,6 +452,15 @@ TransportResponse handleExtensionRequest(ExtensionRequest extensionRequest) thro
442452
}
443453
}
444454

455+
/**
456+
* A helper method called during initialization that issues a service accounts to extensions
457+
* @param extension The extension to be issued a service account
458+
*/
459+
private String issueServiceAccount(DiscoveryExtensionNode extension) {
460+
AuthToken serviceAccountToken = identityService.getTokenManager().issueServiceAccountToken(extension.getId());
461+
return serviceAccountToken.asAuthHeaderValue();
462+
}
463+
445464
static String getRequestExtensionActionName() {
446465
return REQUEST_EXTENSION_ACTION_NAME;
447466
}

server/src/main/java/org/opensearch/extensions/NoopExtensionsManager.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,11 @@
1616
import org.opensearch.extensions.action.ExtensionActionRequest;
1717
import org.opensearch.extensions.action.ExtensionActionResponse;
1818
import org.opensearch.extensions.action.RemoteExtensionActionResponse;
19+
import org.opensearch.identity.IdentityService;
1920
import org.opensearch.transport.TransportService;
2021

2122
import java.io.IOException;
23+
import java.util.List;
2224
import java.util.Optional;
2325
import java.util.Set;
2426

@@ -30,7 +32,7 @@
3032
public class NoopExtensionsManager extends ExtensionsManager {
3133

3234
public NoopExtensionsManager() throws IOException {
33-
super(Set.of());
35+
super(Set.of(), new IdentityService(Settings.EMPTY, List.of()));
3436
}
3537

3638
@Override
@@ -40,7 +42,8 @@ public void initializeServicesAndRestHandler(
4042
TransportService transportService,
4143
ClusterService clusterService,
4244
Settings initialEnvironmentSettings,
43-
NodeClient client
45+
NodeClient client,
46+
IdentityService identityService
4447
) {
4548
// no-op
4649
}

server/src/main/java/org/opensearch/extensions/rest/ExtensionRestRequest.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import java.util.Objects;
3232
import java.util.Set;
3333

34+
import static java.util.Objects.requireNonNull;
35+
3436
/**
3537
* Request to execute REST actions on extension node.
3638
* This contains necessary portions of a {@link RestRequest} object, but does not pass the full request for security concerns.
@@ -86,7 +88,7 @@ public ExtensionRestRequest(
8688
this.headers = headers;
8789
this.mediaType = mediaType;
8890
this.content = content;
89-
this.principalIdentifierToken = principalIdentifier;
91+
this.principalIdentifierToken = requireNonNull(principalIdentifier);
9092
this.httpVersion = httpVersion;
9193
}
9294

@@ -280,7 +282,7 @@ public boolean isContentConsumed() {
280282
}
281283

282284
/**
283-
* Gets a parser for the contents of this request if there is content and an xContentType.
285+
* Gets a parser for the contents of this request if there is content, an xContentType, and a principal identifier.
284286
*
285287
* @param xContentRegistry The extension's xContentRegistry
286288
* @return A parser for the given content and content type.
@@ -291,6 +293,9 @@ public final XContentParser contentParser(NamedXContentRegistry xContentRegistry
291293
if (!hasContent() || getXContentType() == null) {
292294
throw new OpenSearchParseException("There is no request body or the ContentType is invalid.");
293295
}
296+
if (getRequestIssuerIdentity() == null) {
297+
throw new OpenSearchParseException("There is no request body or the requester identity is invalid.");
298+
}
294299
return getXContentType().xContent().createParser(xContentRegistry, LoggingDeprecationHandler.INSTANCE, content.streamInput());
295300
}
296301

0 commit comments

Comments
 (0)