Skip to content

Commit

Permalink
Service Accounts - Get service account API (elastic#71315)
Browse files Browse the repository at this point in the history
This PR adds a new API endpoint to retrieve service accounts. Depends on the
request parameters, it returns either all accounts, accounts belong to a
namespace, a specific account, or an empty map if nothing is found.
  • Loading branch information
ywangd committed Apr 9, 2021
1 parent c88f93b commit 5404113
Show file tree
Hide file tree
Showing 13 changed files with 627 additions and 2 deletions.
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.service;

import org.elasticsearch.action.ActionType;

public class GetServiceAccountAction extends ActionType<GetServiceAccountResponse> {

public static final String NAME = "cluster:admin/xpack/security/service_account/get";
public static final GetServiceAccountAction INSTANCE = new GetServiceAccountAction();

public GetServiceAccountAction() {
super(NAME, GetServiceAccountResponse::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.service;

import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;

import java.io.IOException;
import java.util.Objects;

public class GetServiceAccountRequest extends ActionRequest {

@Nullable
private final String namespace;
@Nullable
private final String serviceName;

public GetServiceAccountRequest(@Nullable String namespace, @Nullable String serviceName) {
this.namespace = namespace;
this.serviceName = serviceName;
}

public GetServiceAccountRequest(StreamInput in) throws IOException {
super(in);
this.namespace = in.readOptionalString();
this.serviceName = in.readOptionalString();
}

public String getNamespace() {
return namespace;
}

public String getServiceName() {
return serviceName;
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
GetServiceAccountRequest that = (GetServiceAccountRequest) o;
return Objects.equals(namespace, that.namespace) && Objects.equals(serviceName, that.serviceName);
}

@Override
public int hashCode() {
return Objects.hash(namespace, serviceName);
}

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

@Override
public ActionRequestValidationException validate() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.service;

import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;

import java.io.IOException;
import java.util.Arrays;
import java.util.Objects;

public class GetServiceAccountResponse extends ActionResponse implements ToXContentObject {

private final ServiceAccountInfo[] serviceAccountInfos;

public GetServiceAccountResponse(ServiceAccountInfo[] serviceAccountInfos) {
this.serviceAccountInfos = Objects.requireNonNull(serviceAccountInfos);
}

public GetServiceAccountResponse(StreamInput in) throws IOException {
super(in);
this.serviceAccountInfos = in.readArray(ServiceAccountInfo::new, ServiceAccountInfo[]::new);
}

public ServiceAccountInfo[] getServiceAccountInfos() {
return serviceAccountInfos;
}

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

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
for (ServiceAccountInfo info : serviceAccountInfos) {
info.toXContent(builder, params);
}
builder.endObject();
return builder;
}

@Override
public String toString() {
return "GetServiceAccountResponse{" + "serviceAccountInfos=" + Arrays.toString(serviceAccountInfos) + '}';
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
GetServiceAccountResponse that = (GetServiceAccountResponse) o;
return Arrays.equals(serviceAccountInfos, that.serviceAccountInfos);
}

@Override
public int hashCode() {
return Arrays.hashCode(serviceAccountInfos);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* 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.service;

import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;

import java.io.IOException;
import java.util.Objects;

public class ServiceAccountInfo implements Writeable, ToXContent {

private final String principal;
private final RoleDescriptor roleDescriptor;

public ServiceAccountInfo(String principal, RoleDescriptor roleDescriptor) {
this.principal = Objects.requireNonNull(principal, "service account principal cannot be null");
this.roleDescriptor = Objects.requireNonNull(roleDescriptor, "service account descriptor cannot be null");
}

public ServiceAccountInfo(StreamInput in) throws IOException {
this.principal = in.readString();
this.roleDescriptor = new RoleDescriptor(in);
}

public String getPrincipal() {
return principal;
}

public RoleDescriptor getRoleDescriptor() {
return roleDescriptor;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(principal);
roleDescriptor.writeTo(out);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject(principal);
builder.field("role_descriptor");
roleDescriptor.toXContent(builder, params);
builder.endObject();
return builder;
}

@Override
public String toString() {
return "ServiceAccountInfo{" + "principal='" + principal + '\'' + ", roleDescriptor=" + roleDescriptor + '}';
}

@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
ServiceAccountInfo that = (ServiceAccountInfo) o;
return principal.equals(that.principal) && roleDescriptor.equals(that.roleDescriptor);
}

@Override
public int hashCode() {
return Objects.hash(principal, roleDescriptor);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* 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.service;

import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.test.AbstractWireSerializingTestCase;

import java.io.IOException;

public class GetServiceAccountRequestTests extends AbstractWireSerializingTestCase<GetServiceAccountRequest> {

@Override
protected Writeable.Reader<GetServiceAccountRequest> instanceReader() {
return GetServiceAccountRequest::new;
}

@Override
protected GetServiceAccountRequest createTestInstance() {
return new GetServiceAccountRequest(randomFrom(randomAlphaOfLengthBetween(3, 8), null),
randomFrom(randomAlphaOfLengthBetween(3, 8), null));
}

@Override
protected GetServiceAccountRequest mutateInstance(GetServiceAccountRequest instance) throws IOException {
if (randomBoolean()) {
return new GetServiceAccountRequest(
randomValueOtherThan(instance.getNamespace(), () -> randomFrom(randomAlphaOfLengthBetween(3, 8), null)),
instance.getServiceName());
} else {
return new GetServiceAccountRequest(
instance.getNamespace(),
randomValueOtherThan(instance.getServiceName(), () -> randomFrom(randomAlphaOfLengthBetween(3, 8), null)));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* 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.service;

import org.elasticsearch.common.bytes.BytesReference;
import org.elasticsearch.common.io.stream.Writeable;
import org.elasticsearch.common.xcontent.ToXContent;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.test.AbstractWireSerializingTestCase;
import org.elasticsearch.test.XContentTestUtils;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;

import java.io.IOException;
import java.util.Map;

import static org.hamcrest.Matchers.anEmptyMap;
import static org.hamcrest.Matchers.equalTo;

public class GetServiceAccountResponseTests extends AbstractWireSerializingTestCase<GetServiceAccountResponse> {

@Override
protected Writeable.Reader<GetServiceAccountResponse> instanceReader() {
return GetServiceAccountResponse::new;
}

@Override
protected GetServiceAccountResponse createTestInstance() {
final String principal = randomPrincipal();
return new GetServiceAccountResponse(randomBoolean()
? new ServiceAccountInfo[]{new ServiceAccountInfo(principal, getRoleDescriptorFor(principal))}
: new ServiceAccountInfo[0]);
}

@Override
protected GetServiceAccountResponse mutateInstance(GetServiceAccountResponse instance) throws IOException {
if (instance.getServiceAccountInfos().length == 0) {
final String principal = randomPrincipal();
return new GetServiceAccountResponse(new ServiceAccountInfo[]{
new ServiceAccountInfo(principal, getRoleDescriptorFor(principal))});
} else {
return new GetServiceAccountResponse(new ServiceAccountInfo[0]);
}
}

@SuppressWarnings("unchecked")
public void testToXContent() throws IOException {
final GetServiceAccountResponse response = createTestInstance();
XContentBuilder builder = XContentFactory.jsonBuilder();
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
final Map<String, Object> responseMap = XContentHelper.convertToMap(
BytesReference.bytes(builder),
false, builder.contentType()).v2();
final ServiceAccountInfo[] serviceAccountInfos = response.getServiceAccountInfos();
if (serviceAccountInfos.length == 0) {
assertThat(responseMap, anEmptyMap());
} else {
assertThat(responseMap.size(), equalTo(serviceAccountInfos.length));
for (int i = 0; i < serviceAccountInfos.length - 1; i++) {
final String key = serviceAccountInfos[i].getPrincipal();
assertRoleDescriptorEquals((Map<String, Object>) responseMap.get(key), serviceAccountInfos[i].getRoleDescriptor());
}
}
}

private String randomPrincipal() {
return randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8);
}

private RoleDescriptor getRoleDescriptorFor(String name) {
return new RoleDescriptor(name,
new String[] { "monitor", "manage_own_api_key" },
new RoleDescriptor.IndicesPrivileges[] {
RoleDescriptor.IndicesPrivileges.builder()
.indices("logs-*", "metrics-*", "traces-*")
.privileges("write", "create_index", "auto_configure").build() },
null,
null,
null,
null,
null);
}

private void assertRoleDescriptorEquals(Map<String, Object> responseFragment, RoleDescriptor roleDescriptor) throws IOException {
@SuppressWarnings("unchecked")
final Map<String, Object> descriptorMap = (Map<String, Object>) responseFragment.get("role_descriptor");
assertThat(RoleDescriptor.parse(roleDescriptor.getName(),
XContentTestUtils.convertToXContent(descriptorMap, XContentType.JSON), false, XContentType.JSON),
equalTo(roleDescriptor));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ public class Constants {
"cluster:admin/xpack/security/saml/invalidate",
"cluster:admin/xpack/security/saml/logout",
"cluster:admin/xpack/security/saml/prepare",
"cluster:admin/xpack/security/service_account/get",
"cluster:admin/xpack/security/service_account/token/create",
"cluster:admin/xpack/security/service_account/token/get",
"cluster:admin/xpack/security/token/create",
Expand Down
Loading

0 comments on commit 5404113

Please sign in to comment.