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

HLRest: model role and privileges #35128

Merged
merged 34 commits into from
Nov 11, 2018
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
706b736
ClusterPrivilege
albertzaharovits Oct 24, 2018
21bc280
IndexPrivilege
albertzaharovits Oct 24, 2018
9e174eb
Index Privileges
albertzaharovits Oct 26, 2018
64a91e2
ApplicationResourcePrivilege WIP
albertzaharovits Oct 29, 2018
d98232b
Merge branch 'master' into hlrc_put_role
albertzaharovits Oct 30, 2018
aabbc5f
IndicesPrivileges is neat!
albertzaharovits Oct 30, 2018
9883726
Oh my!
albertzaharovits Oct 30, 2018
ba34f65
Proper manage app priv
albertzaharovits Oct 30, 2018
e03a05b
Bare RoleDescriptor
albertzaharovits Oct 30, 2018
a7b176b
Slightly less bare RoleDescriptor
albertzaharovits Oct 31, 2018
701f85e
All Collections are Lists, well..
albertzaharovits Oct 31, 2018
523f108
WIP :((
albertzaharovits Oct 31, 2018
ee37904
Fields are Set
albertzaharovits Oct 31, 2018
36f7be5
Role entity
albertzaharovits Oct 31, 2018
395e29c
Merge branch 'master' into hlrc_put_role
albertzaharovits Nov 1, 2018
4604127
Follow Tim's advice
albertzaharovits Nov 4, 2018
beaa373
Follow Tim's advice
albertzaharovits Nov 5, 2018
05ccbd0
Removed ManageApplicationsPrivilege
albertzaharovits Nov 5, 2018
4c339e9
Rename to GlobalPrivileges
albertzaharovits Nov 5, 2018
9b75cdb
public ApplicationResourcePrivileges constructor
albertzaharovits Nov 5, 2018
4839eaa
Merge branch 'master' into hlrc_put_role
albertzaharovits Nov 6, 2018
192295a
Merge branch 'master' into hlrc_put_role
albertzaharovits Nov 7, 2018
d83113d
Nits + javadoc
albertzaharovits Nov 7, 2018
1d9f874
ApplicationResourcePrivilegesTests
albertzaharovits Nov 7, 2018
d4170f2
WIP
albertzaharovits Nov 7, 2018
9869124
More tests
albertzaharovits Nov 8, 2018
3ead50c
More docs
albertzaharovits Nov 8, 2018
b68a3cf
WIP
albertzaharovits Nov 8, 2018
769a0e1
Merge branch 'master' into hlrc_put_role
albertzaharovits Nov 8, 2018
24351b4
Done!
albertzaharovits Nov 8, 2018
36d3203
Merge branch 'master' into hlrc_put_role
albertzaharovits Nov 9, 2018
20f14f7
Javadocs and privilegesByCategoryMap as member
albertzaharovits Nov 9, 2018
59014c6
Merge branch 'master' into hlrc_put_role
albertzaharovits Nov 9, 2018
7fd629a
Merge branch 'master' into hlrc_put_role
albertzaharovits Nov 9, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.client.security.user.privileges;

import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentHelper;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;

/**
* Represents privileges over resources that are scoped under an application.
* The application, resources and privileges are completely managed by the
* client and can be arbitrary string identifiers. Elasticsearch is not
* concerned by any resources under an application scope.
*/
public final class ApplicationResourcePrivileges implements ToXContentObject {

private static final ParseField APPLICATION = new ParseField("application");
private static final ParseField PRIVILEGES = new ParseField("privileges");
private static final ParseField RESOURCES = new ParseField("resources");

@SuppressWarnings("unchecked")
static final ConstructingObjectParser<ApplicationResourcePrivileges, Void> PARSER = new ConstructingObjectParser<>(
"application_privileges", false, constructorObjects -> {
// Don't ignore unknown fields. It is dangerous if the object we parse is also
// part of a request that we build later on, and the fields that we now ignore will
// end up being implicitly set to null in that request.
int i = 0;
final String application = (String) constructorObjects[i++];
final Collection<String> privileges = (Collection<String>) constructorObjects[i++];
final Collection<String> resources = (Collection<String>) constructorObjects[i];
return new ApplicationResourcePrivileges(application, privileges, resources);
});

static {
PARSER.declareString(constructorArg(), APPLICATION);
PARSER.declareStringArray(constructorArg(), PRIVILEGES);
PARSER.declareStringArray(constructorArg(), RESOURCES);
}

private final String application;
private final Set<String> privileges;
private final Set<String> resources;

/**
* Constructs privileges for resources under an application scope.
*
* @param application
* The application name. This identifier is completely under the
* clients control.
* @param privileges
* The privileges names. Cannot be null or empty. Privilege
* identifiers are completely under the clients control.
* @param resources
* The resources names. Cannot be null or empty. Resource identifiers
* are completely under the clients control.
*/
public ApplicationResourcePrivileges(String application, Collection<String> privileges, Collection<String> resources) {
if (Strings.isNullOrEmpty(application)) {
throw new IllegalArgumentException("application privileges must have an application name");
}
if (null == privileges || privileges.isEmpty()) {
throw new IllegalArgumentException("application privileges must define at least one privilege");
}
if (null == resources || resources.isEmpty()) {
throw new IllegalArgumentException("application privileges must refer to at least one resource");
}
this.application = application;
this.privileges = Collections.unmodifiableSet(new HashSet<>(privileges));
this.resources = Collections.unmodifiableSet(new HashSet<>(resources));
}

public String getApplication() {
return application;
}

public Set<String> getResources() {
return this.resources;
}

public Set<String> getPrivileges() {
return this.privileges;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || this.getClass() != o.getClass()) {
return false;
}
ApplicationResourcePrivileges that = (ApplicationResourcePrivileges) o;
return application.equals(that.application)
&& privileges.equals(that.privileges)
&& resources.equals(that.resources);
}

@Override
public int hashCode() {
return Objects.hash(application, privileges, resources);
}

@Override
public String toString() {
try {
return XContentHelper.toXContent(this, XContentType.JSON, true).utf8ToString();
} catch (IOException e) {
throw new RuntimeException("Unexpected", e);
}
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject();
builder.field(APPLICATION.getPreferredName(), application);
builder.field(PRIVILEGES.getPreferredName(), privileges);
builder.field(RESOURCES.getPreferredName(), resources);
return builder.endObject();
}

public static ApplicationResourcePrivileges fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.client.security.user.privileges;

import org.elasticsearch.common.xcontent.XContentParser;

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

/**
* Represents generic global cluster privileges that can be scoped by categories
* and then by operations. The privilege definition, as well as the operation
* identifier, are outside of the Elasticsearch jurisdiction. Categories are
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't quite follow (or perhaps don't agree) with your point about the definition being "outside of the Elasticsearch jurisdiction". Elasticsearch definitely controls & evaluates these.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was plainly wrong in my understanding.
This is now:

Represents generic global cluster privileges that can be scoped by categories
and then further by operations. The privilege's syntactic and semantic
meaning is specific to each category and operation; there is no general
definition template. It is not permitted to define different privileges under
the same category and operation.

* predefined and enforced by Elasticsearch. It is not permitted to define
* different privileges for the same category and operation.
*/
public class GlobalOperationPrivilege {

private final String category;
private final String operation;
private final Map<String, Object> privilege;

/**
* Constructs privileges under a certain {@code category} and for some
* {@code operation}. There is no constraint over the {@code operation}
* identifier, only the categories are predefined. The privilege definition is
* also out of Elasticsearch's control.
*
* @param category
* The category of the privilege.
* @param operation
* The operation of the privilege.
* @param privilege
* The privilege definition. This is out of the Elasticsearch's
* control.
*/
public GlobalOperationPrivilege(String category, String operation, Map<String, Object> privilege) {
this.category = Objects.requireNonNull(category);
this.operation = Objects.requireNonNull(operation);
if (privilege == null || privilege.isEmpty()) {
throw new IllegalArgumentException("Privileges cannot be empty or null");
}
this.privilege = Collections.unmodifiableMap(privilege);
}

public String getCategory() {
return category;
}

public String getOperation() {
return operation;
}

public Map<String, Object> getRaw() {
return privilege;
}

public static GlobalOperationPrivilege fromXContent(String category, String operation, XContentParser parser) throws IOException {
// parser is still placed on the field name, advance to next token (field value)
assert parser.currentToken().equals(XContentParser.Token.FIELD_NAME);
parser.nextToken();
return new GlobalOperationPrivilege(category, operation, parser.map());
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || (false == this instanceof GlobalOperationPrivilege)) {
return false;
}
final GlobalOperationPrivilege that = (GlobalOperationPrivilege) o;
return category.equals(that.category) && operation.equals(that.operation) && privilege.equals(that.privilege);
}

@Override
public int hashCode() {
return Objects.hash(category, operation, privilege);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

package org.elasticsearch.client.security.user.privileges;

import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;

import java.io.IOException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;

/**
* Represents global privileges. "Global Privilege" is a mantra for granular
* generic cluster privileges. These privileges are organized into categories.
* Elasticsearch defines the set of categories. Under each category there are
* operations that are under the clients jurisdiction. The privilege is hence
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not true.
The definition

"application": { "manage": { "applications": [ "kibana-*" ] } }

Instructs Elasticsearch that this role is allowed to manage application privileges under the application names "kibana-*".
It's not an instruction for Kibana, it's enforced by Elasticsearch.

Copy link
Contributor Author

@albertzaharovits albertzaharovits Nov 9, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wrong, I got carried astray by too much abstraction.
This is now:

Constructs privileges under a specific {@code category} and for some
{@code operation}. The privilege definition is flexible, it is a {@code Map},
and the semantics is bound to the {@code category} and {@code operation}.

* defined under an operation under a category.
*/
public final class GlobalPrivileges implements ToXContentObject {

// When categories change, adapting this field should suffice. Categories are NOT
// opaque "named_objects", we wish to maintain control over these namespaces
static final List<String> CATEGORIES = Collections.unmodifiableList(Arrays.asList("application"));

@SuppressWarnings("unchecked")
static final ConstructingObjectParser<GlobalPrivileges, Void> PARSER = new ConstructingObjectParser<>("global_category_privileges",
false, constructorObjects -> {
// ignore_unknown_fields is irrelevant here anyway, but let's keep it to false
// because this conveys strictness (woop woop)
return new GlobalPrivileges((Collection<GlobalOperationPrivilege>) constructorObjects[0]);
});

static {
for (final String category : CATEGORIES) {
PARSER.declareNamedObjects(optionalConstructorArg(),
(parser, context, operation) -> GlobalOperationPrivilege.fromXContent(category, operation, parser),
new ParseField(category));
}
}

private final Set<? extends GlobalOperationPrivilege> privileges;

/**
* Constructs global privileges by bundling the set of privileges.
*
* @param privileges
* The privileges under a category and for an operation in that category.
*/
public GlobalPrivileges(Collection<? extends GlobalOperationPrivilege> privileges) {
if (privileges == null || privileges.isEmpty()) {
throw new IllegalArgumentException("Privileges cannot be empty or null");
}
// duplicates are just ignored
this.privileges = Collections.unmodifiableSet(new HashSet<>(Objects.requireNonNull(privileges)));
final Map<String, List<GlobalOperationPrivilege>> privilegesByCategoryMap =
this.privileges.stream().collect(Collectors.groupingBy(GlobalOperationPrivilege::getCategory));
for (final Map.Entry<String, List<GlobalOperationPrivilege>> privilegesByCategory : privilegesByCategoryMap.entrySet()) {
// all operations for a specific category
final Set<String> allOperations = privilegesByCategory.getValue().stream().map(p -> p.getOperation()).collect(Collectors.toSet());
if (allOperations.size() != privilegesByCategory.getValue().size()) {
throw new IllegalArgumentException("Different privileges for the same category and operation are not permitted");
}
}
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
final Map<String, List<GlobalOperationPrivilege>> privilegesByCategoryMap =
this.privileges.stream().collect(Collectors.groupingBy(GlobalOperationPrivilege::getCategory));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we just keep this Map around?
We use it in the contructor, and in toXContent, couldn't it just be a field?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with your suggestion. At first, I have resisted duplicating information, but since it's immutable and private, I suppose it's OK.

builder.startObject();
for (final Map.Entry<String, List<GlobalOperationPrivilege>> privilegesByCategory : privilegesByCategoryMap.entrySet()) {
builder.startObject(privilegesByCategory.getKey());
for (final GlobalOperationPrivilege privilege : privilegesByCategory.getValue()) {
builder.field(privilege.getOperation(), privilege.getRaw());
}
builder.endObject();
}
return builder.endObject();
}

public static GlobalPrivileges fromXContent(XContentParser parser) {
return PARSER.apply(parser, null);
}

public Set<? extends GlobalOperationPrivilege> getPrivileges() {
return privileges;
}

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

@Override
public int hashCode() {
return Objects.hash(privileges);
}

}
Loading