You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@aurora.apache.org by ke...@apache.org on 2015/04/03 23:20:12 UTC
aurora git commit: Extract job key from RPC parameters
Repository: aurora
Updated Branches:
refs/heads/master 9e46dd8b1 -> 05d75e5dc
Extract job key from RPC parameters
Testing Done:
./gradlew -Pq build
Bugs closed: AURORA-1187
Reviewed at https://reviews.apache.org/r/32329/
Project: http://git-wip-us.apache.org/repos/asf/aurora/repo
Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/05d75e5d
Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/05d75e5d
Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/05d75e5d
Branch: refs/heads/master
Commit: 05d75e5dc104f0f2adce011639acf085042cee99
Parents: 9e46dd8
Author: Kevin Sweeney <ke...@apache.org>
Authored: Fri Apr 3 17:19:55 2015 -0400
Committer: Kevin Sweeney <ke...@apache.org>
Committed: Fri Apr 3 17:19:55 2015 -0400
----------------------------------------------------------------------
config/pmd/custom.xml | 34 +-
.../aurora/scheduler/http/api/ApiBeta.java | 4 +-
.../aurora/scheduler/http/api/ApiModule.java | 3 +-
.../http/api/security/ApiSecurityModule.java | 42 ++-
.../http/api/security/AuthorizingParam.java | 10 +-
.../http/api/security/FieldGetter.java | 73 ++++
.../http/api/security/FieldGetters.java | 50 +++
.../ShiroAuthenticatingThriftInterceptor.java | 63 ++++
.../security/ShiroAuthorizingInterceptor.java | 100 +++++
.../ShiroAuthorizingParamInterceptor.java | 372 +++++++++++++++++++
.../api/security/ShiroThriftInterceptor.java | 101 -----
.../http/api/security/ThriftFieldGetter.java | 66 ++++
.../aurora/scheduler/thrift/aop/AopModule.java | 2 +-
.../aurora/scheduler/http/api/ApiBetaTest.java | 8 +-
.../apache/aurora/scheduler/http/api/ApiIT.java | 8 +-
.../http/api/security/ApiSecurityIT.java | 64 +++-
...hiroAuthenticatingThriftInterceptorTest.java | 66 ++++
.../ShiroAuthorizingInterceptorTest.java | 94 +++++
.../ShiroAuthorizingParamInterceptorTest.java | 189 ++++++++++
.../security/ShiroThriftInterceptorTest.java | 106 ------
.../api/security/ThriftFieldGetterTest.java | 46 +++
.../aurora/scheduler/thrift/ThriftIT.java | 3 +-
.../scheduler/thrift/aop/AopModuleTest.java | 2 +-
23 files changed, 1264 insertions(+), 242 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/config/pmd/custom.xml
----------------------------------------------------------------------
diff --git a/config/pmd/custom.xml b/config/pmd/custom.xml
index 521fd50..8ad6228 100644
--- a/config/pmd/custom.xml
+++ b/config/pmd/custom.xml
@@ -22,7 +22,21 @@ limitations under the License.
Aurora PMD ruleset.
</description>
- <rule ref="rulesets/java/basic.xml"/>
+ <rule ref="rulesets/java/basic.xml">
+ <!-- Duplicate definitions - defined in empty.xml below and marked deprecated.
+ See http://sourceforge.net/p/pmd/discussion/188193/thread/6e9c6017/ -->
+ <exclude name="EmptyCatchBlock"/>
+ <exclude name="EmptyIfStmt"/>
+ <exclude name="EmptyWhileStmt"/>
+ <exclude name="EmptyTryBlock"/>
+ <exclude name="EmptyFinallyBlock"/>
+ <exclude name="EmptySwitchStatements"/>
+ <exclude name="EmptySynchronizedBlock"/>
+ <exclude name="EmptyStatementNotInLoop"/>
+ <exclude name="EmptyInitializer"/>
+ <exclude name="EmptyStatementBlock"/>
+ <exclude name="EmptyStaticInitializer"/>
+ </rule>
<rule ref="rulesets/java/design.xml">
<!-- We're not currently focusing on localization. -->
<exclude name="SimpleDateFormatNeedsLocale"/>
@@ -38,10 +52,20 @@ limitations under the License.
TODO(wfarner): Break apart god classes. -->
<exclude name="GodClass"/>
</rule>
- <rule ref="rulesets/java/empty.xml"/>
- <rule ref="rulesets/java/imports.xml">
- <!-- We frequently use static imports for enum fields (making other code more concise), but
- those trip this rule. -->
+ <rule ref="rulesets/java/empty.xml">
+ <!-- Configured below -->
+ <exclude name="EmptyCatchBlock"/>
+ </rule>
+ <rule ref="rulesets/java/empty.xml/EmptyCatchBlock">
+ <properties>
+ <!-- Some APIs, like the Java Reflection API, use exceptions to indicate the absence of
+ a value and we legitimately want to ignore them. -->
+ <property name="allowCommentedBlocks" value="true"/>
+ </properties>
+ </rule>
+ <rule ref="rulesets/java/imports.xml">
+ <!-- We frequently use static imports for enum fields (making other code more concise), but
+ those trip this rule. -->
<exclude name="TooManyStaticImports"/>
</rule>
<rule ref="rulesets/java/logging-java.xml">
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/http/api/ApiBeta.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/ApiBeta.java b/src/main/java/org/apache/aurora/scheduler/http/api/ApiBeta.java
index 827e85b..690e82e 100644
--- a/src/main/java/org/apache/aurora/scheduler/http/api/ApiBeta.java
+++ b/src/main/java/org/apache/aurora/scheduler/http/api/ApiBeta.java
@@ -46,10 +46,10 @@ import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonSyntaxException;
-import org.apache.aurora.gen.AuroraAdmin;
import org.apache.aurora.gen.AuroraAdmin.Iface;
import org.apache.aurora.scheduler.storage.entities.AuroraAdminMetadata;
import org.apache.aurora.scheduler.thrift.Responses;
+import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin;
import static org.apache.aurora.scheduler.http.api.GsonMessageBodyHandler.GSON;
@@ -66,7 +66,7 @@ public class ApiBeta {
private final Iface api;
@Inject
- ApiBeta(AuroraAdmin.Iface api) {
+ ApiBeta(AnnotatedAuroraAdmin api) {
this.api = Objects.requireNonNull(api);
}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/http/api/ApiModule.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/ApiModule.java b/src/main/java/org/apache/aurora/scheduler/http/api/ApiModule.java
index 2408cd1..63c31ee 100644
--- a/src/main/java/org/apache/aurora/scheduler/http/api/ApiModule.java
+++ b/src/main/java/org/apache/aurora/scheduler/http/api/ApiModule.java
@@ -28,6 +28,7 @@ import org.apache.aurora.gen.AuroraAdmin;
import org.apache.aurora.scheduler.http.CorsFilter;
import org.apache.aurora.scheduler.http.JettyServerModule;
import org.apache.aurora.scheduler.http.LeaderRedirectFilter;
+import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin;
import org.apache.thrift.protocol.TJSONProtocol;
import org.apache.thrift.server.TServlet;
import org.eclipse.jetty.servlet.DefaultServlet;
@@ -80,7 +81,7 @@ public class ApiModule extends ServletModule {
@Provides
@Singleton
- TServlet provideApiThriftServlet(AuroraAdmin.Iface schedulerThriftInterface) {
+ TServlet provideApiThriftServlet(AnnotatedAuroraAdmin schedulerThriftInterface) {
return new TServlet(
new AuroraAdmin.Processor<>(schedulerThriftInterface), new TJSONProtocol.Factory());
}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityModule.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityModule.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityModule.java
index ec6a02c..1f773ca 100644
--- a/src/main/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityModule.java
+++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityModule.java
@@ -13,6 +13,7 @@
*/
package org.apache.aurora.scheduler.http.api.security;
+import java.lang.reflect.Method;
import java.util.Set;
import javax.inject.Singleton;
@@ -21,6 +22,7 @@ import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Module;
import com.google.inject.Provides;
+import com.google.inject.matcher.Matcher;
import com.google.inject.matcher.Matchers;
import com.google.inject.name.Names;
import com.google.inject.servlet.RequestScoped;
@@ -34,6 +36,7 @@ import org.apache.aurora.gen.AuroraAdmin;
import org.apache.aurora.gen.AuroraSchedulerManager;
import org.apache.aurora.scheduler.app.Modules;
import org.apache.aurora.scheduler.http.api.ApiModule;
+import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.guice.aop.ShiroAopModule;
import org.apache.shiro.guice.web.ShiroWebModule;
@@ -50,6 +53,16 @@ import static java.util.Objects.requireNonNull;
* this package.
*/
public class ApiSecurityModule extends ServletModule {
+ /**
+ * Prefix for the permission protecting all AuroraSchedulerManager RPCs.
+ */
+ public static final String AURORA_SCHEDULER_MANAGER_PERMISSION = "thrift.AuroraSchedulerManager";
+
+ /**
+ * Prefix for the permission protecting all AuroraAdmin RPCs.
+ */
+ public static final String AURORA_ADMIN_PERMISSION = "thrift.AuroraAdmin";
+
public static final String HTTP_REALM_NAME = "Apache Aurora Scheduler";
@CmdLine(name = "enable_api_security",
@@ -61,6 +74,14 @@ public class ApiSecurityModule extends ServletModule {
private static final Arg<Set<Module>> SHIRO_REALM_MODULE = Arg.<Set<Module>>create(
ImmutableSet.of(Modules.lazilyInstantiated(IniShiroRealmModule.class)));
+ @VisibleForTesting
+ static final Matcher<Method> AURORA_SCHEDULER_MANAGER_SERVICE =
+ GuiceUtils.interfaceMatcher(AuroraSchedulerManager.Iface.class, true);
+
+ @VisibleForTesting
+ static final Matcher<Method> AURORA_ADMIN_SERVICE =
+ GuiceUtils.interfaceMatcher(AuroraAdmin.Iface.class, true);
+
private final boolean enableApiSecurity;
private final Set<Module> shiroConfigurationModules;
@@ -110,18 +131,29 @@ public class ApiSecurityModule extends ServletModule {
// TODO(ksweeney): Disable RememberMe cookie.
install(new ShiroAopModule());
- MethodInterceptor apiInterceptor = new ShiroThriftInterceptor("thrift.AuroraSchedulerManager");
+
+ // It is important that authentication happen before authorization is attempted, otherwise
+ // the authorizing interceptor will always fail.
+ MethodInterceptor authenticatingInterceptor = new ShiroAuthenticatingThriftInterceptor();
+ requestInjection(authenticatingInterceptor);
+ bindInterceptor(
+ Matchers.subclassesOf(AuroraSchedulerManager.Iface.class),
+ AURORA_SCHEDULER_MANAGER_SERVICE.or(AURORA_ADMIN_SERVICE),
+ authenticatingInterceptor);
+
+ MethodInterceptor apiInterceptor =
+ new ShiroAuthorizingParamInterceptor(AURORA_SCHEDULER_MANAGER_PERMISSION);
requestInjection(apiInterceptor);
bindInterceptor(
Matchers.subclassesOf(AuroraSchedulerManager.Iface.class),
- GuiceUtils.interfaceMatcher(AuroraSchedulerManager.Iface.class, true),
+ AURORA_SCHEDULER_MANAGER_SERVICE,
apiInterceptor);
- MethodInterceptor adminInterceptor = new ShiroThriftInterceptor("thrift.AuroraAdmin");
+ MethodInterceptor adminInterceptor = new ShiroAuthorizingInterceptor(AURORA_ADMIN_PERMISSION);
requestInjection(adminInterceptor);
bindInterceptor(
- Matchers.subclassesOf(AuroraAdmin.Iface.class),
- GuiceUtils.interfaceMatcher(AuroraAdmin.Iface.class, true),
+ Matchers.subclassesOf(AnnotatedAuroraAdmin.class),
+ AURORA_ADMIN_SERVICE,
adminInterceptor);
}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/http/api/security/AuthorizingParam.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/AuthorizingParam.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/AuthorizingParam.java
index 8089879..11d7e46 100644
--- a/src/main/java/org/apache/aurora/scheduler/http/api/security/AuthorizingParam.java
+++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/AuthorizingParam.java
@@ -13,16 +13,17 @@
*/
package org.apache.aurora.scheduler.http.api.security;
+import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
- * Signals to {@link org.apache.aurora.scheduler.http.api.security.ShiroThriftInterceptor} that this
- * parameter should be used to determine the instance the calling subject is attempting to operate
- * on. Methods using this parameter should ensure that the RPC cannot operate on an instance
- * outside the scope of this parameter, otherwise a privilege escalation vulnerability exists.
+ * Signals to {@link ShiroAuthorizingParamInterceptor} that this parameter should be used to
+ * determine the instance the calling subject is attempting to operate on. Methods using this
+ * parameter should ensure that the RPC cannot operate on an instance outside the scope of this
+ * parameter, otherwise a privilege escalation vulnerability exists.
*
* <p>
* A method intercepted by this interceptor that does not contain an AuthorizingParam or with
@@ -69,4 +70,5 @@ import java.lang.annotation.Target;
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
+@Documented
public @interface AuthorizingParam { }
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/http/api/security/FieldGetter.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/FieldGetter.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/FieldGetter.java
new file mode 100644
index 0000000..b2ca012
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/FieldGetter.java
@@ -0,0 +1,73 @@
+/**
+ * Licensed 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.apache.aurora.scheduler.http.api.security;
+
+import com.google.common.base.Function;
+import com.google.common.base.Optional;
+
+import static java.util.Objects.requireNonNull;
+
+/**
+ * Function for retrieving an optional field from a thrift struct with runtime type-information.
+ *
+ * @param <T> the container struct
+ * @param <V> a field that can be contained within T
+ */
+interface FieldGetter<T, V> extends Function<T, Optional<V>> {
+ /**
+ * The type of the container struct.
+ */
+ Class<T> getStructClass();
+
+ /**
+ * The type of the optionally-contained struct.
+ */
+ Class<V> getValueClass();
+
+ abstract class AbstractFieldGetter<T, V> implements FieldGetter<T, V> {
+ private final Class<T> structClass;
+ private final Class<V> valueClass;
+
+ protected AbstractFieldGetter(Class<T> structClass, Class<V> valueClass) {
+ this.structClass = requireNonNull(structClass);
+ this.valueClass = requireNonNull(valueClass);
+ }
+
+ @Override
+ public final Class<T> getStructClass() {
+ return structClass;
+ }
+
+ @Override
+ public final Class<V> getValueClass() {
+ return valueClass;
+ }
+ }
+
+ /**
+ * Special case of field getter that can get itself.
+ *
+ * @param <T> The input and ouput type.
+ */
+ class IdentityFieldGetter<T> extends AbstractFieldGetter<T, T> {
+ IdentityFieldGetter(Class<T> structClass) {
+ super(structClass, structClass);
+ }
+
+ @Override
+ public Optional<T> apply(T input) {
+ return Optional.fromNullable(input);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/http/api/security/FieldGetters.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/FieldGetters.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/FieldGetters.java
new file mode 100644
index 0000000..a833672
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/FieldGetters.java
@@ -0,0 +1,50 @@
+/**
+ * Licensed 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.apache.aurora.scheduler.http.api.security;
+
+import com.google.common.base.Optional;
+
+import org.apache.thrift.TBase;
+
+final class FieldGetters {
+ private FieldGetters() {
+ // Utility class.
+ }
+
+ public static <P extends TBase<P, ?>, C extends TBase<C, ?>, G extends TBase<G, ?>>
+ FieldGetter<P, G> compose(final FieldGetter<P, C> parent, final FieldGetter<C, G> child) {
+
+ return new FieldGetter<P, G>() {
+ @Override
+ public Class<P> getStructClass() {
+ return parent.getStructClass();
+ }
+
+ @Override
+ public Class<G> getValueClass() {
+ return child.getValueClass();
+ }
+
+ @Override
+ public Optional<G> apply(P input) {
+ Optional<C> parentValue = parent.apply(input);
+ if (parentValue.isPresent()) {
+ return child.apply(parentValue.get());
+ } else {
+ return Optional.absent();
+ }
+ }
+ };
+ }
+}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthenticatingThriftInterceptor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthenticatingThriftInterceptor.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthenticatingThriftInterceptor.java
new file mode 100644
index 0000000..bf7828b
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthenticatingThriftInterceptor.java
@@ -0,0 +1,63 @@
+/**
+ * Licensed 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.apache.aurora.scheduler.http.api.security;
+
+import javax.inject.Provider;
+
+import com.google.inject.Inject;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.apache.shiro.authz.UnauthenticatedException;
+import org.apache.shiro.subject.Subject;
+
+import static java.util.Objects.requireNonNull;
+
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Prevents invocation of intercepted methods if the current {@link Subject} is not authenticated.
+ */
+class ShiroAuthenticatingThriftInterceptor implements MethodInterceptor {
+ private volatile boolean initialized;
+
+ private Provider<Subject> subjectProvider;
+
+ ShiroAuthenticatingThriftInterceptor() {
+ // Guice constructor.
+ }
+
+ @Inject
+ void initialize(Provider<Subject> newSubjectProvider) {
+ checkState(!initialized);
+
+ subjectProvider = requireNonNull(newSubjectProvider);
+
+ initialized = true;
+ }
+
+ @Override
+ public Object invoke(MethodInvocation invocation) throws Throwable {
+ checkState(initialized);
+ Subject subject = subjectProvider.get();
+ if (subject.isAuthenticated()) {
+ return invocation.proceed();
+ } else {
+ // This is a special exception that will signal the BasicHttpAuthenticationFilter to send
+ // a 401 with a challenge. This is necessary at this layer since we only apply this
+ // interceptor to methods that require authentication.
+ throw new UnauthenticatedException();
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingInterceptor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingInterceptor.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingInterceptor.java
new file mode 100644
index 0000000..7a124cc
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingInterceptor.java
@@ -0,0 +1,100 @@
+/**
+ * Licensed 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.apache.aurora.scheduler.http.api.security;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.logging.Logger;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Joiner;
+import com.twitter.common.base.MorePreconditions;
+import com.twitter.common.stats.StatsProvider;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.apache.aurora.gen.Response;
+import org.apache.aurora.gen.ResponseCode;
+import org.apache.aurora.scheduler.thrift.Responses;
+import org.apache.shiro.authz.Permission;
+import org.apache.shiro.authz.permission.WildcardPermission;
+import org.apache.shiro.subject.Subject;
+
+import static java.util.Objects.requireNonNull;
+
+import static com.google.common.base.Preconditions.checkArgument;
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Does not allow the intercepted method call to proceed if the caller does not have permission to
+ * call it.
+ * The {@link org.apache.shiro.authz.Permission} checked is a
+ * {@link org.apache.shiro.authz.permission.WildcardPermission} constructed from a prefix and
+ * the name of the method being invoked. For example if the prefix is {@code api} and the method
+ * is {@code snapshot} the current {@link org.apache.shiro.subject.Subject} must have the
+ * {@code api:snapshot} permission.
+ */
+class ShiroAuthorizingInterceptor implements MethodInterceptor {
+ private static final Logger LOG = Logger.getLogger(ShiroAuthorizingInterceptor.class.getName());
+
+ @VisibleForTesting
+ static final String SHIRO_AUTHORIZATION_FAILURES = "shiro_authorization_failures";
+
+ private static final Joiner PERMISSION_JOINER = Joiner.on(":");
+
+ private final String permissionPrefix;
+
+ private volatile boolean initialized;
+
+ private Provider<Subject> subjectProvider;
+ private AtomicLong shiroAdminAuthorizationFailures;
+
+ ShiroAuthorizingInterceptor(String permissionPrefix) {
+ this.permissionPrefix = MorePreconditions.checkNotBlank(permissionPrefix);
+ }
+
+ @Inject
+ void initialize(Provider<Subject> newSubjectProvider, StatsProvider statsProvider) {
+ checkState(!initialized);
+
+ subjectProvider = requireNonNull(newSubjectProvider);
+ shiroAdminAuthorizationFailures = statsProvider.makeCounter(SHIRO_AUTHORIZATION_FAILURES);
+
+ initialized = true;
+ }
+
+ @Override
+ public Object invoke(MethodInvocation invocation) throws Throwable {
+ checkState(initialized);
+ Method method = invocation.getMethod();
+ checkArgument(Response.class.isAssignableFrom(method.getReturnType()));
+
+ Subject subject = subjectProvider.get();
+ Permission checkedPermission = new WildcardPermission(
+ PERMISSION_JOINER.join(permissionPrefix, method.getName()));
+ if (subject.isPermitted(checkedPermission)) {
+ return invocation.proceed();
+ } else {
+ shiroAdminAuthorizationFailures.incrementAndGet();
+ String responseMessage =
+ "Subject " + subject.getPrincipal() + " lacks permission " + checkedPermission;
+ LOG.warning(responseMessage);
+ // TODO(ksweeney): 403 FORBIDDEN would be a more accurate translation of this response code.
+ return Responses.addMessage(Responses.empty(), ResponseCode.AUTH_FAILED, responseMessage);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingParamInterceptor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingParamInterceptor.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingParamInterceptor.java
new file mode 100644
index 0000000..fde6c84
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingParamInterceptor.java
@@ -0,0 +1,372 @@
+/**
+ * Licensed 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.apache.aurora.scheduler.http.api.security;
+
+import java.lang.reflect.Method;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicLong;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheBuilder;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.AbstractSequentialIterator;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.reflect.Invokable;
+import com.google.common.reflect.Parameter;
+import com.twitter.common.base.MorePreconditions;
+import com.twitter.common.stats.StatsProvider;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.apache.aurora.gen.AddInstancesConfig;
+import org.apache.aurora.gen.JobConfiguration;
+import org.apache.aurora.gen.JobKey;
+import org.apache.aurora.gen.JobUpdateKey;
+import org.apache.aurora.gen.JobUpdateRequest;
+import org.apache.aurora.gen.Lock;
+import org.apache.aurora.gen.LockKey;
+import org.apache.aurora.gen.Response;
+import org.apache.aurora.gen.ResponseCode;
+import org.apache.aurora.gen.TaskConfig;
+import org.apache.aurora.gen.TaskQuery;
+import org.apache.aurora.scheduler.base.JobKeys;
+import org.apache.aurora.scheduler.base.Query;
+import org.apache.aurora.scheduler.http.api.security.FieldGetter.AbstractFieldGetter;
+import org.apache.aurora.scheduler.http.api.security.FieldGetter.IdentityFieldGetter;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.apache.aurora.scheduler.thrift.Responses;
+import org.apache.shiro.authz.Permission;
+import org.apache.shiro.authz.permission.WildcardPermission;
+import org.apache.shiro.subject.Subject;
+import org.apache.thrift.TBase;
+
+import static com.google.common.base.Preconditions.checkState;
+
+/**
+ * Interceptor that extracts and validates job keys from parameters annotated with
+ * {@link org.apache.aurora.scheduler.http.api.security.AuthorizingParam} and performs permission
+ * checks scoped to it.
+ *
+ * <p>
+ * For example, if intercepting a class that implements {@code A}:
+ *
+ * <pre>
+ * public interface A {
+ * Response setInstances(@AuthorizingParam JobKey jobKey, int instances);
+ * }
+ * </pre>
+ *
+ * This interceptor will check that the current {@link org.apache.shiro.subject.Subject} has the
+ * permission (prefix + ":setInstances:role:env:name").
+ *
+ * <p>
+ * It is important that this interceptor only be applied to methods returning
+ * {@link org.apache.aurora.gen.Response} and that authentication is called before this interceptor
+ * is invoked, otherwise this interceptor will not allow the invocation to proceed.
+ */
+class ShiroAuthorizingParamInterceptor implements MethodInterceptor {
+ @VisibleForTesting
+ static final FieldGetter<TaskQuery, JobKey> QUERY_TO_JOB_KEY =
+ new AbstractFieldGetter<TaskQuery, JobKey>(TaskQuery.class, JobKey.class) {
+ @Override
+ public Optional<JobKey> apply(TaskQuery input) {
+ Optional<Set<IJobKey>> targetJobs = JobKeys.from(Query.arbitrary(input));
+ if (targetJobs.isPresent() && targetJobs.get().size() == 1) {
+ return Optional.of(Iterables.getOnlyElement(targetJobs.get()))
+ .transform(IJobKey.TO_BUILDER);
+ } else {
+ return Optional.absent();
+ }
+ }
+ };
+
+ private static final FieldGetter<JobUpdateRequest, TaskConfig> UPDATE_REQUEST_GETTER =
+ new ThriftFieldGetter<>(
+ JobUpdateRequest.class,
+ JobUpdateRequest._Fields.TASK_CONFIG,
+ TaskConfig.class);
+
+ private static final FieldGetter<TaskConfig, JobKey> TASK_CONFIG_GETTER =
+ new ThriftFieldGetter<>(TaskConfig.class, TaskConfig._Fields.JOB, JobKey.class);
+
+ private static final FieldGetter<JobConfiguration, JobKey> JOB_CONFIGURATION_GETTER =
+ new ThriftFieldGetter<>(JobConfiguration.class, JobConfiguration._Fields.KEY, JobKey.class);
+
+ private static final FieldGetter<Lock, LockKey> LOCK_GETTER =
+ new ThriftFieldGetter<>(Lock.class, Lock._Fields.KEY, LockKey.class);
+
+ private static final FieldGetter<LockKey, JobKey> LOCK_KEY_GETTER =
+ new ThriftFieldGetter<>(LockKey.class, LockKey._Fields.JOB, JobKey.class);
+
+ private static final FieldGetter<JobUpdateKey, JobKey> JOB_UPDATE_KEY_GETTER =
+ new ThriftFieldGetter<>(JobUpdateKey.class, JobUpdateKey._Fields.JOB, JobKey.class);
+
+ private static final FieldGetter<AddInstancesConfig, JobKey> ADD_INSTANCES_CONFIG_GETTER =
+ new ThriftFieldGetter<>(
+ AddInstancesConfig.class,
+ AddInstancesConfig._Fields.KEY,
+ JobKey.class);
+
+ @SuppressWarnings("unchecked")
+ private static final Set<FieldGetter<?, JobKey>> FIELD_GETTERS =
+ ImmutableSet.<FieldGetter<?, JobKey>>of(
+ FieldGetters.compose(UPDATE_REQUEST_GETTER, TASK_CONFIG_GETTER),
+ TASK_CONFIG_GETTER,
+ JOB_CONFIGURATION_GETTER,
+ FieldGetters.compose(LOCK_GETTER, LOCK_KEY_GETTER),
+ LOCK_KEY_GETTER,
+ JOB_UPDATE_KEY_GETTER,
+ ADD_INSTANCES_CONFIG_GETTER,
+ QUERY_TO_JOB_KEY,
+ new IdentityFieldGetter<>(JobKey.class));
+
+ private static final Map<Class<?>, Function<?, Optional<JobKey>>> FIELD_GETTERS_BY_TYPE =
+ ImmutableMap.<Class<?>, Function<?, Optional<JobKey>>>builder()
+ .putAll(Maps.uniqueIndex(
+ FIELD_GETTERS,
+ new Function<FieldGetter<?, JobKey>, Class<?>>() {
+ @Override
+ public Class<?> apply(FieldGetter<?, JobKey> input) {
+ return input.getStructClass();
+ }
+ }))
+ .build();
+
+ @VisibleForTesting
+ static final String SHIRO_AUTHORIZATION_FAILURES = "shiro_authorization_failures";
+
+ @VisibleForTesting
+ static final String SHIRO_BAD_REQUESTS = "shiro_bad_requests";
+
+ /**
+ * Return each method in the inheritance hierarchy of method in the order described by
+ * {@link AuthorizingParam}.
+ *
+ * @see org.apache.aurora.scheduler.http.api.security.AuthorizingParam
+ */
+ private static Iterable<Method> getCandidateMethods(final Method method) {
+ return new Iterable<Method>() {
+ @Override
+ public Iterator<Method> iterator() {
+ return new AbstractSequentialIterator<Method>(method) {
+ @Override
+ protected Method computeNext(Method previous) {
+ String name = previous.getName();
+ Class<?>[] parameterTypes = previous.getParameterTypes();
+ Class<?> declaringClass = previous.getDeclaringClass();
+
+ if (declaringClass.isInterface()) {
+ return null;
+ }
+
+ Iterable<Class<?>> searchOrder = ImmutableList.<Class<?>>builder()
+ .addAll(Optional.fromNullable(declaringClass.getSuperclass()).asSet())
+ .addAll(ImmutableList.copyOf(declaringClass.getInterfaces()))
+ .build();
+
+ for (Class<?> klazz : searchOrder) {
+ try {
+ return klazz.getMethod(name, parameterTypes);
+ } catch (NoSuchMethodException ignored) {
+ // Expected.
+ }
+ }
+
+ return null;
+ }
+ };
+ }
+ };
+ }
+
+ private static int annotatedParameterIndex(Method method) {
+ for (Method candidateMethod : getCandidateMethods(method)) {
+ List<Parameter> parameters = Invokable.from(candidateMethod).getParameters();
+ List<Integer> parameterIndicies = Lists.newArrayList();
+ for (int i = 0; i < parameters.size(); i++) {
+ if (parameters.get(i).isAnnotationPresent(AuthorizingParam.class)) {
+ parameterIndicies.add(i);
+ }
+ }
+
+ if (parameterIndicies.size() == 1) {
+ return Iterables.getOnlyElement(parameterIndicies);
+ } else if (parameterIndicies.size() > 1) {
+ throw new UnsupportedOperationException(
+ "Too many parameters annotated with "
+ + AuthorizingParam.class.getName()
+ + " found on method "
+ + method.getName()
+ + " of class "
+ + method.getDeclaringClass().getName());
+ }
+ }
+
+ throw new UnsupportedOperationException(
+ "No parameter annotated with "
+ + AuthorizingParam.class.getName()
+ + " found on method "
+ + method.getName()
+ + " of "
+ + method.getDeclaringClass().getName()
+ + " or any of its superclasses.");
+ }
+
+ private static final CacheLoader<Method, Function<Object[], Optional<JobKey>>> LOADER =
+ new CacheLoader<Method, Function<Object[], Optional<JobKey>>>() {
+ @Override
+ public Function<Object[], Optional<JobKey>> load(Method method) {
+ if (!Response.class.isAssignableFrom(method.getReturnType())) {
+ throw new UnsupportedOperationException(
+ "Method "
+ + method.getName()
+ + " of class "
+ + method.getDeclaringClass().getName()
+ + " does not return "
+ + Response.class.getName());
+ }
+
+ final int index = annotatedParameterIndex(method);
+ ImmutableList<Parameter> parameters = Invokable.from(method).getParameters();
+ Class<?> parameterType = parameters.get(index).getType().getRawType();
+ if (!TBase.class.isAssignableFrom(parameterType)) {
+ throw new UnsupportedOperationException(
+ "Annotated parameter must be a thrift struct.");
+ }
+ @SuppressWarnings("unchecked")
+ final Optional<Function<Object, Optional<JobKey>>> jobKeyGetter =
+ Optional.fromNullable(
+ (Function<Object, Optional<JobKey>>) FIELD_GETTERS_BY_TYPE.get(parameterType));
+ if (!jobKeyGetter.isPresent()) {
+ throw new UnsupportedOperationException(
+ "No "
+ + JobKey.class.getName()
+ + " field getter was supplied for "
+ + parameterType.getName());
+ }
+ return new Function<Object[], Optional<JobKey>>() {
+ @Override
+ public Optional<JobKey> apply(Object[] arguments) {
+ Optional<Object> argument = Optional.fromNullable(arguments[index]);
+ if (argument.isPresent()) {
+ return jobKeyGetter.get().apply(argument.get());
+ } else {
+ return Optional.absent();
+ }
+ }
+ };
+ }
+ };
+
+ private static final Joiner COLON_JOINER = Joiner.on(":");
+
+ private final LoadingCache<Method, Function<Object[], Optional<JobKey>>> authorizingParamGetters =
+ CacheBuilder.<Method, Function<Object[], Optional<JobKey>>>newBuilder().build(LOADER);
+
+ private final String permissionPrefix;
+ private volatile boolean initialized;
+
+ private Provider<Subject> subjectProvider;
+ private AtomicLong authorizationFailures;
+ private AtomicLong badRequests;
+
+ ShiroAuthorizingParamInterceptor(String permissionPrefix) {
+ this.permissionPrefix = MorePreconditions.checkNotBlank(permissionPrefix);
+ }
+
+ @Inject
+ void initialize(Provider<Subject> newSubjectProvider, StatsProvider statsProvider) {
+ checkState(!initialized);
+
+ this.subjectProvider = Objects.requireNonNull(newSubjectProvider);
+ authorizationFailures = statsProvider.makeCounter(SHIRO_AUTHORIZATION_FAILURES);
+ badRequests = statsProvider.makeCounter(SHIRO_BAD_REQUESTS);
+
+ initialized = true;
+ }
+
+ @VisibleForTesting
+ Permission makeWildcardPermission(String methodName) {
+ return new WildcardPermission(
+ COLON_JOINER.join(permissionPrefix, methodName));
+ }
+
+ @VisibleForTesting
+ Permission makeTargetPermission(String methodName, IJobKey jobKey) {
+ return new WildcardPermission(
+ COLON_JOINER.join(
+ permissionPrefix,
+ methodName,
+ jobKey.getRole(),
+ jobKey.getEnvironment(),
+ jobKey.getName()));
+ }
+
+ @Override
+ public Object invoke(MethodInvocation invocation) throws Throwable {
+ checkState(initialized);
+
+ Method method = invocation.getMethod();
+ // Short-circuit request processing restrictions if the caller would be allowed to
+ // operate on every possible job key. This allows a broadly-scoped TaskQuery.
+ Subject subject = subjectProvider.get();
+ if (subject.isPermitted(makeWildcardPermission(method.getName()))) {
+ return invocation.proceed();
+ }
+
+ Optional<IJobKey> jobKey = authorizingParamGetters
+ .getUnchecked(invocation.getMethod())
+ .apply(invocation.getArguments())
+ .transform(IJobKey.FROM_BUILDER);
+ if (jobKey.isPresent() && JobKeys.isValid(jobKey.get())) {
+ Permission targetPermission = makeTargetPermission(method.getName(), jobKey.get());
+ if (subject.isPermitted(targetPermission)) {
+ return invocation.proceed();
+ } else {
+ authorizationFailures.incrementAndGet();
+ return Responses.addMessage(
+ Responses.empty(),
+ ResponseCode.AUTH_FAILED,
+ "Subject " + subject + " is not permitted to " + targetPermission + ".");
+ }
+ } else {
+ badRequests.incrementAndGet();
+ return Responses.addMessage(
+ Responses.empty(),
+ ResponseCode.INVALID_REQUEST,
+ "Missing or invalid job key from request.");
+ }
+ }
+
+ @VisibleForTesting
+ LoadingCache<Method, Function<Object[], Optional<JobKey>>> getAuthorizingParamGetters() {
+ return authorizingParamGetters;
+ }
+}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroThriftInterceptor.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroThriftInterceptor.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroThriftInterceptor.java
deleted file mode 100644
index 4e341e0..0000000
--- a/src/main/java/org/apache/aurora/scheduler/http/api/security/ShiroThriftInterceptor.java
+++ /dev/null
@@ -1,101 +0,0 @@
-/**
- * Licensed 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.apache.aurora.scheduler.http.api.security;
-
-import java.util.concurrent.atomic.AtomicLong;
-import java.util.logging.Logger;
-
-import javax.inject.Provider;
-
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.Joiner;
-import com.google.inject.Inject;
-import com.twitter.common.base.MorePreconditions;
-import com.twitter.common.stats.StatsProvider;
-
-import org.aopalliance.intercept.MethodInterceptor;
-import org.aopalliance.intercept.MethodInvocation;
-import org.apache.aurora.gen.ResponseCode;
-import org.apache.aurora.scheduler.thrift.Responses;
-import org.apache.shiro.authz.Permission;
-import org.apache.shiro.authz.UnauthenticatedException;
-import org.apache.shiro.authz.permission.WildcardPermission;
-import org.apache.shiro.subject.Subject;
-
-import static java.util.Objects.requireNonNull;
-
-import static com.google.common.base.Preconditions.checkState;
-
-/**
- * Prevents invocation of intercepted methods if the current {@link Subject} is not authenticated
- * or the current subject does not have the permission defined a prefix the name of the method
- * attempting to be invoked. The {@link org.apache.shiro.authz.Permission} checked is a
- * {@link org.apache.shiro.authz.permission.WildcardPermission} constructed from the prefix and
- * the name of the method being invoked, for example if the prefix is {@code api} and the method
- * is {@code snapshot} the current subject must have the permission {@code api:snapshot} permission.
- */
-class ShiroThriftInterceptor implements MethodInterceptor {
- private static final Logger LOG = Logger.getLogger(ShiroThriftInterceptor.class.getName());
- private static final Joiner PERMISSION_JOINER = Joiner.on(":");
-
- @VisibleForTesting
- static final String SHIRO_AUTHORIZATION_FAILURES = "shiro_authorization_failures";
-
- private final String permissionPrefix;
-
- private volatile boolean initialized;
-
- private Provider<Subject> subjectProvider;
- private AtomicLong shiroAuthorizationFailures;
-
- @Inject
- void initialize(Provider<Subject> newSubjectProvider, StatsProvider statsProvider) {
- checkState(!initialized);
-
- this.subjectProvider = requireNonNull(newSubjectProvider);
- shiroAuthorizationFailures = statsProvider.makeCounter(SHIRO_AUTHORIZATION_FAILURES);
-
- initialized = true;
- }
-
- public ShiroThriftInterceptor(String permissionPrefix) {
- this.permissionPrefix = MorePreconditions.checkNotBlank(permissionPrefix);
- }
-
- @Override
- public Object invoke(MethodInvocation invocation) throws Throwable {
- checkState(initialized);
-
- Subject subject = subjectProvider.get();
- if (!subject.isAuthenticated()) {
- // This is a special exception that will signal the BasicHttpAuthenticationFilter to send
- // a 401 with a challenge. This is necessary at this layer since we only apply this
- // interceptor to methods that require authentication.
- throw new UnauthenticatedException();
- }
-
- Permission checkedPermission = new WildcardPermission(
- PERMISSION_JOINER.join(permissionPrefix, invocation.getMethod().getName()));
- if (subject.isPermitted(checkedPermission)) {
- return invocation.proceed();
- } else {
- shiroAuthorizationFailures.incrementAndGet();
- String responseMessage =
- "Subject " + subject.getPrincipal() + " lacks permission " + checkedPermission;
- LOG.warning(responseMessage);
- // TODO(ksweeney): 403 FORBIDDEN would be a more accurate translation of this response code.
- return Responses.addMessage(Responses.empty(), ResponseCode.AUTH_FAILED, responseMessage);
- }
- }
-}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/http/api/security/ThriftFieldGetter.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/http/api/security/ThriftFieldGetter.java b/src/main/java/org/apache/aurora/scheduler/http/api/security/ThriftFieldGetter.java
new file mode 100644
index 0000000..2044b79
--- /dev/null
+++ b/src/main/java/org/apache/aurora/scheduler/http/api/security/ThriftFieldGetter.java
@@ -0,0 +1,66 @@
+/**
+ * Licensed 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.apache.aurora.scheduler.http.api.security;
+
+import com.google.common.base.Optional;
+
+import org.apache.aurora.scheduler.http.api.security.FieldGetter.AbstractFieldGetter;
+import org.apache.thrift.TBase;
+import org.apache.thrift.TFieldIdEnum;
+import org.apache.thrift.meta_data.FieldMetaData;
+import org.apache.thrift.meta_data.FieldValueMetaData;
+import org.apache.thrift.meta_data.StructMetaData;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+/**
+ * Retrieves an optional struct-type field from a struct.
+ */
+class ThriftFieldGetter<T extends TBase<T, F>, F extends TFieldIdEnum, V extends TBase<V, ?>>
+ extends AbstractFieldGetter<T, V> {
+
+ private final F fieldId;
+
+ ThriftFieldGetter(Class<T> structClass, F fieldId, Class<V> valueClass) {
+ super(structClass, valueClass);
+
+ FieldValueMetaData fieldValueMetaData = FieldMetaData
+ .getStructMetaDataMap(structClass)
+ .get(fieldId)
+ .valueMetaData;
+
+ checkArgument(fieldValueMetaData instanceof StructMetaData);
+ StructMetaData structMetaData = (StructMetaData) fieldValueMetaData;
+ checkArgument(
+ valueClass.equals(structMetaData.structClass),
+ "Value class "
+ + valueClass.getName()
+ + " does not match field metadata for "
+ + fieldId
+ + " (expected " + structMetaData.structClass
+ + ").");
+
+ this.fieldId = fieldId;
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public Optional<V> apply(T input) {
+ if (input.isSet(fieldId)) {
+ return Optional.of((V) input.getFieldValue(fieldId));
+ } else {
+ return Optional.absent();
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/main/java/org/apache/aurora/scheduler/thrift/aop/AopModule.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/apache/aurora/scheduler/thrift/aop/AopModule.java b/src/main/java/org/apache/aurora/scheduler/thrift/aop/AopModule.java
index bdd2185..3490394 100644
--- a/src/main/java/org/apache/aurora/scheduler/thrift/aop/AopModule.java
+++ b/src/main/java/org/apache/aurora/scheduler/thrift/aop/AopModule.java
@@ -59,7 +59,7 @@ public class AopModule extends AbstractModule {
private static final Arg<Boolean> ENABLE_JOB_CREATION = Arg.create(true);
private static final Matcher<? super Class<?>> THRIFT_IFACE_MATCHER =
- Matchers.subclassesOf(AuroraAdmin.Iface.class)
+ Matchers.subclassesOf(AnnotatedAuroraAdmin.class)
.and(Matchers.annotatedWith(DecoratedThrift.class));
private final Map<String, Boolean> toggledMethods;
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/test/java/org/apache/aurora/scheduler/http/api/ApiBetaTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/ApiBetaTest.java b/src/test/java/org/apache/aurora/scheduler/http/api/ApiBetaTest.java
index cafd10f..08e1284 100644
--- a/src/test/java/org/apache/aurora/scheduler/http/api/ApiBetaTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/http/api/ApiBetaTest.java
@@ -30,7 +30,6 @@ import com.sun.jersey.api.client.config.ClientConfig;
import com.sun.jersey.api.client.config.DefaultClientConfig;
import org.apache.aurora.gen.AssignedTask;
-import org.apache.aurora.gen.AuroraAdmin;
import org.apache.aurora.gen.Constraint;
import org.apache.aurora.gen.CronCollisionPolicy;
import org.apache.aurora.gen.ExecutorConfig;
@@ -55,6 +54,7 @@ import org.apache.aurora.gen.TaskQuery;
import org.apache.aurora.scheduler.http.JettyServerModuleTest;
import org.apache.aurora.scheduler.storage.entities.IJobConfiguration;
import org.apache.aurora.scheduler.storage.entities.ITaskConfig;
+import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin;
import org.junit.Before;
import org.junit.Test;
@@ -64,11 +64,11 @@ import static org.easymock.EasyMock.expect;
import static org.junit.Assert.assertEquals;
public class ApiBetaTest extends JettyServerModuleTest {
- private AuroraAdmin.Iface thrift;
+ private AnnotatedAuroraAdmin thrift;
@Before
public void setUp() {
- thrift = createMock(AuroraAdmin.Iface.class);
+ thrift = createMock(AnnotatedAuroraAdmin.class);
}
@Override
@@ -78,7 +78,7 @@ public class ApiBetaTest extends JettyServerModuleTest {
new AbstractModule() {
@Override
protected void configure() {
- bind(AuroraAdmin.Iface.class).toInstance(thrift);
+ bind(AnnotatedAuroraAdmin.class).toInstance(thrift);
}
}
);
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/test/java/org/apache/aurora/scheduler/http/api/ApiIT.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/ApiIT.java b/src/test/java/org/apache/aurora/scheduler/http/api/ApiIT.java
index ed284f4..aa3a85a 100644
--- a/src/test/java/org/apache/aurora/scheduler/http/api/ApiIT.java
+++ b/src/test/java/org/apache/aurora/scheduler/http/api/ApiIT.java
@@ -20,9 +20,9 @@ import com.google.inject.Module;
import com.google.inject.util.Modules;
import com.sun.jersey.api.client.ClientResponse;
-import org.apache.aurora.gen.AuroraAdmin;
import org.apache.aurora.gen.Response;
import org.apache.aurora.scheduler.http.JettyServerModuleTest;
+import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin;
import org.junit.Before;
import org.junit.Test;
@@ -30,11 +30,11 @@ import static org.easymock.EasyMock.expect;
import static org.junit.Assert.assertEquals;
public class ApiIT extends JettyServerModuleTest {
- private AuroraAdmin.Iface thrift;
+ private AnnotatedAuroraAdmin thrift;
@Before
public void setUp() {
- thrift = createMock(AuroraAdmin.Iface.class);
+ thrift = createMock(AnnotatedAuroraAdmin.class);
}
@Override
@@ -44,7 +44,7 @@ public class ApiIT extends JettyServerModuleTest {
new AbstractModule() {
@Override
protected void configure() {
- bind(AuroraAdmin.Iface.class).toInstance(thrift);
+ bind(AnnotatedAuroraAdmin.class).toInstance(thrift);
}
});
}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/test/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityIT.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityIT.java b/src/test/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityIT.java
index 76cb691..45a23fd 100644
--- a/src/test/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityIT.java
+++ b/src/test/java/org/apache/aurora/scheduler/http/api/security/ApiSecurityIT.java
@@ -17,6 +17,7 @@ import java.io.IOException;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
+import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.testing.TearDown;
@@ -29,8 +30,12 @@ import org.apache.aurora.gen.AuroraAdmin;
import org.apache.aurora.gen.Lock;
import org.apache.aurora.gen.Response;
import org.apache.aurora.gen.ResponseCode;
+import org.apache.aurora.gen.TaskQuery;
+import org.apache.aurora.scheduler.base.JobKeys;
+import org.apache.aurora.scheduler.base.Query;
import org.apache.aurora.scheduler.http.JettyServerModuleTest;
import org.apache.aurora.scheduler.http.api.ApiModule;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin;
import org.apache.aurora.scheduler.thrift.aop.MockDecoratedThrift;
import org.apache.http.auth.AuthScope;
@@ -67,6 +72,8 @@ public class ApiSecurityIT extends JettyServerModuleTest {
new UsernamePasswordCredentials("ksweeney", "12345");
private static final UsernamePasswordCredentials BACKUP_SERVICE =
new UsernamePasswordCredentials("backupsvc", "s3cret!!1");
+ private static final UsernamePasswordCredentials DEPLOY_SERVICE =
+ new UsernamePasswordCredentials("deploysvc", "0_0-x_0");
private static final UsernamePasswordCredentials INCORRECT =
new UsernamePasswordCredentials("root", "wrong");
@@ -79,24 +86,45 @@ public class ApiSecurityIT extends JettyServerModuleTest {
private static final Set<Credentials> VALID_CREDENTIALS =
ImmutableSet.<Credentials>of(ROOT, WFARNER, UNPRIVILEGED, BACKUP_SERVICE);
+ private static final IJobKey ADS_STAGING_JOB = JobKeys.from("ads", "staging", "job");
+
private Ini ini;
private AnnotatedAuroraAdmin auroraAdmin;
private StatsProvider statsProvider;
+ private static final Joiner COMMA_JOINER = Joiner.on(", ");
+ private static final String ADMIN_ROLE = "admin";
+ private static final String ENG_ROLE = "eng";
+ private static final String BACKUP_ROLE = "backup";
+ private static final String DEPLOY_ROLE = "deploy";
+
@Before
public void setUp() {
ini = new Ini();
Ini.Section users = ini.addSection(IniRealm.USERS_SECTION_NAME);
- users.put(ROOT.getUserName(), ROOT.getPassword() + ", admin");
- users.put(WFARNER.getUserName(), WFARNER.getPassword() + ", eng");
+ users.put(ROOT.getUserName(), COMMA_JOINER.join(ROOT.getPassword(), ADMIN_ROLE));
+ users.put(WFARNER.getUserName(), COMMA_JOINER.join(WFARNER.getPassword(), ENG_ROLE));
users.put(UNPRIVILEGED.getUserName(), UNPRIVILEGED.getPassword());
- users.put(BACKUP_SERVICE.getUserName(), BACKUP_SERVICE.getPassword() + ", backupsvc");
+ users.put(
+ BACKUP_SERVICE.getUserName(),
+ COMMA_JOINER.join(BACKUP_SERVICE.getPassword(), BACKUP_ROLE));
+ users.put(
+ DEPLOY_SERVICE.getUserName(),
+ COMMA_JOINER.join(DEPLOY_SERVICE.getPassword(), DEPLOY_ROLE));
Ini.Section roles = ini.addSection(IniRealm.ROLES_SECTION_NAME);
- roles.put("admin", "*");
- roles.put("eng", "thrift.AuroraSchedulerManager:*");
- roles.put("backupsvc", "thrift.AuroraAdmin:listBackups");
+ roles.put(ADMIN_ROLE, "*");
+ roles.put(ENG_ROLE, "thrift.AuroraSchedulerManager:*");
+ roles.put(BACKUP_ROLE, "thrift.AuroraAdmin:listBackups");
+ roles.put(
+ DEPLOY_ROLE,
+ "thrift.AuroraSchedulerManager:killTasks:"
+ + ADS_STAGING_JOB.getRole()
+ + ":"
+ + ADS_STAGING_JOB.getEnvironment()
+ + ":"
+ + ADS_STAGING_JOB.getName());
auroraAdmin = createMock(AnnotatedAuroraAdmin.class);
statsProvider = createMock(StatsProvider.class);
@@ -173,6 +201,10 @@ public class ApiSecurityIT extends JettyServerModuleTest {
expect(auroraAdmin.killTasks(null, new Lock().setMessage("1"), null)).andReturn(OK);
expect(auroraAdmin.killTasks(null, new Lock().setMessage("2"), null)).andReturn(OK);
+ TaskQuery jobScopedQuery = Query.jobScoped(JobKeys.from("role", "env", "name")).get();
+ TaskQuery adsScopedQuery = Query.jobScoped(ADS_STAGING_JOB).get();
+ expect(auroraAdmin.killTasks(adsScopedQuery, null, null)).andReturn(OK);
+
replayAndStart();
assertEquals(OK,
@@ -180,11 +212,29 @@ public class ApiSecurityIT extends JettyServerModuleTest {
assertEquals(OK,
getAuthenticatedClient(ROOT).killTasks(null, new Lock().setMessage("2"), null));
assertEquals(
- ResponseCode.AUTH_FAILED,
+ ResponseCode.INVALID_REQUEST,
getAuthenticatedClient(UNPRIVILEGED).killTasks(null, null, null).getResponseCode());
assertEquals(
ResponseCode.AUTH_FAILED,
+ getAuthenticatedClient(UNPRIVILEGED)
+ .killTasks(jobScopedQuery, null, null)
+ .getResponseCode());
+ assertEquals(
+ ResponseCode.INVALID_REQUEST,
getAuthenticatedClient(BACKUP_SERVICE).killTasks(null, null, null).getResponseCode());
+ assertEquals(
+ ResponseCode.AUTH_FAILED,
+ getAuthenticatedClient(BACKUP_SERVICE)
+ .killTasks(jobScopedQuery, null, null)
+ .getResponseCode());
+ assertEquals(
+ ResponseCode.AUTH_FAILED,
+ getAuthenticatedClient(DEPLOY_SERVICE)
+ .killTasks(jobScopedQuery, null, null)
+ .getResponseCode());
+ assertEquals(
+ OK,
+ getAuthenticatedClient(DEPLOY_SERVICE).killTasks(adsScopedQuery, null, null));
assertKillTasksFails(getUnauthenticatedClient());
assertKillTasksFails(getAuthenticatedClient(INCORRECT));
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthenticatingThriftInterceptorTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthenticatingThriftInterceptorTest.java b/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthenticatingThriftInterceptorTest.java
new file mode 100644
index 0000000..568cd8f
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthenticatingThriftInterceptorTest.java
@@ -0,0 +1,66 @@
+/**
+ * Licensed 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.apache.aurora.scheduler.http.api.security;
+
+import com.google.inject.util.Providers;
+import com.twitter.common.testing.easymock.EasyMockTest;
+
+import org.aopalliance.intercept.MethodInvocation;
+import org.apache.aurora.gen.Response;
+import org.apache.aurora.scheduler.thrift.Responses;
+import org.apache.shiro.authz.UnauthenticatedException;
+import org.apache.shiro.subject.Subject;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.easymock.EasyMock.expect;
+import static org.junit.Assert.assertSame;
+
+public class ShiroAuthenticatingThriftInterceptorTest extends EasyMockTest {
+ private ShiroAuthenticatingThriftInterceptor interceptor;
+ private Subject subject;
+ private MethodInvocation methodInvocation;
+
+ @Before
+ public void setUp() throws Exception {
+ interceptor = new ShiroAuthenticatingThriftInterceptor();
+ subject = createMock(Subject.class);
+ methodInvocation = createMock(MethodInvocation.class);
+ }
+
+ private void replayAndInitialize() {
+ control.replay();
+ interceptor.initialize(Providers.of(subject));
+ }
+
+ @Test(expected = UnauthenticatedException.class)
+ public void testInvokeNotAuthenticated() throws Throwable {
+ expect(subject.isAuthenticated()).andReturn(false);
+
+ replayAndInitialize();
+
+ interceptor.invoke(methodInvocation);
+ }
+
+ @Test
+ public void testInvokeAuthenticated() throws Throwable {
+ Response response = Responses.ok();
+ expect(subject.isAuthenticated()).andReturn(true);
+ expect(methodInvocation.proceed()).andReturn(response);
+
+ replayAndInitialize();
+
+ assertSame(response, interceptor.invoke(methodInvocation));
+ }
+}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingInterceptorTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingInterceptorTest.java b/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingInterceptorTest.java
new file mode 100644
index 0000000..16f2da5
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingInterceptorTest.java
@@ -0,0 +1,94 @@
+/**
+ * Licensed 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.apache.aurora.scheduler.http.api.security;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicLong;
+
+import com.google.inject.util.Providers;
+import com.twitter.common.stats.StatsProvider;
+import com.twitter.common.testing.easymock.EasyMockTest;
+
+import org.aopalliance.intercept.MethodInvocation;
+import org.apache.aurora.gen.AuroraAdmin;
+import org.apache.aurora.gen.Response;
+import org.apache.aurora.gen.ResponseCode;
+import org.apache.aurora.gen.SessionKey;
+import org.apache.aurora.scheduler.thrift.Responses;
+import org.apache.shiro.authz.permission.WildcardPermission;
+import org.apache.shiro.subject.Subject;
+import org.easymock.IExpectationSetters;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.apache.aurora.scheduler.http.api.security.ShiroAuthorizingInterceptor.SHIRO_AUTHORIZATION_FAILURES;
+import static org.easymock.EasyMock.expect;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertSame;
+
+public class ShiroAuthorizingInterceptorTest extends EasyMockTest {
+ private static final String PERMISSION_PREFIX = "adminRPC";
+
+ private Subject subject;
+ private StatsProvider statsProvider;
+ private MethodInvocation methodInvocation;
+ private Method interceptedMethod;
+
+ private ShiroAuthorizingInterceptor interceptor;
+
+ @Before
+ public void setUp() throws NoSuchMethodException {
+ interceptor = new ShiroAuthorizingInterceptor(PERMISSION_PREFIX);
+ subject = createMock(Subject.class);
+ statsProvider = createMock(StatsProvider.class);
+ methodInvocation = createMock(MethodInvocation.class);
+ interceptedMethod = AuroraAdmin.Iface.class.getMethod("snapshot", SessionKey.class);
+ expect(statsProvider.makeCounter(SHIRO_AUTHORIZATION_FAILURES)).andReturn(new AtomicLong());
+ }
+
+ private void replayAndInitialize() {
+ control.replay();
+ interceptor.initialize(Providers.of(subject), statsProvider);
+ }
+
+ private IExpectationSetters<Boolean> expectSubjectPermitted() {
+ return expect(subject.isPermitted(
+ new WildcardPermission(PERMISSION_PREFIX + ":" + interceptedMethod.getName())));
+ }
+
+ @Test
+ public void testAuthorized() throws Throwable {
+ Response response = Responses.ok();
+ expect(methodInvocation.getMethod()).andReturn(interceptedMethod);
+ expectSubjectPermitted().andReturn(true);
+ expect(methodInvocation.proceed()).andReturn(response);
+
+ replayAndInitialize();
+
+ assertSame(response, interceptor.invoke(methodInvocation));
+ }
+
+ @Test
+ public void testNotAuthorized() throws Throwable {
+ expect(methodInvocation.getMethod()).andReturn(interceptedMethod);
+ expectSubjectPermitted().andReturn(false);
+ expect(subject.getPrincipal()).andReturn("ksweeney");
+
+ replayAndInitialize();
+
+ assertEquals(
+ ResponseCode.AUTH_FAILED,
+ ((Response) interceptor.invoke(methodInvocation)).getResponseCode());
+ }
+}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingParamInterceptorTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingParamInterceptorTest.java b/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingParamInterceptorTest.java
new file mode 100644
index 0000000..781cf5a
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroAuthorizingParamInterceptorTest.java
@@ -0,0 +1,189 @@
+/**
+ * Licensed 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.apache.aurora.scheduler.http.api.security;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.atomic.AtomicLong;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.matcher.Matchers;
+import com.twitter.common.stats.StatsProvider;
+import com.twitter.common.testing.easymock.EasyMockTest;
+
+import org.apache.aurora.gen.JobConfiguration;
+import org.apache.aurora.gen.Response;
+import org.apache.aurora.gen.ResponseCode;
+import org.apache.aurora.gen.TaskQuery;
+import org.apache.aurora.scheduler.base.JobKeys;
+import org.apache.aurora.scheduler.base.Query;
+import org.apache.aurora.scheduler.storage.entities.IJobKey;
+import org.apache.aurora.scheduler.thrift.Responses;
+import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin;
+import org.apache.aurora.scheduler.thrift.aop.MockDecoratedThrift;
+import org.apache.shiro.subject.Subject;
+import org.apache.thrift.TException;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.apache.aurora.scheduler.http.api.security.ShiroAuthorizingParamInterceptor.QUERY_TO_JOB_KEY;
+import static org.apache.aurora.scheduler.http.api.security.ShiroAuthorizingParamInterceptor.SHIRO_AUTHORIZATION_FAILURES;
+import static org.apache.aurora.scheduler.http.api.security.ShiroAuthorizingParamInterceptor.SHIRO_BAD_REQUESTS;
+import static org.easymock.EasyMock.expect;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+public class ShiroAuthorizingParamInterceptorTest extends EasyMockTest {
+ private static final String PERMISSION_PREFIX = "testperm";
+
+ private ShiroAuthorizingParamInterceptor interceptor;
+
+ private Subject subject;
+ private AnnotatedAuroraAdmin thrift;
+ private StatsProvider statsProvider;
+
+ private AnnotatedAuroraAdmin decoratedThrift;
+
+ private static final IJobKey JOB_KEY = JobKeys.from("role", "env", "name");
+
+ @Before
+ public void setUp() {
+ interceptor = new ShiroAuthorizingParamInterceptor(PERMISSION_PREFIX);
+ subject = createMock(Subject.class);
+ statsProvider = createMock(StatsProvider.class);
+ thrift = createMock(AnnotatedAuroraAdmin.class);
+ };
+
+ private void replayAndInitialize() {
+ expect(statsProvider.makeCounter(SHIRO_AUTHORIZATION_FAILURES))
+ .andReturn(new AtomicLong());
+ expect(statsProvider.makeCounter(SHIRO_BAD_REQUESTS))
+ .andReturn(new AtomicLong());
+ control.replay();
+ decoratedThrift = Guice
+ .createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(Subject.class).toInstance(subject);
+ MockDecoratedThrift.bindForwardedMock(binder(), thrift);
+ bindInterceptor(
+ Matchers.subclassesOf(AnnotatedAuroraAdmin.class),
+ ApiSecurityModule.AURORA_SCHEDULER_MANAGER_SERVICE,
+ interceptor);
+ bind(StatsProvider.class).toInstance(statsProvider);
+ requestInjection(interceptor);
+ }
+ }).getInstance(AnnotatedAuroraAdmin.class);
+ }
+
+ @Test
+ public void testHandlesAllDecoratedParamTypes() {
+ control.replay();
+
+ for (Method method : AnnotatedAuroraAdmin.class.getMethods()) {
+ if (ApiSecurityModule.AURORA_SCHEDULER_MANAGER_SERVICE.matches(method)) {
+ interceptor.getAuthorizingParamGetters().getUnchecked(method);
+ }
+ }
+ }
+
+ @Test
+ public void testCreateJobWithScopedPermission() throws TException {
+ JobConfiguration jobConfiguration = new JobConfiguration().setKey(JOB_KEY.newBuilder());
+ Response response = Responses.ok();
+
+ expect(subject.isPermitted(interceptor.makeWildcardPermission("createJob")))
+ .andReturn(false);
+ expect(subject
+ .isPermitted(interceptor.makeTargetPermission("createJob", JOB_KEY)))
+ .andReturn(true);
+ expect(thrift.createJob(jobConfiguration, null, null))
+ .andReturn(response);
+
+ replayAndInitialize();
+
+ assertSame(response, decoratedThrift.createJob(jobConfiguration, null, null));
+ }
+
+ @Test
+ public void testKillTasksWithWildcardPermission() throws TException {
+ TaskQuery taskQuery = Query.unscoped().get();
+ Response response = Responses.ok();
+
+ expect(subject.isPermitted(interceptor.makeWildcardPermission("killTasks")))
+ .andReturn(true);
+ expect(thrift.killTasks(taskQuery, null, null))
+ .andReturn(response);
+
+ replayAndInitialize();
+
+ assertSame(response, decoratedThrift.killTasks(taskQuery, null, null));
+ }
+
+ @Test
+ public void testKillTasksWithoutWildcardPermission() throws TException {
+ TaskQuery taskQuery = Query.unscoped().get();
+
+ expect(subject.isPermitted(interceptor.makeWildcardPermission("killTasks")))
+ .andReturn(false);
+
+ replayAndInitialize();
+
+ assertEquals(
+ ResponseCode.INVALID_REQUEST,
+ decoratedThrift.killTasks(taskQuery, null, null).getResponseCode());
+ }
+
+ @Test
+ public void testExtractTaskQuerySingleJobKey() {
+ replayAndInitialize();
+
+ assertEquals(
+ JOB_KEY.newBuilder(),
+ QUERY_TO_JOB_KEY
+ .apply(new TaskQuery()
+ .setRole(JOB_KEY.getRole())
+ .setEnvironment(JOB_KEY.getEnvironment())
+ .setJobName(JOB_KEY.getName()))
+ .orNull());
+
+ assertEquals(
+ JOB_KEY.newBuilder(),
+ QUERY_TO_JOB_KEY.apply(new TaskQuery().setJobKeys(ImmutableSet.of(JOB_KEY.newBuilder())))
+ .orNull());
+ }
+
+ @Test
+ public void testExtractTaskQueryBroadlyScoped() {
+ control.replay();
+
+ assertNull(QUERY_TO_JOB_KEY.apply(new TaskQuery().setRole("role")).orNull());
+ }
+
+ @Test
+ public void testExtractTaskQueryMultiScoped() {
+ // TODO(ksweeney): Reconsider behavior here, this is possibly too restrictive as it
+ // will mean that only admins are authorized to operate on multiple jobs at once regardless
+ // of whether they share a common role.
+ control.replay();
+
+ assertNull(QUERY_TO_JOB_KEY
+ .apply(
+ new TaskQuery().setJobKeys(
+ ImmutableSet.of(JOB_KEY.newBuilder(), JOB_KEY.newBuilder().setName("other"))))
+ .orNull());
+ }
+}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroThriftInterceptorTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroThriftInterceptorTest.java b/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroThriftInterceptorTest.java
deleted file mode 100644
index d2ba273..0000000
--- a/src/test/java/org/apache/aurora/scheduler/http/api/security/ShiroThriftInterceptorTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-/**
- * Licensed 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.apache.aurora.scheduler.http.api.security;
-
-import java.lang.reflect.Method;
-import java.util.concurrent.atomic.AtomicLong;
-
-import com.google.inject.util.Providers;
-import com.twitter.common.stats.StatsProvider;
-import com.twitter.common.testing.easymock.EasyMockTest;
-
-import org.aopalliance.intercept.MethodInvocation;
-import org.apache.aurora.gen.AuroraAdmin;
-import org.apache.aurora.gen.Response;
-import org.apache.aurora.gen.ResponseCode;
-import org.apache.aurora.gen.SessionKey;
-import org.apache.aurora.scheduler.thrift.Responses;
-import org.apache.shiro.authz.UnauthenticatedException;
-import org.apache.shiro.authz.permission.WildcardPermission;
-import org.apache.shiro.subject.Subject;
-import org.easymock.IExpectationSetters;
-import org.junit.Before;
-import org.junit.Test;
-
-import static org.easymock.EasyMock.expect;
-import static org.junit.Assert.assertEquals;
-
-public class ShiroThriftInterceptorTest extends EasyMockTest {
- private static final String PERMISSION = "test";
- private static final String PRINCIPAL = "test-user";
-
- private ShiroThriftInterceptor interceptor;
- private Subject subject;
- private StatsProvider statsProvider;
- private AtomicLong shiroAuthorizationFailures;
- private MethodInvocation methodInvocation;
- private Method interceptedMethod;
-
- @Before
- public void setUp() throws Exception {
- interceptor = new ShiroThriftInterceptor(PERMISSION);
- subject = createMock(Subject.class);
- statsProvider = createMock(StatsProvider.class);
- shiroAuthorizationFailures = new AtomicLong();
- expect(statsProvider.makeCounter(ShiroThriftInterceptor.SHIRO_AUTHORIZATION_FAILURES))
- .andReturn(shiroAuthorizationFailures);
- methodInvocation = createMock(MethodInvocation.class);
- interceptedMethod = AuroraAdmin.Iface.class.getMethod("snapshot", SessionKey.class);
- }
-
- private void replayAndInitialize() {
- control.replay();
- interceptor.initialize(Providers.of(subject), statsProvider);
- }
-
- @Test(expected = UnauthenticatedException.class)
- public void testInvokeNotAuthenticated() throws Throwable {
- expect(subject.isAuthenticated()).andReturn(false);
-
- replayAndInitialize();
-
- interceptor.invoke(methodInvocation);
- }
-
- private IExpectationSetters<Boolean> expectSubjectPermitted() {
- return expect(subject.isPermitted(
- new WildcardPermission(PERMISSION + ":" + interceptedMethod.getName())));
- }
-
- @Test
- public void testInvokeNotAuthorized() throws Throwable {
- expect(subject.isAuthenticated()).andReturn(true);
- expect(methodInvocation.getMethod()).andReturn(interceptedMethod);
- expectSubjectPermitted().andReturn(false);
- expect(subject.getPrincipal()).andReturn(PRINCIPAL);
-
- replayAndInitialize();
-
- assertEquals(
- ResponseCode.AUTH_FAILED,
- ((Response) interceptor.invoke(methodInvocation)).getResponseCode());
- }
-
- @Test
- public void testInvokeAuthorized() throws Throwable {
- expect(subject.isAuthenticated()).andReturn(true);
- expect(methodInvocation.getMethod()).andReturn(interceptedMethod);
- expectSubjectPermitted().andReturn(true);
- expect(methodInvocation.proceed()).andReturn(Responses.ok());
-
- replayAndInitialize();
-
- assertEquals(Responses.ok(), interceptor.invoke(methodInvocation));
- }
-}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/test/java/org/apache/aurora/scheduler/http/api/security/ThriftFieldGetterTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/http/api/security/ThriftFieldGetterTest.java b/src/test/java/org/apache/aurora/scheduler/http/api/security/ThriftFieldGetterTest.java
new file mode 100644
index 0000000..b0a8d75
--- /dev/null
+++ b/src/test/java/org/apache/aurora/scheduler/http/api/security/ThriftFieldGetterTest.java
@@ -0,0 +1,46 @@
+/**
+ * Licensed 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.apache.aurora.scheduler.http.api.security;
+
+import org.apache.aurora.gen.JobConfiguration;
+import org.apache.aurora.gen.JobConfiguration._Fields;
+import org.apache.aurora.gen.JobKey;
+import org.apache.aurora.gen.TaskConfig;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertSame;
+
+public class ThriftFieldGetterTest {
+ @Test
+ public void testStructFieldGetter() {
+ JobKey jobKey = new JobKey();
+ FieldGetter<JobConfiguration, JobKey> fieldGetter =
+ new ThriftFieldGetter<>(JobConfiguration.class, _Fields.KEY, JobKey.class);
+
+ JobConfiguration jobConfiguration = new JobConfiguration().setKey(jobKey);
+
+ assertSame(jobKey, fieldGetter.apply(jobConfiguration).orNull());
+ }
+
+ @Test
+ public void testStructFieldGetterUnsetField() {
+ FieldGetter<JobConfiguration, TaskConfig> fieldGetter =
+ new ThriftFieldGetter<>(JobConfiguration.class, _Fields.TASK_CONFIG, TaskConfig.class);
+
+ JobConfiguration jobConfiguration = new JobConfiguration().setInstanceCount(5);
+
+ assertNull(fieldGetter.apply(jobConfiguration).orNull());
+ }
+}
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/test/java/org/apache/aurora/scheduler/thrift/ThriftIT.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/thrift/ThriftIT.java b/src/test/java/org/apache/aurora/scheduler/thrift/ThriftIT.java
index 1f24e7d..2a2b499 100644
--- a/src/test/java/org/apache/aurora/scheduler/thrift/ThriftIT.java
+++ b/src/test/java/org/apache/aurora/scheduler/thrift/ThriftIT.java
@@ -47,6 +47,7 @@ import org.apache.aurora.scheduler.storage.backup.StorageBackup;
import org.apache.aurora.scheduler.storage.entities.IResourceAggregate;
import org.apache.aurora.scheduler.storage.entities.IServerInfo;
import org.apache.aurora.scheduler.storage.testing.StorageTestUtil;
+import org.apache.aurora.scheduler.thrift.aop.AnnotatedAuroraAdmin;
import org.apache.aurora.scheduler.thrift.auth.ThriftAuthModule;
import org.apache.aurora.scheduler.updater.JobUpdateController;
import org.junit.Before;
@@ -174,7 +175,7 @@ public class ThriftIT extends EasyMockTest {
}
}
);
- thrift = injector.getInstance(AuroraAdmin.Iface.class);
+ thrift = injector.getInstance(AnnotatedAuroraAdmin.class);
}
private void setQuota(String user, boolean allowed) throws Exception {
http://git-wip-us.apache.org/repos/asf/aurora/blob/05d75e5d/src/test/java/org/apache/aurora/scheduler/thrift/aop/AopModuleTest.java
----------------------------------------------------------------------
diff --git a/src/test/java/org/apache/aurora/scheduler/thrift/aop/AopModuleTest.java b/src/test/java/org/apache/aurora/scheduler/thrift/aop/AopModuleTest.java
index d20c9da..5c85300 100644
--- a/src/test/java/org/apache/aurora/scheduler/thrift/aop/AopModuleTest.java
+++ b/src/test/java/org/apache/aurora/scheduler/thrift/aop/AopModuleTest.java
@@ -65,7 +65,7 @@ public class AopModuleTest extends EasyMockTest {
}
},
new AopModule(toggledMethods));
- return injector.getInstance(Iface.class);
+ return injector.getInstance(AnnotatedAuroraAdmin.class);
}
@Test