Skip to content

Commit

Permalink
Security global privilege for writing profile data of applications (#…
Browse files Browse the repository at this point in the history
…83728)

This PR adds a new global privilege which can be used to restrict writes
for user profile data. The privilege is configurable for the names of
the top level keys in the profile data maps (`data` and `access`), which
by convetion are "application" names. Lastly it adds such a privilege,
for the `kibana-*` application namespace, to the `kibana_system`
built-in role.

Eg:

```
{
  "global": {
    "application": {
      "manage": {
        "applications": [...]
      }
    },
    "profile": {
      "write": {
          "applications": [...]
        }
      }
    }
}
```

Notes: * for every role there can be only one list of application names
for the write profile privilege, and the list does not support excludes
(and it supports wildcards) * there is no validation that the privilege
refers to valid application names (eg empty application name)
  • Loading branch information
albertzaharovits committed Feb 16, 2022
1 parent b001a6f commit 476240e
Show file tree
Hide file tree
Showing 20 changed files with 796 additions and 59 deletions.
5 changes: 5 additions & 0 deletions docs/changelog/83728.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
pr: 83728
summary: Security global privilege for updating profile data of applications
area: Authorization
type: enhancement
issues: []
9 changes: 6 additions & 3 deletions x-pack/docs/en/security/authorization/built-in-roles.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,11 @@ This role does not have access to editing tools in {kib}.
[[built-in-roles-kibana-system]] `kibana_system` ::
Grants access necessary for the {kib} system user to read from and write to the
{kib} indices, manage index templates and tokens, and check the availability of
the {es} cluster. This role grants read access to the `.monitoring-*` indices
and read and write access to the `.reporting-*` indices. For more information,
the {es} cluster. It also permits
<<security-user-profile-apis,activating, searching, and retrieving user profiles>>,
as well as updating user profile data for the `kibana-*` namespace.
This role grants read access to the `.monitoring-*` indices and read and write
access to the `.reporting-*` indices. For more information,
see {kibana-ref}/using-kibana-with-security.html[Configuring Security in {kib}].
+
NOTE: This role should not be assigned to users as the granted permissions may
Expand Down Expand Up @@ -172,7 +175,7 @@ Grants full access to cluster management and data indices. This role also grants
direct read-only access to restricted indices like `.security`. A user with the
`superuser` role can <<run-as-privilege, impersonate>> any other user in the system.
+
On {ecloud}, all standard users, including those with the `superuser` role are
On {ecloud}, all standard users, including those with the `superuser` role are
restricted from performing <<operator-only-functionality,operator-only>> actions.
+
IMPORTANT: This role can manage security and create roles with unlimited privileges.
Expand Down
21 changes: 14 additions & 7 deletions x-pack/docs/en/security/authorization/managing-roles.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -101,25 +101,32 @@ multiple data streams, indices, and aliases.

[[roles-global-priv]]
==== Global Privileges
The following describes the structure of a global privileges entry:
The following describes the structure of the global privileges entry:

[source,js]
-------
{
"application": {
"manage": { <1>
"applications": [ ... ] <2>
"applications": [ ... ] <2>
}
},
"profile": {
"write": { <3>
"applications": [ ... ] <4>
}
}
}
-------
// NOTCONSOLE

<1> The only supported global privilege is the ability to manage application
privileges
<1> The privilege for the ability to manage application privileges
<2> The list of application names that may be managed. This list supports
wildcards (e.g. `"myapp-*"`) and regular expressions (e.g.
`"/app[0-9]*/"`)
<3> The privilege for the ability to write the `access` and `data` of any user profile
<4> The list of names, wildcards and regular expressions to which the write
privilege is restricted to

[[roles-application-priv]]
==== Application Privileges
Expand Down Expand Up @@ -195,16 +202,16 @@ see <<custom-roles-authorization>>.
=== Role management UI

You can manage users and roles easily in {kib}. To
manage roles, log in to {kib} and go to *Management / Security / Roles*.
manage roles, log in to {kib} and go to *Management / Security / Roles*.

[discrete]
[[roles-management-api]]
=== Role management API

