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

User Profile: Activate user profile API #82400

Merged
merged 12 commits into from
Jan 17, 2022
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* 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.action.ActionType;

public class ActivateProfileAction extends ActionType<ActivateProfileResponse> {

public static final String NAME = "cluster:admin/xpack/security/profile/activate";
public static final ActivateProfileAction INSTANCE = new ActivateProfileAction();

public ActivateProfileAction() {
super(NAME, ActivateProfileResponse::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xpack.core.security.action.GrantApiKeyRequest;

import java.io.IOException;

public class ActivateProfileRequest extends ActionRequest {

// TODO: move grant to separate class
private final GrantApiKeyRequest.Grant grant;

public ActivateProfileRequest() {
this.grant = new GrantApiKeyRequest.Grant();
}

public ActivateProfileRequest(GrantApiKeyRequest.Grant grant) {
this.grant = grant;
}

public ActivateProfileRequest(StreamInput in) throws IOException {
super(in);
this.grant = new GrantApiKeyRequest.Grant(in);
}

public GrantApiKeyRequest.Grant getGrant() {
return grant;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
grant.writeTo(out);
}

@Override
public ActionRequestValidationException validate() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* 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.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.xcontent.ToXContentObject;
import org.elasticsearch.xcontent.XContentBuilder;

import java.io.IOException;

public class ActivateProfileResponse extends ActionResponse implements ToXContentObject {

private final Profile profile;

public ActivateProfileResponse(Profile profile) {
this.profile = profile;
}

public ActivateProfileResponse(StreamInput in) throws IOException {
super(in);
this.profile = new Profile(in);
}

public Profile getProfile() {
return profile;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
profile.writeTo(out);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
profile.toXContent(builder, params);
return builder;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public record ProfileUser(
@Nullable String realmDomain,
String email,
String fullName,
String displayName
String displayName,
boolean active
) implements Writeable, ToXContent {

public ProfileUser(StreamInput in) throws IOException {
Expand All @@ -47,7 +48,8 @@ public ProfileUser(StreamInput in) throws IOException {
in.readOptionalString(),
in.readOptionalString(),
in.readOptionalString(),
in.readOptionalString()
in.readOptionalString(),
in.readBoolean()
);
}

Expand All @@ -67,11 +69,12 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
builder.field("email", email);
}
if (fullName != null) {
builder.field("full_name", email);
builder.field("full_name", fullName);
}
if (displayName != null) {
builder.field("display_name", displayName);
}
builder.field("active", active);
builder.endObject();
return builder;
}
Expand All @@ -84,6 +87,7 @@ public void writeTo(StreamOutput out) throws IOException {
out.writeOptionalString(email);
out.writeOptionalString(fullName);
out.writeOptionalString(displayName);
out.writeBoolean(active);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import org.elasticsearch.Version;
import org.elasticsearch.test.ESTestCase;
import org.elasticsearch.test.VersionUtils;
import org.elasticsearch.xpack.core.security.action.service.TokenInfo;
import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType;
import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef;
import org.elasticsearch.xpack.core.security.authc.esnative.NativeRealmSettings;
Expand All @@ -20,6 +21,7 @@
import java.util.Arrays;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -218,6 +220,23 @@ public static Authentication randomApiKeyAuthentication(User user, String apiKey
);
}

public static Authentication randomServiceAccountAuthentication() {
final RealmRef realmRef = new RealmRef("_service_account", "_service_account", randomAlphaOfLengthBetween(3, 8));
return new Authentication(
new User(randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8)),
realmRef,
null,
Version.CURRENT,
AuthenticationType.TOKEN,
Map.of(
"_token_name",
randomAlphaOfLength(8),
"_token_source",
randomFrom(TokenInfo.TokenSource.values()).name().toLowerCase(Locale.ROOT)
)
);
}

private boolean realmIsSingleton(RealmRef realmRef) {
return Set.of(FileRealmSettings.TYPE, NativeRealmSettings.TYPE).contains(realmRef.getType());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ public class Constants {
"cluster:admin/xpack/security/privilege/delete",
"cluster:admin/xpack/security/privilege/get",
"cluster:admin/xpack/security/privilege/put",
"cluster:admin/xpack/security/profile/activate",
"cluster:admin/xpack/security/profile/get",
"cluster:admin/xpack/security/realm/cache/clear",
"cluster:admin/xpack/security/role/delete",
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugin/security/qa/profile/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ testClusters.matching { it.name == 'javaRestTest' }.configureEach {
setting 'xpack.security.authc.api_key.enabled', 'true'

user username: "test_admin", password: 'x-pack-test-password', role: "superuser"
user username: "rac_user", password: 'x-pack-test-password', role: "rac_role"
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ public class ProfileIT extends ESRestTestCase {
},
"email": "foo@example.com",
"full_name": "User Foo",
"display_name": "Curious Foo"
"display_name": "Curious Foo",
"active": true
},
"last_synchronized": %s,
"access": {
Expand All @@ -65,6 +66,28 @@ protected Settings restAdminSettings() {
.build();
}

public void testActivateProfile() throws IOException {
final Request activateProfileRequest = new Request("POST", "_security/profile/_activate");
activateProfileRequest.setJsonEntity("""
{
"grant_type": "password",
"username": "rac_user",
"password": "x-pack-test-password"
}""");

final Response activateProfileResponse = adminClient().performRequest(activateProfileRequest);
assertOK(activateProfileResponse);
final Map<String, Object> activateProfileMap = responseAsMap(activateProfileResponse);

final String profileUid = (String) activateProfileMap.get("uid");
final Request getProfileRequest1 = new Request("GET", "_security/profile/" + profileUid);
final Response getProfileResponse1 = adminClient().performRequest(getProfileRequest1);
assertOK(getProfileResponse1);
final Map<String, Object> getProfileMap1 = responseAsMap(getProfileResponse1);
final Map<String, Object> profile1 = castToMap(getProfileMap1.get(profileUid));
assertThat(profile1, equalTo(activateProfileMap));
}

public void testGetProfile() throws IOException {
final String uid = randomAlphaOfLength(20);
final String source = SAMPLE_PROFILE_DOCUMENT_TEMPLATE.formatted(uid, Instant.now().toEpochMilli());
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
rac_user_role:
cluster:
- "monitor"
indices:
- names: ['rac_index_*' ]
privileges:
- read
- write
- create_index
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,19 @@
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.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.test.SecuritySingleNodeTestCase;
import org.elasticsearch.xcontent.XContentType;
import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileAction;
import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileRequest;
import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileResponse;
import org.elasticsearch.xpack.core.security.action.profile.GetProfileAction;
import org.elasticsearch.xpack.core.security.action.profile.GetProfileRequest;
import org.elasticsearch.xpack.core.security.action.profile.GetProfilesResponse;
import org.elasticsearch.xpack.core.security.action.profile.Profile;
import org.elasticsearch.xpack.core.security.action.user.PutUserAction;
import org.elasticsearch.xpack.core.security.action.user.PutUserRequest;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.user.User;

Expand All @@ -25,13 +35,19 @@
import java.util.stream.Stream;

import static org.elasticsearch.test.SecuritySettingsSource.TEST_PASSWORD_HASHED;
import static org.elasticsearch.test.SecuritySettingsSourceField.TEST_PASSWORD_SECURE_STRING;
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.anEmptyMap;
import static org.hamcrest.Matchers.arrayContaining;
import static org.hamcrest.Matchers.arrayWithSize;
import static org.hamcrest.Matchers.containsInAnyOrder;
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.is;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;

Expand Down Expand Up @@ -119,7 +135,74 @@ public void testGetProfileByAuthentication() {
);
}

public String indexDocument() {
public void testActivateProfile() {
final Profile profile1 = doActivateProfile(RAC_USER_NAME, TEST_PASSWORD_SECURE_STRING);
assertThat(profile1.user().username(), equalTo(RAC_USER_NAME));
assertThat(profile1.user().email(), nullValue());
assertThat(profile1.user().fullName(), nullValue());

assertThat(getProfile(profile1.uid(), Set.of()), equalTo(profile1));

// activate again should be getting the same profile
final Profile profile2 = doActivateProfile(RAC_USER_NAME, TEST_PASSWORD_SECURE_STRING);
assertThat(profile2.uid(), equalTo(profile1.uid()));

// Create another rac user in the native realm
final PutUserRequest putUserRequest1 = new PutUserRequest();
putUserRequest1.username(RAC_USER_NAME);
putUserRequest1.roles("rac_role");
final SecureString nativeRacUserPassword = new SecureString("native_rac_user_password".toCharArray());
final String nativeRacUserPasswordHash = new String(getFastStoredHashAlgoForTests().hash(nativeRacUserPassword));
putUserRequest1.passwordHash(nativeRacUserPasswordHash.toCharArray());
putUserRequest1.email(RAC_USER_NAME + "@example.com");
assertThat(client().execute(PutUserAction.INSTANCE, putUserRequest1).actionGet().created(), is(true));

// Since file and native realms are not in the same domain yet, the new profile should be a different one
final Profile profile3 = doActivateProfile(RAC_USER_NAME, nativeRacUserPassword);
assertThat(profile3.uid(), not(equalTo(profile1.uid())));
assertThat(profile3.user().email(), equalTo(RAC_USER_NAME + "@example.com"));
assertThat(profile3.user().fullName(), nullValue());
assertThat(profile3.access().roles(), containsInAnyOrder("rac_role"));

// Update native rac user
final PutUserRequest putUserRequest2 = new PutUserRequest();
putUserRequest2.username(RAC_USER_NAME);
putUserRequest2.roles("rac_role", "superuser");
putUserRequest2.email(null);
putUserRequest2.fullName("Native RAC User");
assertThat(client().execute(PutUserAction.INSTANCE, putUserRequest2).actionGet().created(), is(false));

// Activate again should see the updated user info
final Profile profile4 = doActivateProfile(RAC_USER_NAME, nativeRacUserPassword);
assertThat(profile4.uid(), equalTo(profile3.uid()));
assertThat(profile4.user().email(), nullValue());
assertThat(profile4.user().fullName(), equalTo("Native RAC User"));
assertThat(profile4.access().roles(), containsInAnyOrder("rac_role", "superuser"));
}

private Profile doActivateProfile(String username, SecureString password) {
final ActivateProfileRequest activateProfileRequest = new ActivateProfileRequest();
activateProfileRequest.getGrant().setType("password");
activateProfileRequest.getGrant().setUsername(username);
activateProfileRequest.getGrant().setPassword(password);

final ActivateProfileResponse activateProfileResponse = client().execute(ActivateProfileAction.INSTANCE, activateProfileRequest)
.actionGet();
final Profile profile = activateProfileResponse.getProfile();
assertThat(profile, notNullValue());
assertThat(profile.user().username(), equalTo(username));
assertThat(profile.applicationData(), anEmptyMap());
return profile;
}

private Profile getProfile(String uid, Set<String> dataKeys) {
final GetProfilesResponse getProfilesResponse = client().execute(GetProfileAction.INSTANCE, new GetProfileRequest(uid, dataKeys))
.actionGet();
assertThat(getProfilesResponse.getProfiles(), arrayWithSize(1));
return getProfilesResponse.getProfiles()[0];
}

private 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))
Expand Down
Loading