Skip to content

Commit

Permalink
User Profile: Add an initial ProfileService (#81899)
Browse files Browse the repository at this point in the history
Profile service manages the underlying profile system index and provides
methods for get profile by either profile uid or authentication.
  • Loading branch information
ywangd authored Dec 23, 2021
1 parent a2bc485 commit 8eb23b6
Show file tree
Hide file tree
Showing 8 changed files with 610 additions and 13 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.core.security.action.profile;

import org.elasticsearch.core.Nullable;

import java.util.List;
import java.util.Map;

public record Profile(
String uid,
boolean enabled,
long lastSynchronized,
ProfileUser user,
Access access,
Map<String, Object> applicationData,
VersionControl versionControl
) {

public record QualifiedName(String username, String realmDomain) {}

public record ProfileUser(
String username,
String realmName,
@Nullable String realmDomain,
String email,
String fullName,
String displayName
) {
public QualifiedName qualifiedName() {
return new QualifiedName(username, realmDomain);
}
}

public record Access(List<String> roles, Map<String, Object> applications) {}

public record VersionControl(long primaryTerm, long seqNo) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.ToXContentObject;
import org.elasticsearch.xcontent.XContentBuilder;
import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
Expand All @@ -29,6 +31,8 @@
import java.util.Map;
import java.util.Objects;

import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;

// TODO(hub-cap) Clean this up after moving User over - This class can re-inherit its field AUTHENTICATION_KEY in AuthenticationField.
// That interface can be removed
public class Authentication implements ToXContentObject {
Expand Down Expand Up @@ -354,6 +358,18 @@ public String toString() {
}
}

public static ConstructingObjectParser<RealmRef, Void> REALM_REF_PARSER = new ConstructingObjectParser<>(
"realm_ref",
false,
(args, v) -> new RealmRef((String) args[0], (String) args[1], (String) args[2])
);

static {
REALM_REF_PARSER.declareString(constructorArg(), new ParseField("name"));
REALM_REF_PARSER.declareString(constructorArg(), new ParseField("type"));
REALM_REF_PARSER.declareString(constructorArg(), new ParseField("node_name"));
}

public enum AuthenticationType {
REALM,
API_KEY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,37 @@

package org.elasticsearch.xpack.security.profile;

import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.action.admin.indices.get.GetIndexAction;
import org.elasticsearch.action.admin.indices.get.GetIndexRequest;
import org.elasticsearch.action.support.PlainActionFuture;
import org.elasticsearch.action.support.WriteRequest;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.index.mapper.extras.MapperExtrasPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.test.SecuritySingleNodeTestCase;
import org.elasticsearch.xpack.security.support.SecuritySystemIndices;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.user.User;

import java.time.Instant;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static org.elasticsearch.test.SecuritySettingsSource.TEST_PASSWORD_HASHED;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.INTERNAL_SECURITY_PROFILE_INDEX_8;
import static org.elasticsearch.xpack.security.support.SecuritySystemIndices.SECURITY_PROFILE_ALIAS;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;

public class ProfileSingleNodeTests extends SecuritySingleNodeTestCase {

Expand Down Expand Up @@ -53,34 +66,78 @@ protected Collection<Class<? extends Plugin>> getPlugins() {
}

public void testProfileIndexAutoCreation() {
var indexResponse = client().prepareIndex(
randomFrom(SecuritySystemIndices.INTERNAL_SECURITY_PROFILE_INDEX_8, SecuritySystemIndices.SECURITY_PROFILE_ALIAS)
).setSource(Map.of("uid", randomAlphaOfLength(22))).get();
var indexResponse = client().prepareIndex(randomFrom(INTERNAL_SECURITY_PROFILE_INDEX_8, SECURITY_PROFILE_ALIAS))
.setSource(Map.of("uid", randomAlphaOfLength(22)))
.get();

assertThat(indexResponse.status().getStatus(), equalTo(201));

var getIndexRequest = new GetIndexRequest();
getIndexRequest.indices(SecuritySystemIndices.INTERNAL_SECURITY_PROFILE_INDEX_8);
getIndexRequest.indices(INTERNAL_SECURITY_PROFILE_INDEX_8);

var getIndexResponse = client().execute(GetIndexAction.INSTANCE, getIndexRequest).actionGet();

assertThat(getIndexResponse.getIndices(), arrayContaining(SecuritySystemIndices.INTERNAL_SECURITY_PROFILE_INDEX_8));
assertThat(getIndexResponse.getIndices(), arrayContaining(INTERNAL_SECURITY_PROFILE_INDEX_8));

var aliases = getIndexResponse.getAliases().get(SecuritySystemIndices.INTERNAL_SECURITY_PROFILE_INDEX_8);
var aliases = getIndexResponse.getAliases().get(INTERNAL_SECURITY_PROFILE_INDEX_8);
assertThat(aliases, hasSize(1));
assertThat(aliases.get(0).alias(), equalTo(SecuritySystemIndices.SECURITY_PROFILE_ALIAS));
assertThat(aliases.get(0).alias(), equalTo(SECURITY_PROFILE_ALIAS));

final Settings settings = getIndexResponse.getSettings().get(SecuritySystemIndices.INTERNAL_SECURITY_PROFILE_INDEX_8);
final Settings settings = getIndexResponse.getSettings().get(INTERNAL_SECURITY_PROFILE_INDEX_8);
assertThat(settings.get("index.number_of_shards"), equalTo("1"));
assertThat(settings.get("index.auto_expand_replicas"), equalTo("0-1"));
assertThat(settings.get("index.routing.allocation.include._tier_preference"), equalTo("data_content"));

final Map<String, Object> mappings = getIndexResponse.getMappings()
.get(SecuritySystemIndices.INTERNAL_SECURITY_PROFILE_INDEX_8)
.getSourceAsMap();
final Map<String, Object> mappings = getIndexResponse.getMappings().get(INTERNAL_SECURITY_PROFILE_INDEX_8).getSourceAsMap();

@SuppressWarnings("unchecked")
final Set<String> topLevelFields = ((Map<String, Object>) mappings.get("properties")).keySet();
assertThat(topLevelFields, hasItems("uid", "enabled", "last_synchronized", "user", "access", "application_data"));
}

public void testGetProfileByAuthentication() {
final ProfileService profileService = node().injector().getInstance(ProfileService.class);
final Authentication authentication = new Authentication(
new User("foo"),
new Authentication.RealmRef("realm_name", "realm_type", randomAlphaOfLengthBetween(3, 8)),
null
);

// Profile does not exist yet
final PlainActionFuture<ProfileService.VersionedDocument> future1 = new PlainActionFuture<>();
profileService.getVersionedDocument(authentication, future1);
assertThat(future1.actionGet(), nullValue());

// Index the document so it can be found
final String uid2 = indexDocument();
final PlainActionFuture<ProfileService.VersionedDocument> future2 = new PlainActionFuture<>();
profileService.getVersionedDocument(authentication, future2);
final ProfileService.VersionedDocument versionedDocument = future2.actionGet();
assertThat(versionedDocument, notNullValue());
assertThat(versionedDocument.doc().uid(), equalTo(uid2));

// Index it again to trigger duplicate exception
final String uid3 = indexDocument();
final PlainActionFuture<ProfileService.VersionedDocument> future3 = new PlainActionFuture<>();
profileService.getVersionedDocument(authentication, future3);
final ElasticsearchException e3 = expectThrows(ElasticsearchException.class, future3::actionGet);

assertThat(
e3.getMessage(),
containsString(
"multiple [2] profiles [" + Stream.of(uid2, uid3).sorted().collect(Collectors.joining(",")) + "] found for user [foo]"
)
);
}

public String indexDocument() {
final String uid = randomAlphaOfLength(20);
final String source = ProfileServiceTests.SAMPLE_PROFILE_DOCUMENT_TEMPLATE.formatted(uid, Instant.now().toEpochMilli());
client().prepareIndex(randomFrom(INTERNAL_SECURITY_PROFILE_INDEX_8, SECURITY_PROFILE_ALIAS))
.setId("profile_" + uid)
.setRefreshPolicy(WriteRequest.RefreshPolicy.WAIT_UNTIL)
.setSource(source, XContentType.JSON)
.get();
return uid;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@
import org.elasticsearch.xpack.security.operator.OperatorOnlyRegistry;
import org.elasticsearch.xpack.security.operator.OperatorPrivileges;
import org.elasticsearch.xpack.security.operator.OperatorPrivileges.OperatorPrivilegesService;
import org.elasticsearch.xpack.security.profile.ProfileService;
import org.elasticsearch.xpack.security.rest.SecurityRestFilter;
import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction;
import org.elasticsearch.xpack.security.rest.action.RestDelegatePkiAuthenticationAction;
Expand Down Expand Up @@ -739,6 +740,15 @@ Collection<Object> createComponents(
);
systemIndices.getMainIndexManager().addStateListener(allRolesStore::onSecurityIndexStateChange);

final ProfileService profileService = new ProfileService(
settings,
getClock(),
client,
systemIndices.getProfileIndexManager(),
threadPool
);
components.add(profileService);

// We use the value of the {@code ENROLLMENT_ENABLED} setting to determine if the node is starting up with auto-generated
// certificates (which have been generated by pre-startup scripts). In this case, and further if the node forms a new cluster by
// itself, rather than joining an existing one, we complete the auto-configuration by generating and printing credentials and
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/

package org.elasticsearch.xpack.security.profile;

import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.xcontent.ObjectParserHelper;
import org.elasticsearch.core.Nullable;
import org.elasticsearch.xcontent.ConstructingObjectParser;
import org.elasticsearch.xcontent.ParseField;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xpack.core.security.action.profile.Profile;
import org.elasticsearch.xpack.core.security.authc.Authentication;

import java.util.List;
import java.util.Map;

import static org.elasticsearch.xcontent.ConstructingObjectParser.constructorArg;

public record ProfileDocument(
String uid,
boolean enabled,
long lastSynchronized,
ProfileDocumentUser user,
Access access,
BytesReference applicationData
) {

public record ProfileDocumentUser(String username, Authentication.RealmRef realm, String email, String fullName, String displayName) {

public Profile.ProfileUser toProfileUser(@Nullable String realmDomain) {
return new Profile.ProfileUser(username, realm.getName(), realmDomain, email, fullName, displayName);
}
}

public record Access(List<String> roles, Map<String, Object> applications) {
public Profile.Access toProfileAccess() {
return new Profile.Access(roles, applications);
}
}

public static ProfileDocument fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}

static final ConstructingObjectParser<ProfileDocumentUser, Void> PROFILE_USER_PARSER = new ConstructingObjectParser<>(
"profile_document_user",
false,
(args, v) -> new ProfileDocumentUser(
(String) args[0],
(Authentication.RealmRef) args[1],
(String) args[2],
(String) args[3],
(String) args[4]
)
);

@SuppressWarnings("unchecked")
static final ConstructingObjectParser<Access, Void> ACCESS_PARSER = new ConstructingObjectParser<>(
"profile_access",
false,
(args, v) -> new Access((List<String>) args[0], (Map<String, Object>) args[1])
);

static final ConstructingObjectParser<ProfileDocument, Void> PARSER = new ConstructingObjectParser<>(
"profile_document",
false,
(args, v) -> new ProfileDocument(
(String) args[0],
(boolean) args[1],
(long) args[2],
(ProfileDocumentUser) args[3],
(Access) args[4],
(BytesReference) args[5]
)
);

static {
PROFILE_USER_PARSER.declareString(constructorArg(), new ParseField("username"));
PROFILE_USER_PARSER.declareObject(
constructorArg(),
(p, c) -> Authentication.REALM_REF_PARSER.parse(p, null),
new ParseField("realm")
);
PROFILE_USER_PARSER.declareString(constructorArg(), new ParseField("email"));
PROFILE_USER_PARSER.declareString(constructorArg(), new ParseField("full_name"));
PROFILE_USER_PARSER.declareString(constructorArg(), new ParseField("display_name"));
ACCESS_PARSER.declareStringArray(constructorArg(), new ParseField("roles"));
ACCESS_PARSER.declareObject(constructorArg(), (p, c) -> p.map(), new ParseField("applications"));

PARSER.declareString(constructorArg(), new ParseField("uid"));
PARSER.declareBoolean(constructorArg(), new ParseField("enabled"));
PARSER.declareLong(constructorArg(), new ParseField("last_synchronized"));
PARSER.declareObject(constructorArg(), (p, c) -> PROFILE_USER_PARSER.parse(p, null), new ParseField("user"));
PARSER.declareObject(constructorArg(), (p, c) -> ACCESS_PARSER.parse(p, null), new ParseField("access"));
ObjectParserHelper<ProfileDocument, Void> parserHelper = new ObjectParserHelper<>();
parserHelper.declareRawObject(PARSER, constructorArg(), new ParseField("application_data"));
}
}
Loading

0 comments on commit 8eb23b6

Please sign in to comment.