Skip to content

Commit

Permalink
[Feature/Identity] Internal user operations: create update delete (#4741
Browse files Browse the repository at this point in the history
)

* Add line item in CHANGELOG
* Adds Create, Update, Delete operations for subject and adds tests for those operations
* Addresses PR comments and changes signature of create and delete methods
* Addresses PR comments
* Rebases with source branch and updates some method signatures as well as adds a couple of tests
* Deletes incorrectly commit file
* Adds this PR to changelog
* Adds missing javadoc
* Makes user a data storage medium only and moves all crud logic in the realm itself
Co-authored-by: Craig Perkins <cwperx@amazon.com>
Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Signed-off-by: Darshit Chanpura <35282393+DarshitChanpura@users.noreply.github.com>
  • Loading branch information
DarshitChanpura authored Nov 14, 2022
1 parent d3b60b5 commit 01f7a50
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- [Identity] Include scrawfor99 to the identity team ([#4658](https://github.com/opensearch-project/OpenSearch/pull/4658))
- [Identity] Prototype Internal IdP ([#4659](https://github.com/opensearch-project/OpenSearch/pull/4659))
- [Identity] Strategy for Delegated Authority using Tokens ([#4826](https://github.com/opensearch-project/OpenSearch/pull/4826))
- [Identity] User operations: create update delete ([#4741](https://github.com/opensearch-project/OpenSearch/pull/4741))


### Dependencies
Expand Down
1 change: 1 addition & 0 deletions sandbox/libs/authn/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,7 @@ thirdPartyAudit.ignoreMissingClasses(
'org.yaml.snakeyaml.events.MappingStartEvent',
'org.yaml.snakeyaml.events.NodeEvent',
'org.yaml.snakeyaml.events.ScalarEvent',
'org.yaml.snakeyaml.LoaderOptions',
'org.yaml.snakeyaml.nodes.NodeId',
'org.yaml.snakeyaml.nodes.Tag',
'org.yaml.snakeyaml.parser.ParserImpl',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@
*
* @opensearch.experimental
*/
class StringPrincipal implements Principal {
public class StringPrincipal implements Principal {

private final String name;

/**
* Creates a principal for an identity specified as a string
* @param name A persistent string that represent an identity
*/
StringPrincipal(final String name) {
public StringPrincipal(final String name) {
this.name = name;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public interface Subject {
/**
* Get the application-wide uniquely identifying principal
* */
public Principal getPrincipal();
Principal getPrincipal();

/**
* Authentications from a token
Expand All @@ -28,6 +28,5 @@ public interface Subject {
* throws SubjectNotFound
* throws SubjectDisabled
*/
public void login(final AuthenticationToken token);

void login(final AuthenticationToken token);
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,6 @@ public void setAttributes(Map<String, String> attributes) {

@Override
public String toString() {
return "InternalSubject [primaryPrincipal=" + primaryPrincipal + ", bcryptHash=" + bcryptHash + ", attributes=" + attributes + "]";
return "User [primaryPrincipal=" + primaryPrincipal + ", bcryptHash=" + bcryptHash + ", attributes=" + attributes + "]";
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
/*
* Copyright OpenSearch Contributors
* 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.realm;
Expand All @@ -14,17 +17,25 @@
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.realm.AuthenticatingRealm;

import org.opensearch.authn.StringPrincipal;
import org.opensearch.authn.User;

import java.util.Objects;
import java.util.concurrent.ConcurrentMap;
import java.util.List;
import java.util.Map;

/**
* Internal Realm is a custom realm using the internal OpenSearch IdP
*
* @opensearch.experimental
*/
public class InternalRealm extends AuthenticatingRealm {
public static final String INVALID_SUBJECT_MESSAGE = "Subject can't be null";

public static final String INVALID_ARGUMENTS_MESSAGE = "primaryPrincipal or hash can't be null or empty";

private static final String DEFAULT_REALM_NAME = "internal";

private static final String DEFAULT_INTERNAL_USERS_FILE = "example/example_internal_users.yml";
Expand Down Expand Up @@ -52,14 +63,14 @@ public Builder(String name, String pathToInternalUsersYaml) {
}

public InternalRealm build() {
ConcurrentMap<String, User> internalUsers = InternalUsersStore.readInternalSubjectsAsMap(pathToInternalUsersYaml);
ConcurrentMap<String, User> internalUsers = InternalUsersStore.readUsersAsMap(pathToInternalUsersYaml);
return new InternalRealm(name, internalUsers);
}
}

private void initializeInternalSubjectsStore(String pathToInternalUsersYaml) {
private void initializeUsersStore(String pathToInternalUsersYaml) {
// TODO load this at cluster start
internalUsers = InternalUsersStore.readInternalSubjectsAsMap(pathToInternalUsersYaml);
internalUsers = InternalUsersStore.readUsersAsMap(pathToInternalUsersYaml);
}

public User getInternalUser(String principalIdentifier) throws UnknownAccountException {
Expand Down Expand Up @@ -104,4 +115,136 @@ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token)
// Don't know what to do with this token
throw new CredentialsException();
}

// TODO: Expose all the operations below as a rest API

/**
* Creates a user in an in-memory data store
* @param user to be created. It should be passed in {@link User}
*/
public void createUser(User user) {
if (user == null) {
throw new IllegalArgumentException(INVALID_SUBJECT_MESSAGE);
}
String primaryPrincipal = user.getPrimaryPrincipal().getName();

// TODO: should we update if an object already exists with same principal.
// If so, it should be handled in updateSubject
if (this.internalUsers.containsKey(primaryPrincipal)) {
throw new RuntimeException("User with principal= " + primaryPrincipal + " already exists in realm= " + realmName);
}

// TODO: add checks to restrict the users that are allowed to create
this.internalUsers.put(primaryPrincipal, user);
}

/**
* Creates a user in in-memory data-store when relevant details are passed
* @param primaryPrincipal the primary identifier of this user (must be unique)
* @param hash the password passed as hash
* @param attributes passed in key-value format
* @throws IllegalArgumentException if primaryPrincipal or hash is null or empty
*/
public void createUser(String primaryPrincipal, String hash, Map<String, String> attributes) {
// We don't create a user if primaryPrincipal and/or hash is empty or null
if (primaryPrincipal == null || hash == null || primaryPrincipal == "" || hash == "") {
throw new IllegalArgumentException(INVALID_ARGUMENTS_MESSAGE);
}

User user = new User();
user.setPrimaryPrincipal(new StringPrincipal(primaryPrincipal));
user.setBcryptHash(hash);
user.setAttributes(attributes);

createUser(user);
}

/**
* Updates the user's password
* @param primaryPrincipal the principal whose password is to be updated
* @param hash The new password
* @return true if password update was successful, false otherwise
*
* TODO: Add restrictions around who can do this
*/
public boolean updateUserPassword(String primaryPrincipal, String hash) {
if (!this.internalUsers.containsKey(primaryPrincipal)) {
throw new RuntimeException(userDoesNotExistMessage(primaryPrincipal));
}
User userToBeUpdated = this.internalUsers.get(primaryPrincipal);
userToBeUpdated.setBcryptHash(hash);

this.internalUsers.put(primaryPrincipal, userToBeUpdated);
return true;
}

/**
* Adds new attributes to the user's current list (stored as map) AND
* updates the existing attributes if there is a match
* @param primaryPrincipal the principal whose attributes are to be updated
* @param attributesToBeAdded new attributes to be added
* @return true if the addition was successful, false otherwise
*
* TODO: Add restrictions around who can do this
*/
public boolean updateUserAttributes(String primaryPrincipal, Map<String, String> attributesToBeAdded) {
if (!this.internalUsers.containsKey(primaryPrincipal)) {
throw new RuntimeException(userDoesNotExistMessage(primaryPrincipal));
}

User userToBeUpdated = this.internalUsers.get(primaryPrincipal);
userToBeUpdated.getAttributes().putAll(attributesToBeAdded);

this.internalUsers.put(primaryPrincipal, userToBeUpdated);
return true;
}

/**
* Removes the list of attributes for a given user
* @param primaryPrincipal the principal whose attributes are to be deleted
* @param attributesToBeRemoved the list of attributes to be deleted (list of keys in the attribute map)
* @return true is successful, false otherwise
*
* TODO: 1. Are we supporting this. 2. If so add restrictions around who can do this
*/
public boolean removeAttributesFromUser(String primaryPrincipal, List<String> attributesToBeRemoved) {
if (!this.internalUsers.containsKey(primaryPrincipal)) {
throw new RuntimeException(userDoesNotExistMessage(primaryPrincipal));
}

User userToBeUpdated = this.internalUsers.get(primaryPrincipal);
Map<String, String> currentAttributes = userToBeUpdated.getAttributes();
for (String attribute : attributesToBeRemoved) {
currentAttributes.remove(attribute);
}
userToBeUpdated.setAttributes(currentAttributes);

this.internalUsers.put(primaryPrincipal, userToBeUpdated);
return true;
}

/**
* Removes a user given its primaryPrincipal from the in-memory store
* @param primaryPrincipal the primaryPrincipal of the user to be deleted
* @return {@linkplain User} the deleted user
*
* TODO: Add restrictions around who can do this
*/
public User removeUser(String primaryPrincipal) {

User removedUser = this.internalUsers.remove(primaryPrincipal);
if (removedUser == null) {
throw new RuntimeException(userDoesNotExistMessage(primaryPrincipal));
}
return removedUser;
}

/**
* Generates an Exception message String
* @param primaryPrincipal to be added to this message
* @return the exception message string
*/
public String userDoesNotExistMessage(String primaryPrincipal) {
return "Subject with primaryPrincipal=" + primaryPrincipal + " doesn't exist";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/
public class InternalUsersStore {

public static ConcurrentMap<String, User> readInternalSubjectsAsMap(String pathToInternalUsersYaml) {
public static ConcurrentMap<String, User> readUsersAsMap(String pathToInternalUsersYaml) {
ConcurrentMap<String, User> internalUsersMap = new ConcurrentHashMap<>();
URL resourceUrl = InternalUsersStore.class.getClassLoader().getResource(pathToInternalUsersYaml);
if (resourceUrl == null) {
Expand Down
Loading

0 comments on commit 01f7a50

Please sign in to comment.