/** * 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 javax.inject.Inject; import javax.inject.Provider; import com.google.common.annotations.VisibleForTesting; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import org.apache.aurora.common.stats.StatsProvider; import org.apache.aurora.gen.Response; import org.apache.aurora.gen.ResponseCode; import org.apache.aurora.scheduler.spi.Permissions; import org.apache.aurora.scheduler.spi.Permissions.Domain; import org.apache.aurora.scheduler.thrift.Responses; import org.apache.shiro.authz.Permission; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; 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 = LoggerFactory.getLogger(ShiroAuthorizingInterceptor.class); @VisibleForTesting static final String SHIRO_AUTHORIZATION_FAILURES = "shiro_authorization_failures"; private final Domain domain; private volatile boolean initialized; private Provider<Subject> subjectProvider; private AtomicLong shiroAdminAuthorizationFailures; ShiroAuthorizingInterceptor(Domain domain) { this.domain = requireNonNull(domain); } @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 = Permissions.createUnscopedPermission(domain, method.getName()); if (subject.isPermitted(checkedPermission)) { return invocation.proceed(); } else { shiroAdminAuthorizationFailures.incrementAndGet(); String responseMessage = "Subject " + subject.getPrincipal() + " lacks permission " + checkedPermission; LOG.warn(responseMessage); // TODO(ksweeney): 403 FORBIDDEN would be a more accurate translation of this response code. return Responses.addMessage(Responses.empty(), ResponseCode.AUTH_FAILED, responseMessage); } } }