Skip to content

Commit

Permalink
Introduce role description field (elastic#107088)
Browse files Browse the repository at this point in the history
This commit introduces new `description` field to roles definitions. 
The description is optional and can have max 1000 characters.

Role API:
```json
POST /_security/role/viewer
{
  "description": "Grants permission to view all indices.",
  "indices": [
    {
      "names": [ "*" ],
      "privileges": [ "read" , "view_index_metadata"]
    }
  ]
}
```

File-based role:
```yml
viewer:
  description: 'Grants permission to view all indices.' 
  indices:
    - names: [ '*' ]
      privileges: [ 'read', 'view_index_metadata' ]
```
  • Loading branch information
slobodanadamovic authored May 8, 2024
1 parent 15be94a commit 7b62ea9
Show file tree
Hide file tree
Showing 57 changed files with 1,611 additions and 430 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/107088.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 107088
summary: Introduce role description field
area: Authorization
type: enhancement
issues: []
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ static TransportVersion def(int id) {
public static final TransportVersion SHUTDOWN_REQUEST_TIMEOUTS_FIX = def(8_651_00_0);
public static final TransportVersion INDEXING_PRESSURE_REQUEST_REJECTIONS_COUNT = def(8_652_00_0);
public static final TransportVersion ROLLUP_USAGE = def(8_653_00_0);
public static final TransportVersion SECURITY_ROLE_DESCRIPTION = def(8_654_00_0);

/*
* STOP! READ THIS FIRST! No, really,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class PutRoleRequest extends ActionRequest {
private List<RoleDescriptor.RemoteIndicesPrivileges> remoteIndicesPrivileges = new ArrayList<>();
private RemoteClusterPermissions remoteClusterPermissions = RemoteClusterPermissions.NONE;
private boolean restrictRequest = false;
private String description;

public PutRoleRequest() {}

Expand All @@ -63,6 +64,10 @@ public void name(String name) {
this.name = name;
}

public void description(String description) {
this.description = description;
}

public void cluster(String... clusterPrivilegesArray) {
this.clusterPrivileges = clusterPrivilegesArray;
}
Expand Down Expand Up @@ -164,6 +169,10 @@ public String name() {
return name;
}

public String description() {
return description;
}

public String[] cluster() {
return clusterPrivileges;
}
Expand Down Expand Up @@ -213,7 +222,8 @@ public RoleDescriptor roleDescriptor() {
Collections.emptyMap(),
remoteIndicesPrivileges.toArray(new RoleDescriptor.RemoteIndicesPrivileges[0]),
remoteClusterPermissions,
null
null,
description
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
*/
public class PutRoleRequestBuilder extends ActionRequestBuilder<PutRoleRequest, PutRoleResponse> {

private static final RoleDescriptor.Parser ROLE_DESCRIPTOR_PARSER = RoleDescriptor.parserBuilder().build();
private static final RoleDescriptor.Parser ROLE_DESCRIPTOR_PARSER = RoleDescriptor.parserBuilder().allowDescription(true).build();

public PutRoleRequestBuilder(ElasticsearchClient client) {
super(client, PutRoleAction.INSTANCE, new PutRoleRequest());
Expand All @@ -43,6 +43,7 @@ public PutRoleRequestBuilder source(String name, BytesReference source, XContent
request.addApplicationPrivileges(descriptor.getApplicationPrivileges());
request.runAs(descriptor.getRunAs());
request.metadata(descriptor.getMetadata());
request.description(descriptor.getDescription());
return this;
}

Expand All @@ -51,6 +52,11 @@ public PutRoleRequestBuilder name(String name) {
return this;
}

public PutRoleRequestBuilder description(String description) {
request.description(description);
return this;
}

public PutRoleRequestBuilder cluster(String... cluster) {
request.cluster(cluster);
return this;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege;
import org.elasticsearch.xpack.core.security.authz.restriction.WorkflowResolver;
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
import org.elasticsearch.xpack.core.security.support.Validation;

import java.util.Arrays;
import java.util.Set;
Expand Down Expand Up @@ -102,6 +103,12 @@ public static ActionRequestValidationException validate(
}
}
}
if (roleDescriptor.hasDescription()) {
Validation.Error error = Validation.Roles.validateRoleDescription(roleDescriptor.getDescription());
if (error != null) {
validationException = addValidationError(error.toString(), validationException);
}
}
return validationException;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,10 @@ public static final class RoleDescriptorsBytes implements Writeable {

public static final RoleDescriptorsBytes EMPTY = new RoleDescriptorsBytes(new BytesArray("{}"));

private static final RoleDescriptor.Parser ROLE_DESCRIPTOR_PARSER = RoleDescriptor.parserBuilder().build();
private static final RoleDescriptor.Parser ROLE_DESCRIPTOR_PARSER = RoleDescriptor.parserBuilder()
.allowRestriction(true)
.allowDescription(true)
.build();

private final BytesReference rawBytes;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@
import java.util.Map;
import java.util.Objects;

import static org.elasticsearch.common.xcontent.XContentHelper.createParserNotCompressed;

/**
* A holder for a Role that contains user-readable information about the Role
* without containing the actual Role object.
Expand All @@ -70,6 +72,7 @@ public class RoleDescriptor implements ToXContentObject, Writeable {
private final Restriction restriction;
private final Map<String, Object> metadata;
private final Map<String, Object> transientMetadata;
private final String description;

/**
* Needed as a stop-gap measure because {@link FieldPermissionsCache} has state (settings) but we need to use one
Expand All @@ -93,7 +96,7 @@ public RoleDescriptor(

/**
* @deprecated Use {@link #RoleDescriptor(String, String[], IndicesPrivileges[], ApplicationResourcePrivileges[],
* ConfigurableClusterPrivilege[], String[], Map, Map, RemoteIndicesPrivileges[], RemoteClusterPermissions, Restriction)}
* ConfigurableClusterPrivilege[], String[], Map, Map, RemoteIndicesPrivileges[], RemoteClusterPermissions, Restriction, String)}
*/
@Deprecated
public RoleDescriptor(
Expand All @@ -108,7 +111,7 @@ public RoleDescriptor(

/**
* @deprecated Use {@link #RoleDescriptor(String, String[], IndicesPrivileges[], ApplicationResourcePrivileges[],
* ConfigurableClusterPrivilege[], String[], Map, Map, RemoteIndicesPrivileges[], RemoteClusterPermissions, Restriction)}
* ConfigurableClusterPrivilege[], String[], Map, Map, RemoteIndicesPrivileges[], RemoteClusterPermissions, Restriction, String)}
*/
@Deprecated
public RoleDescriptor(
Expand All @@ -130,7 +133,8 @@ public RoleDescriptor(
transientMetadata,
RemoteIndicesPrivileges.NONE,
RemoteClusterPermissions.NONE,
Restriction.NONE
Restriction.NONE,
null
);
}

Expand All @@ -155,7 +159,8 @@ public RoleDescriptor(
transientMetadata,
RemoteIndicesPrivileges.NONE,
RemoteClusterPermissions.NONE,
Restriction.NONE
Restriction.NONE,
null
);
}

Expand All @@ -170,7 +175,8 @@ public RoleDescriptor(
@Nullable Map<String, Object> transientMetadata,
@Nullable RemoteIndicesPrivileges[] remoteIndicesPrivileges,
@Nullable RemoteClusterPermissions remoteClusterPermissions,
@Nullable Restriction restriction
@Nullable Restriction restriction,
@Nullable String description
) {
this.name = name;
this.clusterPrivileges = clusterPrivileges != null ? clusterPrivileges : Strings.EMPTY_ARRAY;
Expand All @@ -187,6 +193,7 @@ public RoleDescriptor(
? remoteClusterPermissions
: RemoteClusterPermissions.NONE;
this.restriction = restriction != null ? restriction : Restriction.NONE;
this.description = description != null ? description : "";
}

public RoleDescriptor(StreamInput in) throws IOException {
Expand Down Expand Up @@ -218,12 +225,21 @@ public RoleDescriptor(StreamInput in) throws IOException {
} else {
this.remoteClusterPermissions = RemoteClusterPermissions.NONE;
}
if (in.getTransportVersion().onOrAfter(TransportVersions.SECURITY_ROLE_DESCRIPTION)) {
this.description = in.readOptionalString();
} else {
this.description = "";
}
}

public String getName() {
return this.name;
}

public String getDescription() {
return description;
}

public String[] getClusterPrivileges() {
return this.clusterPrivileges;
}
Expand Down Expand Up @@ -272,6 +288,10 @@ public boolean hasRunAs() {
return runAs.length != 0;
}

public boolean hasDescription() {
return description.length() != 0;
}

public boolean hasUnsupportedPrivilegesInsideAPIKeyConnectedRemoteCluster() {
return hasConfigurableClusterPrivileges()
|| hasApplicationPrivileges()
Expand Down Expand Up @@ -338,6 +358,7 @@ public String toString() {
sb.append(group.toString()).append(",");
}
sb.append("], restriction=").append(restriction);
sb.append(", description=").append(description);
sb.append("]");
return sb.toString();
}
Expand All @@ -358,7 +379,8 @@ public boolean equals(Object o) {
if (Arrays.equals(runAs, that.runAs) == false) return false;
if (Arrays.equals(remoteIndicesPrivileges, that.remoteIndicesPrivileges) == false) return false;
if (remoteClusterPermissions.equals(that.remoteClusterPermissions) == false) return false;
return restriction.equals(that.restriction);
if (restriction.equals(that.restriction) == false) return false;
return Objects.equals(description, that.description);
}

@Override
Expand All @@ -373,6 +395,7 @@ public int hashCode() {
result = 31 * result + Arrays.hashCode(remoteIndicesPrivileges);
result = 31 * result + remoteClusterPermissions.hashCode();
result = 31 * result + restriction.hashCode();
result = 31 * result + Objects.hashCode(description);
return result;
}

Expand Down Expand Up @@ -431,6 +454,9 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params, boolea
if (hasRestriction()) {
builder.field(Fields.RESTRICTION.getPreferredName(), restriction);
}
if (hasDescription()) {
builder.field(Fields.DESCRIPTION.getPreferredName(), description);
}
return builder.endObject();
}

Expand All @@ -456,17 +482,22 @@ public void writeTo(StreamOutput out) throws IOException {
if (out.getTransportVersion().onOrAfter(TransportVersions.ROLE_REMOTE_CLUSTER_PRIVS)) {
remoteClusterPermissions.writeTo(out);
}
if (out.getTransportVersion().onOrAfter(TransportVersions.SECURITY_ROLE_DESCRIPTION)) {
out.writeOptionalString(description);
}
}

public static Parser.Builder parserBuilder() {
return new Parser.Builder();
}

public record Parser(boolean allow2xFormat, boolean allowRestriction) {
public record Parser(boolean allow2xFormat, boolean allowRestriction, boolean allowDescription) {

public static final class Builder {

private boolean allow2xFormat = false;
private boolean allowRestriction = false;
private boolean allowDescription = false;

private Builder() {}

Expand All @@ -480,8 +511,13 @@ public Builder allowRestriction(boolean allowRestriction) {
return this;
}

public Builder allowDescription(boolean allowDescription) {
this.allowDescription = allowDescription;
return this;
}

public Parser build() {
return new Parser(allow2xFormat, allowRestriction);
return new Parser(allow2xFormat, allowRestriction, allowDescription);
}

}
Expand Down Expand Up @@ -565,6 +601,8 @@ public RoleDescriptor parse(String name, XContentParser parser) throws IOExcepti
remoteClusterPermissions = parseRemoteCluster(name, parser);
} else if (allowRestriction && Fields.RESTRICTION.match(currentFieldName, parser.getDeprecationHandler())) {
restriction = Restriction.parse(name, parser);
} else if (allowDescription && Fields.DESCRIPTION.match(currentFieldName, parser.getDeprecationHandler())) {
description = parser.text();
} else if (Fields.TYPE.match(currentFieldName, parser.getDeprecationHandler())) {
// don't need it
} else {
Expand All @@ -586,7 +624,8 @@ public RoleDescriptor parse(String name, XContentParser parser) throws IOExcepti
null,
remoteIndicesPrivileges,
remoteClusterPermissions,
restriction
restriction,
description
);

}
Expand Down Expand Up @@ -686,7 +725,7 @@ public static PrivilegesToCheck parsePrivilegesToCheck(
}

private static XContentParser createParser(BytesReference source, XContentType xContentType) throws IOException {
return XContentHelper.createParserNotCompressed(LoggingDeprecationHandler.XCONTENT_PARSER_CONFIG, source, xContentType);
return createParserNotCompressed(LoggingDeprecationHandler.XCONTENT_PARSER_CONFIG, source, xContentType);
}

public static RoleDescriptor.IndicesPrivileges[] parseIndices(String roleName, XContentParser parser, boolean allow2xFormat)
Expand Down Expand Up @@ -1821,5 +1860,6 @@ public interface Fields {
ParseField TYPE = new ParseField("type");
ParseField RESTRICTION = new ParseField("restriction");
ParseField WORKFLOWS = new ParseField("workflows");
ParseField DESCRIPTION = new ParseField("description");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@ public record RoleDescriptorsIntersection(Collection<Set<RoleDescriptor>> roleDe

public static RoleDescriptorsIntersection EMPTY = new RoleDescriptorsIntersection(Collections.emptyList());

private static final RoleDescriptor.Parser ROLE_DESCRIPTOR_PARSER = RoleDescriptor.parserBuilder().allowRestriction(true).build();
private static final RoleDescriptor.Parser ROLE_DESCRIPTOR_PARSER = RoleDescriptor.parserBuilder()
.allowRestriction(true)
.allowDescription(true)
.build();

public RoleDescriptorsIntersection(RoleDescriptor roleDescriptor) {
this(List.of(Set.of(roleDescriptor)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,7 @@ static RoleDescriptor kibanaSystem(String name) {
getRemoteIndicesReadPrivileges("traces-apm.*"),
getRemoteIndicesReadPrivileges("traces-apm-*") },
null,
null,
null
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ public class ReservedRolesStore implements BiConsumer<Set<String>, ActionListene
new String[] { "*" }
)
),
null,
null
);

Expand Down Expand Up @@ -201,6 +202,7 @@ private static Map<String, RoleDescriptor> initializeReservedRoles() {
getRemoteIndicesReadPrivileges("/metrics-(beats|elasticsearch|enterprisesearch|kibana|logstash).*/"),
getRemoteIndicesReadPrivileges("metricbeat-*") },
null,
null,
null
)
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/
package org.elasticsearch.xpack.core.security.support;

import org.elasticsearch.common.Strings;
import org.elasticsearch.common.settings.SecureString;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.xpack.core.security.authc.esnative.ClientReservedRealm;
Expand Down Expand Up @@ -204,10 +205,19 @@ public static Error validatePassword(SecureString password) {

public static final class Roles {

public static final int MAX_DESCRIPTION_LENGTH = 1000;

public static Error validateRoleName(String roleName, boolean allowReserved) {
return validateRoleName(roleName, allowReserved, MAX_NAME_LENGTH);
}

public static Error validateRoleDescription(String description) {
if (description != null && description.length() > MAX_DESCRIPTION_LENGTH) {
return new Error(Strings.format("Role description must be less than %s characters.", MAX_DESCRIPTION_LENGTH));
}
return null;
}

static Error validateRoleName(String roleName, boolean allowReserved, int maxLength) {
if (roleName == null) {
return new Error("role name is missing");
Expand Down
Loading

0 comments on commit 7b62ea9

Please sign in to comment.