Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature/Identity] Fixes test failures in gradle check for #4798 #5441

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- [Identity] Adds Basic Auth mechanism via Internal IdP ([#4798](https://github.com/opensearch-project/OpenSearch/pull/4798))
- [Identity] Introduce Identity Module ([#5583](https://github.com/opensearch-project/OpenSearch/pull/5583))
- [Identity] Adds Bearer Auth mechanism via internal token handling ([#5611](https://github.com/opensearch-project/OpenSearch/pull/5611))
- [Identity] Fixes test failures in gradle check for #4798 ([#5441](https://github.com/opensearch-project/OpenSearch/pull/5441))
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ private static AuthenticationToken handleBasicAuth(final BasicAuthToken token) {
return null;
}

logger.info("Logging in as: " + username);
logger.info("Attempting authentication as: " + username);

return new UsernamePasswordToken(username, password);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,10 @@ public interface Subject {
* throws SubjectDisabled
*/
void login(final AuthenticationToken token);

/**
* Checks the current authentication status of this subject
* @return true if authenticated, false otherwise
*/
boolean isAuthenticated();
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,27 @@ public void login(AuthenticationToken authenticationToken) {

org.apache.shiro.authc.AuthenticationToken authToken = AuthenticationTokenHandler.extractShiroAuthToken(authenticationToken);

// If already authenticated, do not check login info again
/*
TODO: understand potential repercussions in following situations:
1. How to handle this in password change situations
2. Can two subjects in same environment have same principal name? if so the following check is invalid
*/
if (this.isAuthenticated() && this.getPrincipal().getName().equals(authToken.getPrincipal())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Principal identifiers are unique within the same realm.

What is the purpose of the second part of this check?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test would fail when:

  1. Admin sends a request and is valid. Subject's principal is set to admin and isAuthenticated is set to true
  2. Next, a dummy user with invalid credentials tries to login. Since isAuthenticated is true, this would assume a valid user and skip login without second condition. With second condition we match dummy and admin and see that they don't match so incoming request should go through authentication workflow

Although I'm not 100% sure about this behavior. How should Subject be treated for each incoming request?

return;
}
// Login via shiro realm.
shiroSubject.login(authToken);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried replacing this line with SecurityUtils.getSecurityManager().authenticate(authToken); which is evaluated much faster than login. I'm not sure if the calls are interchangeable, but login calls authenticate under the hood.

See: https://github.com/apache/shiro/blob/main/core/src/main/java/org/apache/shiro/mgt/DefaultSecurityManager.java#L272

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good find!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure how important createSubject call after authenticate is. From skimming through it, it seems like it sets rememberMe features, resolves sessions and creates/resolves principals. I can update the PR to use authenticate to skip all the other steps, but we will have to understand the impact of doing so (mainly, whether we need them for our scope)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The basic auth handling is taking too long which is leading to the build failures in tests that perform many async requests. The way to resolve it in the short-term is to disable identity when running the existing test suite in the gradle check.

Long-term when security is merged into core, the existing tests in the gradle check should work with identity enabled when identity is promoted from sandbox.

I'm not sure why the basic auth handling here is taking 500ms+ per request, but at least part of it is due to the bcrypt password matcher. The security plugin also performs bcrypt password matching and I am not sure how taxing basic auth is with the security plugin installed.

}

/**
* A flag to indicate whether this subject is already authenticated
*
* @return true if authenticated, false otherwise
*/
@Override
public boolean isAuthenticated() {
return shiroSubject.isAuthenticated();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,14 @@ public String toString() {
return "NoopSubject(principal=" + getPrincipal() + ")";
}

/**
* Logs the user in
*/
@Override
public void login(AuthenticationToken authenticationToken) {
// Do nothing as noop subject is always logged in
}

@Override
public boolean isAuthenticated() {
// Noop subject is always authenticated
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/*
* SPDX-License-Identifier: Apache-2.0
*
* The OpenSearch Contributors require contributions made to
* this file be licensed under the Apache-2.0 license or a
* compatible open source license.
*/

package org.opensearch.authn.internal;

import com.carrotsearch.randomizedtesting.annotations.ThreadLeakScope;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.subject.Subject;
import org.junit.After;
import org.junit.Before;
import org.opensearch.authn.tokens.AuthenticationToken;
import org.opensearch.authn.tokens.BasicAuthToken;
import org.opensearch.test.OpenSearchTestCase;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

@ThreadLeakScope(ThreadLeakScope.Scope.NONE)
public class InternalSubjectTests extends OpenSearchTestCase {
private static final AuthenticationToken AUTH_TOKEN = new BasicAuthToken("Basic YWRtaW46YWRtaW4=");

@Before
public void initRealm() {
new InternalAuthenticationManager();
}

@After
public void logoutSubject() {
Subject subject = SecurityUtils.getSubject();
if (subject == null) return;
if (subject.getSession() != null) subject.getSession().stop();
SecurityUtils.getSubject().logout();
}

public void testGetPrincipal() {
InternalSubject internalSubject = new InternalSubject(SecurityUtils.getSubject());
assertNull(internalSubject.getPrincipal());

internalSubject.login(AUTH_TOKEN);

assertNotNull(internalSubject.getPrincipal());
}

public void testLoginBehaviors() {
Subject subject = mock(Subject.class);
InternalSubject internalSubject = new InternalSubject(subject);

// 1. Verify that login() is called on first attempt

internalSubject.login(AUTH_TOKEN);
verify(subject, times(1)).login(any());

// 2. Verify that call to login() is skipped on 2nd attempt for same user

when(subject.isAuthenticated()).thenReturn(true);
when(subject.getPrincipal()).thenReturn("admin");

internalSubject.login(AUTH_TOKEN);
verify(subject, times(1)).login(any());

// 3. Verify that login() is called again for a different user

when(subject.isAuthenticated()).thenReturn(true);
when(subject.getPrincipal()).thenReturn("marvin");

// even though we are using same token, the mock stub returns different principal and hence login is called again
internalSubject.login(AUTH_TOKEN);
verify(subject, times(2)).login(any());
}

public void testSuccessfulLoginAndLogOut() {
InternalSubject internalSubject = new InternalSubject(SecurityUtils.getSubject());

internalSubject.login(AUTH_TOKEN);
assertTrue(internalSubject.isAuthenticated());
}

public void testLoginFailure() {
InternalSubject internalSubject = new InternalSubject(SecurityUtils.getSubject());

AuthenticationToken authToken = new BasicAuthToken("Basic bWFydmluOmdhbGF4eQ==");

assertThrows(AuthenticationException.class, () -> internalSubject.login(authToken));
}

public void testIsAuthenticated() {
InternalSubject internalSubject = new InternalSubject(SecurityUtils.getSubject());

assertFalse(internalSubject.isAuthenticated());
internalSubject.login(AUTH_TOKEN);
assertTrue(internalSubject.isAuthenticated());
}
}