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

Define and use roles for authorization #8874

Merged
merged 21 commits into from Mar 26, 2019
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
4e17eba
Define roles and policies
mpolden Mar 22, 2019
75a3f48
Use roles for authorization
mpolden Mar 22, 2019
7e6efdf
Ensure that all path spec sets are disjoint
mpolden Mar 22, 2019
f19a72b
Verify context for build service paths
mpolden Mar 22, 2019
bff2606
Rename allow -> allows
mpolden Mar 25, 2019
97253bb
Rename deployment -> deploymentStatus
mpolden Mar 25, 2019
402ab8a
Move /zone/v1/ to deploymentStatus
mpolden Mar 25, 2019
e6bebce
Define seperate group for /athenz/v1/
mpolden Mar 25, 2019
18414aa
Fix somewhat funny typo resulting in very strict access control
jonmv Mar 25, 2019
bacdfe7
Remove path which has moved
jonmv Mar 25, 2019
a6db021
Ensure matches are unique for each path group
jonmv Mar 25, 2019
3ffee72
Specify prod for rotation overrisdes to avoid mutiple matches for ten…
jonmv Mar 25, 2019
0a530e8
Ensure path has correct state when evaluating context
jonmv Mar 25, 2019
cbea8cd
Move Principal to RoleResolver.membership signature
jonmv Mar 26, 2019
be8dafb
Test that build service can read application data for compile version
jonmv Mar 26, 2019
6d73b7f
Include all roles principal has in returned RoleMembership
jonmv Mar 26, 2019
85e3f4a
Remove unnecessary declaration of tenant read privileges
jonmv Mar 26, 2019
05bcb12
Update RoleMembershipTest to reflect changes
jonmv Mar 26, 2019
0e02dac
No principal when authorization filter is run is an error
jonmv Mar 26, 2019
09ae8c5
Short-circuit when everyone is authorized for the request
jonmv Mar 26, 2019
4518eb2
Return error messages, instead of throwing, as per superclass doc
jonmv Mar 26, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion config-provisioning/abi-spec.json
Expand Up @@ -707,7 +707,8 @@
"public static com.yahoo.config.provision.SystemName[] values()",
"public static com.yahoo.config.provision.SystemName valueOf(java.lang.String)",
"public static com.yahoo.config.provision.SystemName defaultSystem()",
"public static com.yahoo.config.provision.SystemName from(java.lang.String)"
"public static com.yahoo.config.provision.SystemName from(java.lang.String)",
"public static java.util.Set all()"
],
"fields": [
"public static final enum com.yahoo.config.provision.SystemName dev",
Expand Down
@@ -1,6 +1,9 @@
// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.config.provision;

import java.util.EnumSet;
import java.util.Set;

/**
* Systems in hosted Vespa
*
Expand Down Expand Up @@ -39,4 +42,8 @@ public static SystemName from(String value) {
}
}

public static Set<SystemName> all() {
return EnumSet.allOf(SystemName.class);
}

}

Large diffs are not rendered by default.

@@ -0,0 +1,43 @@
// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.role;

import com.yahoo.jdisc.http.HttpRequest;

import java.util.EnumSet;

/**
* Action defines an operation, typically a HTTP method, that may be performed on an entity in the controller
* (e.g. tenant or application).
*
* @author mpolden
*/
public enum Action {

create,
read,
update,
delete;

public static EnumSet<Action> all() {
return EnumSet.allOf(Action.class);
}

public static EnumSet<Action> write() {
return EnumSet.of(create, update, delete);
}

/** Returns the appropriate action for given HTTP method */
public static Action from(HttpRequest.Method method) {
switch (method) {
case POST: return Action.create;
case GET:
case OPTIONS:
case HEAD: return Action.read;
case PUT:
case PATCH: return Action.update;
case DELETE: return Action.delete;
default: throw new IllegalArgumentException("No action defined for method " + method);
}
}

}
@@ -0,0 +1,84 @@
// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.role;

import com.yahoo.config.provision.ApplicationName;
import com.yahoo.config.provision.SystemName;
import com.yahoo.config.provision.TenantName;

import java.util.Objects;
import java.util.Optional;

/**
* The context in which a role is valid.
*
* @author mpolden
*/
public class Context {
mpolden marked this conversation as resolved.
Show resolved Hide resolved

private final Optional<TenantName> tenant;
private final Optional<ApplicationName> application;
private final SystemName system;

private Context(Optional<TenantName> tenant, Optional<ApplicationName> application, SystemName system) {
this.tenant = Objects.requireNonNull(tenant, "tenant must be non-null");
this.application = Objects.requireNonNull(application, "application must be non-null");
this.system = Objects.requireNonNull(system, "system must be non-null");
}

/** A specific tenant this is valid for, if any */
public Optional<TenantName> tenant() {
return tenant;
}

/** A specific application this is valid for, if any */
public Optional<ApplicationName> application() {
return application;
}

/** System in which this is valid */
public SystemName system() {
return system;
}

/** Returns whether this context is considered limited */
public boolean limited() {
return tenant.isPresent() || application.isPresent();
}

/** Returns a context that has no restrictions on tenant or application in given system */
public static Context unlimitedIn(SystemName system) {
return new Context(Optional.empty(), Optional.empty(), system);
}

/** Returns a context that is limited to given tenant and system */
public static Context limitedTo(TenantName tenant, SystemName system) {
return new Context(Optional.of(tenant), Optional.empty(), system);
}

/** Returns a context that is limited to given tenant, application and system */
public static Context limitedTo(TenantName tenant, ApplicationName application, SystemName system) {
return new Context(Optional.of(tenant), Optional.of(application), system);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Context context = (Context) o;
return tenant.equals(context.tenant) &&
application.equals(context.application) &&
system == context.system;
}

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

@Override
public String toString() {
return "tenant " + tenant.map(TenantName::value).orElse("[none]") + ", application " +
application.map(ApplicationName::value).orElse("[none]") + ", system " + system;
}

}
@@ -0,0 +1,102 @@
// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.role;

import com.yahoo.restapi.Path;

import java.util.EnumSet;
import java.util.Optional;
import java.util.Set;

/**
* This declares and groups all known REST API paths in the controller.
*
* When creating a new API, its paths must be added here and a policy must be declared in {@link Policy}.
*
* @author mpolden
*/
public enum PathGroup {

/** Paths used for system management by operators */
operator("/controller/v1/{*}",
"/provision/v2/{*}",
"/flags/v1/{*}",
"/os/v1/{*}",
"/cost/v1/{*}",
"/zone/v2/{*}",
"/nodes/v2/{*}",
"/orchestrator/v1/{*}"),

/** Paths used when onboarding and creating a new tenants */
onboardingUser("/application/v4/user"),

// Tenant parameter is ignored here as context for the role is not defined until after a tenant has been created
onboardingTenant("/application/v4/tenant/{ignored}"),

/** Read-only paths used when onboarding tenants */
onboardingTenantInformation("/athenz/v1/",
"/athenz/v1/domains"),


/** Paths used by tenant/application administrators */
tenant("/application/v4/",
"/application/v4/property/",
"/application/v4/tenant/",
"/application/v4/tenant-pipeline/",
"/application/v4/tenant/{tenant}",
"/application/v4/tenant/{tenant}/application/",
"/application/v4/tenant/{tenant}/application/{application}",
"/application/v4/tenant/{tenant}/application/{application}/deploying/{*}",
"/application/v4/tenant/{tenant}/application/{application}/instance/{instance}/job/{job}/{*}",
"/application/v4/tenant/{tenant}/application/{application}/environment/dev/{*}",
"/application/v4/tenant/{tenant}/application/{application}/environment/perf/{*}",
"/application/v4/tenant/{tenant}/application/{application}/environment/prod/region/{region}/instance/{instance}/global-rotation/override"),

/** Paths used for deployments by build service(s) */
buildService("/application/v4/tenant/{tenant}/application/{application}/jobreport",
"/application/v4/tenant/{tenant}/application/{application}/submit",
"/application/v4/tenant/{tenant}/application/{application}/promote",
"/application/v4/tenant/{tenant}/application/{application}/environment/prod/{*}",
"/application/v4/tenant/{tenant}/application/{application}/environment/test/{*}",
"/application/v4/tenant/{tenant}/application/{application}/environment/staging/{*}"),

/** Read-only paths providing information related to deployments */
deploymentStatus("/badge/v1/{*}",
"/deployment/v1/{*}",
"/zone/v1/{*}");

final Set<String> pathSpecs;

PathGroup(String... pathSpecs) {
this.pathSpecs = Set.of(pathSpecs);
mpolden marked this conversation as resolved.
Show resolved Hide resolved
}

/** Returns path if it matches any spec in this group, with match groups set by the match. */
private Optional<Path> get(String path) {
Path matcher = new Path(path);
for (String spec : pathSpecs) // Iterate to be sure the Path's state is that of the match.
if (matcher.matches(spec)) return Optional.of(matcher);
return Optional.empty();
}

/** All known path groups */
public static Set<PathGroup> all() {
return EnumSet.allOf(PathGroup.class);
}

/** Returns whether this group matches path in given context */
public boolean matches(String path, Context context) {
return get(path).map(p -> {
boolean match = true;
String tenant = p.get("tenant");
if (tenant != null && context.tenant().isPresent()) {
Copy link
Member

Choose a reason for hiding this comment

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

I find it a bit strange that if the context has a tenant, but the path doesn't, it's a "match". Likewise for application.

Copy link
Member

Choose a reason for hiding this comment

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

And the other way around.

match = context.tenant().get().value().equals(tenant);
}
String application = p.get("application");
if (application != null && context.application().isPresent()) {
match &= context.application().get().value().equals(application);
}
return match;
}).orElse(false);
}

}
@@ -0,0 +1,67 @@
// Copyright 2019 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.vespa.hosted.controller.role;

import com.yahoo.config.provision.SystemName;

import java.util.Set;

/**
* Policies for REST APIs in the controller. A policy is only considered when defined in a {@link Role}.
*
* @author mpolden
*/
public enum Policy {

/** Operator policy allows access to everything in all systems */
operator(Privilege.grant(Action.all())
.on(PathGroup.all())
.in(SystemName.all())),

/**
* Tenant policy allows tenants to access their own tenant, in all systems, and allows global read access in
* selected systems
*/
tenant(Privilege.grant(Action.all())
.on(PathGroup.tenant)
.in(SystemName.all()),
Privilege.grant(Action.read)
.on(PathGroup.all())
.in(SystemName.main, SystemName.cd, SystemName.dev)),

/** Build service policy only allows access relevant for build service(s) */
buildService(Privilege.grant(Action.all())
.on(PathGroup.buildService)
.in(SystemName.all())),

/** Unauthorized policy allows creation of tenants and read of everything in selected systems */
unauthorized(Privilege.grant(Action.update)
.on(PathGroup.onboardingUser)
.in(SystemName.main, SystemName.cd, SystemName.dev),
Privilege.grant(Action.create)
.on(PathGroup.onboardingTenant)
.in(SystemName.main, SystemName.cd, SystemName.dev),
Privilege.grant(Action.read)
.on(PathGroup.onboardingTenantInformation)
.in(SystemName.main, SystemName.cd, SystemName.dev),
Privilege.grant(Action.read)
.on(PathGroup.all())
.in(SystemName.main, SystemName.cd, SystemName.dev),
Privilege.grant(Action.read)
.on(PathGroup.deploymentStatus)
.in(SystemName.all()));

private final Set<Privilege> privileges;

Policy(Privilege... privileges) {
this.privileges = Set.of(privileges);
}

/** Returns whether action is allowed on path in given context */
public boolean evaluate(Action action, String path, Context context) {
return privileges.stream().anyMatch(privilege -> privilege.actions().contains(action) &&
privilege.systems().contains(context.system()) &&
privilege.pathGroups().stream()
.anyMatch(pg -> pg.matches(path, context)));
}

}