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] Reset Password API #6309

Merged
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,5 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- [Identity] Grant Permission REST API ([#6117](https://github.com/opensearch-project/OpenSearch/pull/6154))
- [Identity] REST API for user GET and DELETE ([#6189](https://github.com/opensearch-project/OpenSearch/pull/6189))
- [Identity] Extensions security setup and token creation ([#6204](https://github.com/opensearch-project/OpenSearch/pull/6204))
- [Identity] REST API for user GET and DELETE ([#6189](https://github.com/opensearch-project/OpenSearch/pull/6189))
- [Identity] REST API for Reset password ([#5990](https://github.com/opensearch-project/OpenSearch/pull/6309))
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@
import org.opensearch.identity.rest.user.put.PutUserAction;
import org.opensearch.identity.rest.user.put.RestPutUserAction;
import org.opensearch.identity.rest.user.put.TransportPutUserAction;
import org.opensearch.identity.rest.user.resetpassword.ResetPasswordAction;
import org.opensearch.identity.rest.user.resetpassword.RestResetPasswordAction;
import org.opensearch.identity.rest.user.resetpassword.TransportResetPasswordAction;
import org.opensearch.indices.SystemIndexDescriptor;
import org.opensearch.plugins.ActionPlugin;
import org.opensearch.plugins.ClusterPlugin;
Expand Down Expand Up @@ -142,6 +145,7 @@ public List<RestHandler> getRestHandlers(
handlers.add(new RestMultiGetUserAction());
handlers.add(new RestDeleteUserAction());
handlers.add(new RestPutPermissionAction());
handlers.add(new RestResetPasswordAction());
// TODO: Add handlers for future actions
return handlers;
}
Expand All @@ -161,7 +165,8 @@ public List<RestHandler> getRestHandlers(
new ActionHandler<>(GetUserAction.INSTANCE, TransportGetUserAction.class),
new ActionHandler<>(MultiGetUserAction.INSTANCE, TransportMultiGetUserAction.class),
new ActionHandler<>(DeleteUserAction.INSTANCE, TransportDeleteUserAction.class),
new ActionHandler<>(PutPermissionAction.INSTANCE, TransportPutPermissionAction.class)
new ActionHandler<>(PutPermissionAction.INSTANCE, TransportPutPermissionAction.class),
new ActionHandler<>(ResetPasswordAction.INSTANCE, TransportResetPasswordAction.class)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ public void handleRequest(RestRequest request, RestChannel channel, NodeClient c
if (authTokenHeader == null) {
Subject currentSubject = Identity.getAuthManager().getSubject();
// TODO replace with Principal Identifier Token if destination is extension
jwtClaims.put("sub", currentSubject.getPrincipal().getName());
if (currentSubject != null) {
jwtClaims.put("sub", currentSubject.getPrincipal().getName());
}
jwtClaims.put("iat", Instant.now().toString());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ public class IdentityRestConstants {
public static final String PERMISSION_SUBPATH = "/permissions";
public static final String IDENTITY_API_PERMISSION_PREFIX = IDENTITY_REST_API_REQUEST_PREFIX + PERMISSION_SUBPATH;
public static final String PERMISSION_ACTION_PREFIX = "permission_action";
public static final String IDENTITY_RESET_USER_PASSWORD_ACTION = "reset_password" + IDENTITY_USER_ACTION_SUFFIX;
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bouncycastle.crypto.generators.OpenBSDBCrypt;
import org.opensearch.ExceptionsHelper;
import org.opensearch.ResourceNotFoundException;
import org.opensearch.action.ActionListener;
Expand Down Expand Up @@ -38,6 +39,9 @@
import org.opensearch.identity.rest.user.get.single.GetUserResponseInfo;
import org.opensearch.identity.rest.user.put.PutUserResponse;
import org.opensearch.identity.rest.user.put.PutUserResponseInfo;
import org.opensearch.identity.rest.user.resetpassword.ResetPasswordRequest;
import org.opensearch.identity.rest.user.resetpassword.ResetPasswordResponse;
import org.opensearch.identity.rest.user.resetpassword.ResetPasswordResponseInfo;
import org.opensearch.identity.utils.ErrorType;
import org.opensearch.identity.utils.Hasher;
import org.opensearch.index.IndexNotFoundException;
Expand Down Expand Up @@ -337,6 +341,69 @@ protected void saveAndUpdateConfiguration(
}
}

public void resetPassword(ResetPasswordRequest request, ActionListener<ResetPasswordResponse> listener) {
if (!ensureIndexExists()) {
listener.onFailure(new IndexNotFoundException(ErrorType.IDENTITY_NOT_INITIALIZED.getMessage()));
return;
}

String username = request.getUsername();
String oldPassword = request.getOldPassword();
String newPassword = request.getNewPassword();

// load current user store in memory
final SecurityDynamicConfiguration<?> internalUsersConfiguration = load(getConfigName());

// check if user existed
final boolean userExisted = internalUsersConfiguration.exists(username);

// hash is mandatory for new users
if (!userExisted) {
listener.onFailure(new IllegalArgumentException(ErrorType.USER_NOT_EXISTING.getMessage()));
return;
}

User userToBeUpdated = (User) internalUsersConfiguration.getCEntry(username);

// If current password and provided password doesn't match, throw an exception.
final String currentHash = userToBeUpdated.getHash();
final boolean oldPasswordMatched = OpenBSDBCrypt.checkPassword(currentHash, oldPassword.toCharArray());
if (!oldPasswordMatched) {
listener.onFailure(new IllegalArgumentException(ErrorType.OLDPASSWORD_MISMATCHING.getMessage()));
return;
}

// Verify the new password is not matching the old password
if (newPassword.equals(oldPassword)) {
listener.onFailure(new IllegalArgumentException(ErrorType.NEWPASSWORD_MATCHING_OLDPASSWORD.getMessage()));
return;
}

// Updating the password
userToBeUpdated.setHash(Hasher.hash(newPassword.toCharArray()));

// TODO: check if this is absolutely required
internalUsersConfiguration.remove(username);

// Create or update the user
internalUsersConfiguration.putCObject(username, userToBeUpdated);

// Listener for responding once index update completes
final ActionListener<IndexResponse> indexActionListener = new OnSucessActionListener<>() {
@Override
public void onResponse(IndexResponse indexResponse) {
String message = username + " user's password updated successfully.";
ResetPasswordResponseInfo responseInfo = new ResetPasswordResponseInfo(true, username, message);
ResetPasswordResponse response = new ResetPasswordResponse(responseInfo);

listener.onResponse(response);
}
};

// save the changes to identity index, propagate change to other nodes and reload in-memory configuration
saveAndUpdateConfiguration(this.nodeClient, CType.INTERNALUSERS, internalUsersConfiguration, indexActionListener);
}

abstract class OnSucessActionListener<Response> implements ActionListener<Response> {

public OnSucessActionListener() {
Expand All @@ -347,6 +414,5 @@ public OnSucessActionListener() {
public final void onFailure(Exception e) {
// TODO throw it somewhere??
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* 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.identity.rest.user.resetpassword;

import org.opensearch.action.ActionType;

/**
* Action type for creating or updating a user
*/
public class ResetPasswordAction extends ActionType<ResetPasswordResponse> {

public static final ResetPasswordAction INSTANCE = new ResetPasswordAction();

// TODO : revisit this action type
public static final String NAME = "cluster:admin/user/resetpassword";
Copy link
Member

Choose a reason for hiding this comment

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

Since this API is used by all users, it may need a different namespace than cluster:admin/user/* which are a group of actions intended for the cluster admin. When it comes to authorizing this request, all users of the cluster should be able to call and use this API without being granted permission for it.


private ResetPasswordAction() {
super(NAME, ResetPasswordResponse::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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.identity.rest.user.resetpassword;

import org.opensearch.action.ActionRequest;
import org.opensearch.action.ActionRequestValidationException;
import org.opensearch.common.io.stream.StreamInput;
import org.opensearch.common.io.stream.StreamOutput;
import org.opensearch.common.xcontent.ToXContentObject;
import org.opensearch.common.xcontent.XContentBuilder;

import java.io.IOException;

import static org.opensearch.action.ValidateActions.addValidationError;

/**
* Request to reset an internal user password
*/
public class ResetPasswordRequest extends ActionRequest implements ToXContentObject {

private String username;
private String oldPassword;
Copy link
Member

Choose a reason for hiding this comment

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

We shouldn't require the old password, if a user forgot there password how would they get a new password issued?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Let's leave this as a prototype design for now. (I just wanna transfer our conversation on github :)) I'm not gonna resolve this comment, so that I can use this as a reminder for myself, in case if we wanna change it in the future.

private String newPassword;

public ResetPasswordRequest(StreamInput in) throws IOException {
super(in);
username = in.readString();
oldPassword = in.readString();
newPassword = in.readString();
}

public ResetPasswordRequest(String username, String oldPassword, String newPassword) {
this.username = username;
this.oldPassword = oldPassword;
this.newPassword = newPassword;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getOldPassword() {
return oldPassword;
}

public String getNewPassword() {
return newPassword;
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (username == null) { // TODO: check the condition once
validationException = addValidationError("No username specified", validationException);
} else if (oldPassword == null) {
validationException = addValidationError("No current password specified", validationException);
} else if (newPassword == null) {
validationException = addValidationError("No new password specified", validationException);
}
return validationException;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(username);
out.writeString(oldPassword);
out.writeString(newPassword);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.value(username);
builder.value(oldPassword);
builder.value(newPassword);
builder.endObject();
return builder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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.identity.rest.user.resetpassword;

import org.opensearch.action.ActionResponse;
import org.opensearch.common.ParseField;
import org.opensearch.common.io.stream.StreamInput;
import org.opensearch.common.io.stream.StreamOutput;
import org.opensearch.common.xcontent.ConstructingObjectParser;
import org.opensearch.common.xcontent.StatusToXContentObject;
import org.opensearch.common.xcontent.XContentBuilder;
import org.opensearch.rest.RestStatus;

import java.io.IOException;

import static org.opensearch.common.xcontent.ConstructingObjectParser.constructorArg;
import static org.opensearch.rest.RestStatus.NOT_FOUND;
import static org.opensearch.rest.RestStatus.OK;

/**
* Response class for create user request
* Contains list of responses of each user creation request
*/
public class ResetPasswordResponse extends ActionResponse implements StatusToXContentObject {

// TODO: revisit this class
private final ResetPasswordResponseInfo resetPasswordResponseInfo;

public ResetPasswordResponse(ResetPasswordResponseInfo resetPasswordResponseInfo) {
this.resetPasswordResponseInfo = resetPasswordResponseInfo;
}

public ResetPasswordResponse(StreamInput in) throws IOException {
super(in);
resetPasswordResponseInfo = new ResetPasswordResponseInfo(in);
}

public ResetPasswordResponseInfo getResetPasswordResponseInfo() {
return resetPasswordResponseInfo;
}

/**
* @return Whether the attempt to Create a user was successful
*/
@Override
public RestStatus status() {
if (resetPasswordResponseInfo == null) return NOT_FOUND;
return OK;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
if (resetPasswordResponseInfo != null) {
resetPasswordResponseInfo.writeTo(out);
}
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
if (resetPasswordResponseInfo != null) {
resetPasswordResponseInfo.toXContent(builder, params);
}
return builder;
}

private static final ConstructingObjectParser<ResetPasswordResponse, Void> PARSER = new ConstructingObjectParser<>(
"reset_password_response",
true,
(Object[] parsedObjects) -> {
@SuppressWarnings("unchecked")
ResetPasswordResponseInfo resetPasswordResponseInfo1 = (ResetPasswordResponseInfo) parsedObjects[0];
return new ResetPasswordResponse(resetPasswordResponseInfo1);
}
);
static {
PARSER.declareObject(constructorArg(), ResetPasswordResponseInfo.PARSER, new ParseField("user"));
}

}
Loading