blob: f843b042ae2fd68b8a3d2ab3c9eb111b47eb3771 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.cn.anubis.service;
import static org.apache.fineract.cn.anubis.config.AnubisConstants.LOGGER_NAME;
import io.jsonwebtoken.lang.Assert;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nonnull;
import org.apache.fineract.cn.anubis.annotation.AcceptedTokenType;
import org.apache.fineract.cn.anubis.annotation.Permittable;
import org.apache.fineract.cn.anubis.api.v1.domain.AllowedOperation;
import org.apache.fineract.cn.anubis.api.v1.domain.PermittableEndpoint;
import org.apache.fineract.cn.anubis.config.AnubisProperties;
import org.apache.fineract.cn.anubis.security.ApplicationPermission;
import org.apache.fineract.cn.lang.ApplicationName;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.actuate.endpoint.mvc.EndpointHandlerMapping;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
/**
* @author Myrle Krantz
*/
@Component
public class PermittableService {
private final RequestMappingHandlerMapping requestMappingHandlerMapping;
private final EndpointHandlerMapping endpointHandlerMapping;
private final ApplicationName applicationName;
private final Permittable defaultPermittable;
@Autowired
public PermittableService(final RequestMappingHandlerMapping requestMappingHandlerMapping,
final EndpointHandlerMapping endpointHandlerMapping,
final ApplicationName applicationName,
final AnubisProperties anubisProperties,
final @Qualifier(LOGGER_NAME) Logger logger) {
this.requestMappingHandlerMapping = requestMappingHandlerMapping;
this.endpointHandlerMapping = endpointHandlerMapping;
this.applicationName = applicationName;
if (anubisProperties.getAcceptGuestTokensForSystemEndpoints()) {
logger.error("The service property anubis.tokenTypeRequiredForSystemEndpoints is set to GUEST. This " +
"feature is intended for use only in test environments. Turning it on in a production environment " +
"could be a serious security vulnerability.");
this.defaultPermittable = guestPermittable();}
else {
this.defaultPermittable = systemPermittable();
}
}
public Set<ApplicationPermission> getPermittableEndpointsAsPermissions(
final AcceptedTokenType... acceptedTokenType) {
final Set<PermittableEndpoint> permittableEndpoints
= getPermittableEndpointsHelper(Arrays.asList(acceptedTokenType), false);
return Collections.unmodifiableSet(
permittableEndpoints.stream()
.map(x -> new ApplicationPermission(x.getPath(), mapHttpMethod(x.getMethod()), x.isAcceptTokenIntendedForForeignApplication()))
.collect(Collectors.toSet()));
}
private static AllowedOperation mapHttpMethod(final String httpMethod) {
switch (httpMethod) {
case "GET":
return AllowedOperation.READ;
case "HEAD":
return AllowedOperation.READ;
case "POST":
return AllowedOperation.CHANGE;
case "PUT":
return AllowedOperation.CHANGE;
case "DELETE":
return AllowedOperation.DELETE;
default:
throw new IllegalArgumentException("Unsupported HTTP Method " + httpMethod);
}
}
public Set<PermittableEndpoint> getPermittableEndpoints(final Collection<AcceptedTokenType> acceptedTokenTypes) {
return getPermittableEndpointsHelper(acceptedTokenTypes, true);
}
private Set<PermittableEndpoint> getPermittableEndpointsHelper(
final Collection<AcceptedTokenType> acceptedTokenTypes, boolean withAppName) {
Assert.notEmpty(acceptedTokenTypes);
final Set<PermittableEndpoint> permittableEndpoints = new LinkedHashSet<>();
fillPermittableEndpointsFromHandlerMethods(acceptedTokenTypes, withAppName, this.requestMappingHandlerMapping.getHandlerMethods(), permittableEndpoints);
fillPermittableEndpointsFromHandlerMethods(acceptedTokenTypes, withAppName, this.endpointHandlerMapping.getHandlerMethods(), permittableEndpoints);
if (acceptedTokenTypes.contains(AcceptedTokenType.SYSTEM)) {
final PermittableEndpoint permittableEndpoint = new PermittableEndpoint();
if (withAppName)
permittableEndpoint.setPath(applicationName + "/initialize");
else
permittableEndpoint.setPath("/initialize");
permittableEndpoint.setMethod("POST");
permittableEndpoint.setAcceptTokenIntendedForForeignApplication(false);
permittableEndpoints.add(permittableEndpoint);
}
return permittableEndpoints;
}
private static class WhatINeedToBuildAPermittableEndpoint
{
@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
final Permittable annotation;
final Set<String> patterns;
final Set<RequestMethod> methods;
WhatINeedToBuildAPermittableEndpoint(final @Nonnull Permittable annotation,
final @Nonnull Set<String> patterns,
final @Nonnull Set<RequestMethod> methods) {
this.annotation = annotation;
this.patterns = patterns;
this.methods = methods;
}
}
private void fillPermittableEndpointsFromHandlerMethods(
final Collection<AcceptedTokenType> acceptedTokenTypes,
final boolean withAppName,
final Map<RequestMappingInfo, HandlerMethod> handlerMethods,
final @Nonnull Set<PermittableEndpoint> permittableEndpoints) {
handlerMethods.entrySet()
.stream().flatMap(handlerMethod -> PermittableService.whatINeedToBuildAPermittableEndpoint(handlerMethod, defaultPermittable))
.filter(whatINeedToBuildAPermittableEndpoint -> acceptedTokenTypes.contains(getAcceptedTokenType(whatINeedToBuildAPermittableEndpoint)))
.forEachOrdered(whatINeedToBuildAPermittableEndpoint ->
whatINeedToBuildAPermittableEndpoint.patterns
.forEach(pattern -> whatINeedToBuildAPermittableEndpoint.methods
.forEach(method -> {
final PermittableEndpoint permittableEndpoint = new PermittableEndpoint();
permittableEndpoint.setPath(getPath(
withAppName ? applicationName.toString() : "",
pattern,
whatINeedToBuildAPermittableEndpoint));
permittableEndpoint.setMethod(method.name());
permittableEndpoint.setGroupId(whatINeedToBuildAPermittableEndpoint.annotation.groupId());
permittableEndpoint.setAcceptTokenIntendedForForeignApplication(whatINeedToBuildAPermittableEndpoint.annotation.acceptTokenIntendedForForeignApplication());
permittableEndpoints.add(permittableEndpoint);
})
));
}
static private AcceptedTokenType getAcceptedTokenType(final @Nonnull WhatINeedToBuildAPermittableEndpoint whatINeedToBuildAPermittableEndpoint) {
return whatINeedToBuildAPermittableEndpoint.annotation.value();
}
static private String getPath(final @Nonnull String applicationName,
final @Nonnull String pattern,
final @Nonnull WhatINeedToBuildAPermittableEndpoint whatINeedToBuildAPermittableEndpoint) {
final String programmerSpecifiedEndpoint = whatINeedToBuildAPermittableEndpoint.annotation.permittedEndpoint();
if (!programmerSpecifiedEndpoint.isEmpty())
return applicationName + programmerSpecifiedEndpoint;
final StringBuilder ret = new StringBuilder(applicationName);
PermissionSegmentMatcher.getServletPathSegmentMatchers(pattern).stream() //parse the pattern into segments
.map(x -> x.isParameterSegment() ? "*" : x.getPermissionSegment()) //replace the parameter segments with stars.
.filter(x -> !x.isEmpty()) //remove any empty segments
.forEachOrdered(x -> ret.append("/").append(x)); //reassemble into a string.
return ret.toString();
}
static private Stream<WhatINeedToBuildAPermittableEndpoint> whatINeedToBuildAPermittableEndpoint(
final Map.Entry<RequestMappingInfo, HandlerMethod> handlerMethod,
final Permittable defaultPermittable) {
final Set<Permittable> annotations = getPermittableAnnotations(handlerMethod, defaultPermittable);
final Set<String> patterns = handlerMethod.getKey().getPatternsCondition().getPatterns();
final Set<RequestMethod> methods = handlerMethod.getKey().getMethodsCondition().getMethods();
return annotations.stream()
.map(annotation -> new WhatINeedToBuildAPermittableEndpoint(annotation, patterns, methods));
}
static private Set<Permittable> getPermittableAnnotations(
final Map.Entry<RequestMappingInfo, HandlerMethod> handlerMethod,
final Permittable defaultPermittable) {
final Method method = handlerMethod.getValue().getMethod();
final Set<Permittable> ret = AnnotationUtils.getRepeatableAnnotations(method, Permittable.class);
if (ret.isEmpty())
return Collections.singleton(defaultPermittable);
else
return ret;
}
static private Permittable guestPermittable() {
return new Permittable() {
@Override
public Class<? extends Annotation> annotationType() {
return Permittable.class;
}
@Override
public AcceptedTokenType value() {
return AcceptedTokenType.GUEST;
}
@Override
public String groupId() {
return "";
}
@Override
public String permittedEndpoint() {
return "";
}
@Override
public boolean acceptTokenIntendedForForeignApplication() {
return false;
}
};
}
static private Permittable systemPermittable() {
return new Permittable() {
@Override
public Class<? extends Annotation> annotationType() {
return Permittable.class;
}
@Override
public AcceptedTokenType value() {
return AcceptedTokenType.SYSTEM;
}
@Override
public String groupId() {
return "";
}
@Override
public String permittedEndpoint() {
return "";
}
@Override
public boolean acceptTokenIntendedForForeignApplication() {
return false;
}
};
}
}