Skip to content

Commit d49ee3c

Browse files
authored
Service Accounts - Get service account API (#71315) (#71515)
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.
1 parent c88f93b commit d49ee3c

File tree

13 files changed

+628
-2
lines changed

13 files changed

+628
-2
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.service;
9+
10+
import org.elasticsearch.action.ActionType;
11+
12+
public class GetServiceAccountAction extends ActionType<GetServiceAccountResponse> {
13+
14+
public static final String NAME = "cluster:admin/xpack/security/service_account/get";
15+
public static final GetServiceAccountAction INSTANCE = new GetServiceAccountAction();
16+
17+
public GetServiceAccountAction() {
18+
super(NAME, GetServiceAccountResponse::new);
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.service;
9+
10+
import org.elasticsearch.action.ActionRequest;
11+
import org.elasticsearch.action.ActionRequestValidationException;
12+
import org.elasticsearch.common.Nullable;
13+
import org.elasticsearch.common.io.stream.StreamInput;
14+
import org.elasticsearch.common.io.stream.StreamOutput;
15+
16+
import java.io.IOException;
17+
import java.util.Objects;
18+
19+
public class GetServiceAccountRequest extends ActionRequest {
20+
21+
@Nullable
22+
private final String namespace;
23+
@Nullable
24+
private final String serviceName;
25+
26+
public GetServiceAccountRequest(@Nullable String namespace, @Nullable String serviceName) {
27+
this.namespace = namespace;
28+
this.serviceName = serviceName;
29+
}
30+
31+
public GetServiceAccountRequest(StreamInput in) throws IOException {
32+
super(in);
33+
this.namespace = in.readOptionalString();
34+
this.serviceName = in.readOptionalString();
35+
}
36+
37+
public String getNamespace() {
38+
return namespace;
39+
}
40+
41+
public String getServiceName() {
42+
return serviceName;
43+
}
44+
45+
@Override
46+
public boolean equals(Object o) {
47+
if (this == o)
48+
return true;
49+
if (o == null || getClass() != o.getClass())
50+
return false;
51+
GetServiceAccountRequest that = (GetServiceAccountRequest) o;
52+
return Objects.equals(namespace, that.namespace) && Objects.equals(serviceName, that.serviceName);
53+
}
54+
55+
@Override
56+
public int hashCode() {
57+
return Objects.hash(namespace, serviceName);
58+
}
59+
60+
@Override
61+
public void writeTo(StreamOutput out) throws IOException {
62+
super.writeTo(out);
63+
out.writeOptionalString(namespace);
64+
out.writeOptionalString(serviceName);
65+
}
66+
67+
@Override
68+
public ActionRequestValidationException validate() {
69+
return null;
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.service;
9+
10+
import org.elasticsearch.action.ActionResponse;
11+
import org.elasticsearch.common.io.stream.StreamInput;
12+
import org.elasticsearch.common.io.stream.StreamOutput;
13+
import org.elasticsearch.common.xcontent.ToXContentObject;
14+
import org.elasticsearch.common.xcontent.XContentBuilder;
15+
16+
import java.io.IOException;
17+
import java.util.Arrays;
18+
import java.util.Objects;
19+
20+
public class GetServiceAccountResponse extends ActionResponse implements ToXContentObject {
21+
22+
private final ServiceAccountInfo[] serviceAccountInfos;
23+
24+
public GetServiceAccountResponse(ServiceAccountInfo[] serviceAccountInfos) {
25+
this.serviceAccountInfos = Objects.requireNonNull(serviceAccountInfos);
26+
}
27+
28+
public GetServiceAccountResponse(StreamInput in) throws IOException {
29+
super(in);
30+
this.serviceAccountInfos = in.readArray(ServiceAccountInfo::new, ServiceAccountInfo[]::new);
31+
}
32+
33+
public ServiceAccountInfo[] getServiceAccountInfos() {
34+
return serviceAccountInfos;
35+
}
36+
37+
@Override
38+
public void writeTo(StreamOutput out) throws IOException {
39+
out.writeArray(serviceAccountInfos);
40+
}
41+
42+
@Override
43+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
44+
builder.startObject();
45+
for (ServiceAccountInfo info : serviceAccountInfos) {
46+
info.toXContent(builder, params);
47+
}
48+
builder.endObject();
49+
return builder;
50+
}
51+
52+
@Override
53+
public String toString() {
54+
return "GetServiceAccountResponse{" + "serviceAccountInfos=" + Arrays.toString(serviceAccountInfos) + '}';
55+
}
56+
57+
@Override
58+
public boolean equals(Object o) {
59+
if (this == o)
60+
return true;
61+
if (o == null || getClass() != o.getClass())
62+
return false;
63+
GetServiceAccountResponse that = (GetServiceAccountResponse) o;
64+
return Arrays.equals(serviceAccountInfos, that.serviceAccountInfos);
65+
}
66+
67+
@Override
68+
public int hashCode() {
69+
return Arrays.hashCode(serviceAccountInfos);
70+
}
71+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.service;
9+
10+
import org.elasticsearch.common.io.stream.StreamInput;
11+
import org.elasticsearch.common.io.stream.StreamOutput;
12+
import org.elasticsearch.common.io.stream.Writeable;
13+
import org.elasticsearch.common.xcontent.ToXContent;
14+
import org.elasticsearch.common.xcontent.XContentBuilder;
15+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
16+
17+
import java.io.IOException;
18+
import java.util.Objects;
19+
20+
public class ServiceAccountInfo implements Writeable, ToXContent {
21+
22+
private final String principal;
23+
private final RoleDescriptor roleDescriptor;
24+
25+
public ServiceAccountInfo(String principal, RoleDescriptor roleDescriptor) {
26+
this.principal = Objects.requireNonNull(principal, "service account principal cannot be null");
27+
this.roleDescriptor = Objects.requireNonNull(roleDescriptor, "service account descriptor cannot be null");
28+
}
29+
30+
public ServiceAccountInfo(StreamInput in) throws IOException {
31+
this.principal = in.readString();
32+
this.roleDescriptor = new RoleDescriptor(in);
33+
}
34+
35+
public String getPrincipal() {
36+
return principal;
37+
}
38+
39+
public RoleDescriptor getRoleDescriptor() {
40+
return roleDescriptor;
41+
}
42+
43+
@Override
44+
public void writeTo(StreamOutput out) throws IOException {
45+
out.writeString(principal);
46+
roleDescriptor.writeTo(out);
47+
}
48+
49+
@Override
50+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
51+
builder.startObject(principal);
52+
builder.field("role_descriptor");
53+
roleDescriptor.toXContent(builder, params);
54+
builder.endObject();
55+
return builder;
56+
}
57+
58+
@Override
59+
public String toString() {
60+
return "ServiceAccountInfo{" + "principal='" + principal + '\'' + ", roleDescriptor=" + roleDescriptor + '}';
61+
}
62+
63+
@Override
64+
public boolean equals(Object o) {
65+
if (this == o)
66+
return true;
67+
if (o == null || getClass() != o.getClass())
68+
return false;
69+
ServiceAccountInfo that = (ServiceAccountInfo) o;
70+
return principal.equals(that.principal) && roleDescriptor.equals(that.roleDescriptor);
71+
}
72+
73+
@Override
74+
public int hashCode() {
75+
return Objects.hash(principal, roleDescriptor);
76+
}
77+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.service;
9+
10+
import org.elasticsearch.common.io.stream.Writeable;
11+
import org.elasticsearch.test.AbstractWireSerializingTestCase;
12+
13+
import java.io.IOException;
14+
15+
public class GetServiceAccountRequestTests extends AbstractWireSerializingTestCase<GetServiceAccountRequest> {
16+
17+
@Override
18+
protected Writeable.Reader<GetServiceAccountRequest> instanceReader() {
19+
return GetServiceAccountRequest::new;
20+
}
21+
22+
@Override
23+
protected GetServiceAccountRequest createTestInstance() {
24+
return new GetServiceAccountRequest(randomFrom(randomAlphaOfLengthBetween(3, 8), null),
25+
randomFrom(randomAlphaOfLengthBetween(3, 8), null));
26+
}
27+
28+
@Override
29+
protected GetServiceAccountRequest mutateInstance(GetServiceAccountRequest instance) throws IOException {
30+
if (randomBoolean()) {
31+
return new GetServiceAccountRequest(
32+
randomValueOtherThan(instance.getNamespace(), () -> randomFrom(randomAlphaOfLengthBetween(3, 8), null)),
33+
instance.getServiceName());
34+
} else {
35+
return new GetServiceAccountRequest(
36+
instance.getNamespace(),
37+
randomValueOtherThan(instance.getServiceName(), () -> randomFrom(randomAlphaOfLengthBetween(3, 8), null)));
38+
}
39+
}
40+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.action.service;
9+
10+
import org.elasticsearch.common.bytes.BytesReference;
11+
import org.elasticsearch.common.io.stream.Writeable;
12+
import org.elasticsearch.common.xcontent.ToXContent;
13+
import org.elasticsearch.common.xcontent.XContentBuilder;
14+
import org.elasticsearch.common.xcontent.XContentFactory;
15+
import org.elasticsearch.common.xcontent.XContentHelper;
16+
import org.elasticsearch.common.xcontent.XContentType;
17+
import org.elasticsearch.test.AbstractWireSerializingTestCase;
18+
import org.elasticsearch.test.XContentTestUtils;
19+
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
20+
21+
import java.io.IOException;
22+
import java.util.Map;
23+
24+
import static org.hamcrest.Matchers.anEmptyMap;
25+
import static org.hamcrest.Matchers.equalTo;
26+
27+
public class GetServiceAccountResponseTests extends AbstractWireSerializingTestCase<GetServiceAccountResponse> {
28+
29+
@Override
30+
protected Writeable.Reader<GetServiceAccountResponse> instanceReader() {
31+
return GetServiceAccountResponse::new;
32+
}
33+
34+
@Override
35+
protected GetServiceAccountResponse createTestInstance() {
36+
final String principal = randomPrincipal();
37+
return new GetServiceAccountResponse(randomBoolean()
38+
? new ServiceAccountInfo[]{new ServiceAccountInfo(principal, getRoleDescriptorFor(principal))}
39+
: new ServiceAccountInfo[0]);
40+
}
41+
42+
@Override
43+
protected GetServiceAccountResponse mutateInstance(GetServiceAccountResponse instance) throws IOException {
44+
if (instance.getServiceAccountInfos().length == 0) {
45+
final String principal = randomPrincipal();
46+
return new GetServiceAccountResponse(new ServiceAccountInfo[]{
47+
new ServiceAccountInfo(principal, getRoleDescriptorFor(principal))});
48+
} else {
49+
return new GetServiceAccountResponse(new ServiceAccountInfo[0]);
50+
}
51+
}
52+
53+
@SuppressWarnings("unchecked")
54+
public void testToXContent() throws IOException {
55+
final GetServiceAccountResponse response = createTestInstance();
56+
XContentBuilder builder = XContentFactory.jsonBuilder();
57+
response.toXContent(builder, ToXContent.EMPTY_PARAMS);
58+
final Map<String, Object> responseMap = XContentHelper.convertToMap(
59+
BytesReference.bytes(builder),
60+
false, builder.contentType()).v2();
61+
final ServiceAccountInfo[] serviceAccountInfos = response.getServiceAccountInfos();
62+
if (serviceAccountInfos.length == 0) {
63+
assertThat(responseMap, anEmptyMap());
64+
} else {
65+
assertThat(responseMap.size(), equalTo(serviceAccountInfos.length));
66+
for (int i = 0; i < serviceAccountInfos.length - 1; i++) {
67+
final String key = serviceAccountInfos[i].getPrincipal();
68+
assertRoleDescriptorEquals((Map<String, Object>) responseMap.get(key), serviceAccountInfos[i].getRoleDescriptor());
69+
}
70+
}
71+
}
72+
73+
private String randomPrincipal() {
74+
return randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8);
75+
}
76+
77+
private RoleDescriptor getRoleDescriptorFor(String name) {
78+
return new RoleDescriptor(name,
79+
new String[] { "monitor", "manage_own_api_key" },
80+
new RoleDescriptor.IndicesPrivileges[] {
81+
RoleDescriptor.IndicesPrivileges.builder()
82+
.indices("logs-*", "metrics-*", "traces-*")
83+
.privileges("write", "create_index", "auto_configure").build() },
84+
null,
85+
null,
86+
null,
87+
null,
88+
null);
89+
}
90+
91+
private void assertRoleDescriptorEquals(Map<String, Object> responseFragment, RoleDescriptor roleDescriptor) throws IOException {
92+
@SuppressWarnings("unchecked")
93+
final Map<String, Object> descriptorMap = (Map<String, Object>) responseFragment.get("role_descriptor");
94+
assertThat(RoleDescriptor.parse(roleDescriptor.getName(),
95+
XContentTestUtils.convertToXContent(descriptorMap, XContentType.JSON), false, XContentType.JSON),
96+
equalTo(roleDescriptor));
97+
}
98+
}

x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java

+1
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ public class Constants {
190190
"cluster:admin/xpack/security/saml/invalidate",
191191
"cluster:admin/xpack/security/saml/logout",
192192
"cluster:admin/xpack/security/saml/prepare",
193+
"cluster:admin/xpack/security/service_account/get",
193194
"cluster:admin/xpack/security/service_account/token/create",
194195
"cluster:admin/xpack/security/service_account/token/get",
195196
"cluster:admin/xpack/security/token/create",

0 commit comments

Comments
 (0)