The _Role Management APIs_ enable you to add, update, remove and retrieve roles
dynamically. When you use the APIs to manage roles in the `native` realm, the
roles are stored in an internal {es} index. For more information and examples,
see <<security-role-apis>>.
roles are stored in an internal {es} index. For more information and examples,
see <<security-role-apis>>.

[discrete]
[[roles-management-file]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,11 @@ public List<NamedWriteableRegistry.Entry> getNamedWriteables() {
ConfigurableClusterPrivileges.ManageApplicationPrivileges.WRITEABLE_NAME,
ConfigurableClusterPrivileges.ManageApplicationPrivileges::createFrom
),
new NamedWriteableRegistry.Entry(
ConfigurableClusterPrivilege.class,
ConfigurableClusterPrivileges.WriteProfileDataPrivileges.WRITEABLE_NAME,
ConfigurableClusterPrivileges.WriteProfileDataPrivileges::createFrom
),
// security : role-mappings
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, AllExpression.NAME, AllExpression::new),
new NamedWriteableRegistry.Entry(RoleMapperExpression.class, AnyExpression.NAME, AnyExpression::new),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public RefreshPolicy getRefreshPolicy() {
return refreshPolicy;
}

public Set<String> applicationNames() {
public Set<String> getApplicationNames() {
final Set<String> names = new HashSet<>(access.keySet());
names.addAll(data.keySet());
return Set.copyOf(names);
Expand All @@ -90,7 +90,7 @@ public Set<String> applicationNames() {
@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
final Set<String> applicationNames = applicationNames();
final Set<String> applicationNames = getApplicationNames();
if (applicationNames.isEmpty()) {
validationException = addValidationError("update request is empty", validationException);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -110,9 +111,7 @@ public RoleDescriptor(
) {
this.name = name;
this.clusterPrivileges = clusterPrivileges != null ? clusterPrivileges : Strings.EMPTY_ARRAY;
this.configurableClusterPrivileges = configurableClusterPrivileges != null
? configurableClusterPrivileges
: ConfigurableClusterPrivileges.EMPTY_ARRAY;
this.configurableClusterPrivileges = sortConfigurableClusterPrivileges(configurableClusterPrivileges);
this.indicesPrivileges = indicesPrivileges != null ? indicesPrivileges : IndicesPrivileges.NONE;
this.applicationPrivileges = applicationPrivileges != null ? applicationPrivileges : ApplicationResourcePrivileges.NONE;
this.runAs = runAs != null ? runAs : Strings.EMPTY_ARRAY;
Expand Down Expand Up @@ -669,6 +668,23 @@ private static RoleDescriptor.IndicesPrivileges parseIndex(String roleName, XCon
.build();
}

private static ConfigurableClusterPrivilege[] sortConfigurableClusterPrivileges(
ConfigurableClusterPrivilege[] configurableClusterPrivileges
) {
if (null == configurableClusterPrivileges) {
return ConfigurableClusterPrivileges.EMPTY_ARRAY;
} else if (configurableClusterPrivileges.length < 2) {
return configurableClusterPrivileges;
} else {
ConfigurableClusterPrivilege[] configurableClusterPrivilegesCopy = Arrays.copyOf(
configurableClusterPrivileges,
configurableClusterPrivileges.length
);
Arrays.sort(configurableClusterPrivilegesCopy, Comparator.comparingInt(o -> o.getCategory().ordinal()));
return configurableClusterPrivilegesCopy;
}
}

private static void checkIfExceptFieldsIsSubsetOfGrantedFields(String roleName, String[] grantedFields, String[] deniedFields) {
try {
FieldPermissions.buildPermittedFieldsAutomaton(grantedFields, deniedFields);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ public interface ConfigurableClusterPrivilege extends NamedWriteable, ToXContent
* from the categories.
*/
enum Category {
APPLICATION(new ParseField("application"));
APPLICATION(new ParseField("application")),
PROFILE(new ParseField("profile"));

public final ParseField field;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import org.elasticsearch.xcontent.XContentParseException;
import org.elasticsearch.xcontent.XContentParser;
import org.elasticsearch.xpack.core.security.action.privilege.ApplicationPrivilegesRequest;
import org.elasticsearch.xpack.core.security.action.profile.UpdateProfileDataAction;
import org.elasticsearch.xpack.core.security.action.profile.UpdateProfileDataRequest;
import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission;
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege.Category;
import org.elasticsearch.xpack.core.security.support.StringMatcher;
Expand All @@ -30,6 +32,7 @@
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Predicate;

Expand Down Expand Up @@ -94,13 +97,25 @@ public static List<ConfigurableClusterPrivilege> parse(XContentParser parser) th
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
expectedToken(parser.currentToken(), parser, XContentParser.Token.FIELD_NAME);

expectFieldName(parser, Category.APPLICATION.field);
expectedToken(parser.nextToken(), parser, XContentParser.Token.START_OBJECT);
expectedToken(parser.nextToken(), parser, XContentParser.Token.FIELD_NAME);
expectFieldName(parser, Category.APPLICATION.field, Category.PROFILE.field);
if (Category.APPLICATION.field.match(parser.currentName(), parser.getDeprecationHandler())) {
expectedToken(parser.nextToken(), parser, XContentParser.Token.START_OBJECT);
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
expectedToken(parser.currentToken(), parser, XContentParser.Token.FIELD_NAME);

expectFieldName(parser, ManageApplicationPrivileges.Fields.MANAGE);
privileges.add(ManageApplicationPrivileges.parse(parser));
expectedToken(parser.nextToken(), parser, XContentParser.Token.END_OBJECT);
expectFieldName(parser, ManageApplicationPrivileges.Fields.MANAGE);
privileges.add(ManageApplicationPrivileges.parse(parser));
}
} else {
assert Category.PROFILE.field.match(parser.currentName(), parser.getDeprecationHandler());
expectedToken(parser.nextToken(), parser, XContentParser.Token.START_OBJECT);
while (parser.nextToken() != XContentParser.Token.END_OBJECT) {
expectedToken(parser.currentToken(), parser, XContentParser.Token.FIELD_NAME);

expectFieldName(parser, WriteProfileDataPrivileges.Fields.WRITE);
privileges.add(WriteProfileDataPrivileges.parse(parser));
}
}
}

return privileges;
Expand Down Expand Up @@ -131,6 +146,114 @@ private static void expectFieldName(XContentParser parser, ParseField... fields)
}
}

/**
* The {@link WriteProfileDataPrivileges} privilege is a {@link ConfigurableClusterPrivilege} that grants the
* ability to write the {@code data} and {@code access} sections of any user profile.
* The privilege is namespace configurable such that only specific top-level keys in the {@code data} and {@code access}
* dictionary permit writes (wildcards and regexps are supported, but exclusions are not).
*/
public static class WriteProfileDataPrivileges implements ConfigurableClusterPrivilege {
public static final String WRITEABLE_NAME = "write-profile-data-privileges";

private final Set<String> applicationNames;
private final Predicate<String> applicationPredicate;
private final Predicate<TransportRequest> requestPredicate;

public WriteProfileDataPrivileges(Set<String> applicationNames) {
this.applicationNames = Collections.unmodifiableSet(applicationNames);
this.applicationPredicate = StringMatcher.of(applicationNames);
this.requestPredicate = request -> {
if (request instanceof final UpdateProfileDataRequest updateProfileRequest) {
assert null == updateProfileRequest.validate();
final Collection<String> requestApplicationNames = updateProfileRequest.getApplicationNames();
return requestApplicationNames.stream().allMatch(application -> applicationPredicate.test(application));
}
return false;
};
}

@Override
public Category getCategory() {
return Category.PROFILE;
}

public Collection<String> getApplicationNames() {
return this.applicationNames;
}

@Override
public String getWriteableName() {
return WRITEABLE_NAME;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeCollection(this.applicationNames, StreamOutput::writeString);
}

public static WriteProfileDataPrivileges createFrom(StreamInput in) throws IOException {
final Set<String> applications = in.readSet(StreamInput::readString);
return new WriteProfileDataPrivileges(applications);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.field(Fields.WRITE.getPreferredName(), Map.of(Fields.APPLICATIONS.getPreferredName(), applicationNames));
}

public static WriteProfileDataPrivileges parse(XContentParser parser) throws IOException {
expectedToken(parser.currentToken(), parser, XContentParser.Token.FIELD_NAME);
expectFieldName(parser, Fields.WRITE);
expectedToken(parser.nextToken(), parser, XContentParser.Token.START_OBJECT);
expectedToken(parser.nextToken(), parser, XContentParser.Token.FIELD_NAME);
expectFieldName(parser, Fields.APPLICATIONS);
expectedToken(parser.nextToken(), parser, XContentParser.Token.START_ARRAY);
final String[] applications = XContentUtils.readStringArray(parser, false);
expectedToken(parser.nextToken(), parser, XContentParser.Token.END_OBJECT);
return new WriteProfileDataPrivileges(new LinkedHashSet<>(Arrays.asList(applications)));
}

@Override
public String toString() {
return "{"
+ getCategory()
+ ":"
+ Fields.WRITE.getPreferredName()
+ ":"
+ Fields.APPLICATIONS.getPreferredName()
+ "="
+ Strings.collectionToDelimitedString(applicationNames, ",")
+ "}";
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final WriteProfileDataPrivileges that = (WriteProfileDataPrivileges) o;
return this.applicationNames.equals(that.applicationNames);
}

@Override
public int hashCode() {
return applicationNames.hashCode();
}

@Override
public ClusterPermission.Builder buildPermission(ClusterPermission.Builder builder) {
return builder.add(this, Set.of(UpdateProfileDataAction.NAME), requestPredicate);
}

private interface Fields {
ParseField WRITE = new ParseField("write");
ParseField APPLICATIONS = new ParseField("applications");
}
}

/**
* The {@code ManageApplicationPrivileges} privilege is a {@link ConfigurableClusterPrivilege} that grants the
* ability to execute actions related to the management of application privileges (Get, Put, Delete) for a subset
Expand Down Expand Up @@ -164,7 +287,7 @@ public Category getCategory() {
}

public Collection<String> getApplicationNames() {
return Collections.unmodifiableCollection(this.applicationNames);
return this.applicationNames;
}

@Override
Expand All @@ -184,10 +307,7 @@ public static ManageApplicationPrivileges createFrom(StreamInput in) throws IOEx

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.field(
Fields.MANAGE.getPreferredName(),
Collections.singletonMap(Fields.APPLICATIONS.getPreferredName(), applicationNames)
);
return builder.field(Fields.MANAGE.getPreferredName(), Map.of(Fields.APPLICATIONS.getPreferredName(), applicationNames));
}

public static ManageApplicationPrivileges parse(XContentParser parser) throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction;
import org.elasticsearch.xpack.core.security.action.profile.ActivateProfileAction;
import org.elasticsearch.xpack.core.security.action.profile.GetProfileAction;
import org.elasticsearch.xpack.core.security.action.profile.UpdateProfileDataAction;
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege;
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges.ManageApplicationPrivileges;
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges.WriteProfileDataPrivileges;
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
import org.elasticsearch.xpack.core.security.user.KibanaSystemUser;
import org.elasticsearch.xpack.core.security.user.UsernamesField;
Expand Down Expand Up @@ -667,8 +667,6 @@ public static RoleDescriptor kibanaSystemRoleDescriptor(String name) {
"delegate_pki",
GetProfileAction.NAME,
ActivateProfileAction.NAME,
// TODO: this cluster action will be replaced with a special privilege that grants write access to a subset of namespaces
UpdateProfileDataAction.NAME,
// To facilitate ML UI functionality being controlled using Kibana security privileges
"manage_ml",
// The symbolic constant for this one is in SecurityActionMapper, so not accessible from X-Pack core
Expand Down Expand Up @@ -780,7 +778,9 @@ public static RoleDescriptor kibanaSystemRoleDescriptor(String name) {
.privileges("create_index", "delete_index", "read", "index")
.build(), },
null,
new ConfigurableClusterPrivilege[] { new ManageApplicationPrivileges(Collections.singleton("kibana-*")) },
new ConfigurableClusterPrivilege[] {
new ManageApplicationPrivileges(Set.of("kibana-*")),
new WriteProfileDataPrivileges(Set.of("kibana-*")) },
null,
MetadataUtils.DEFAULT_RESERVED_METADATA,
null
Expand Down

0 comments on commit 476240e

Please sign in to comment.