package io.fnx.backend.tools.authorization;

import com.googlecode.objectify.Key;
import io.fnx.backend.tools.auth.Principal;
import io.fnx.backend.tools.auth.PrincipalRole;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;

import javax.inject.Provider;
import java.lang.annotation.Annotation;
import java.util.Collection;

/**
 * Main class which should be configured to intercept calls to methods you care about (manager methods?) and
 * you want it to check for authorization of these calls.
 *
 * It has simple (plugin) architecture, in that it consists of sets of {@link AuthorizationGuard}s which react to
 * different annotations and different checks.
 * <br>
 * <br>
 * Sample configuration for a Guice based application:
 * <br>
 * <br>
 * <pre>
 *     // be sure to have provider for UserContext
 *     bind(UserContext.class).toProvider(UserContextProvider.class).in(RequestScoped.class);
 *
 *     final AuthorizationInterceptor fnxAuthorizationInterceptor = new AuthorizationInterceptor();
 *     // select your own set of guards
 *     fnxAuthorizationInterceptor.setGuards(createAuthorizationGuards());
 *     requestInjection(fnxAuthorizationInterceptor);
 *     // intercept calls to manager methods and check the authorization of these calls
 *     bindInterceptor(Matchers.subclassesOf(Service.class), Matchers.any(), fnxAuthorizationInterceptor);
 * </pre>
 *
 * @see AuthorizationGuard
 * @see Principal
 */
public class AuthorizationInterceptor implements MethodInterceptor {

    private final Provider<Principal> principalProvider;
    private AuthorizationGuard[] guards = new AuthorizationGuard[0];

    public AuthorizationInterceptor(Provider<Principal> principalProvider) {
        this.principalProvider = principalProvider;
    }

    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {
        final Principal principal = principalProvider.get();
        final AuthorizationResult result = runGuards(invocation, principal);

        if (result == null || result.success) {
            return invocation.proceed();
        } else {
            throw new PermissionDeniedException(result.msg);
        }
    }

    private AuthorizationResult runGuards(MethodInvocation invocation, Principal principal) {
        AuthorizationResult result = null;
        final Key<? extends Principal> callingUser;
        final PrincipalRole callingRole;

        if (principal != null) {
            callingUser = principal.getPrincipalKey();
            callingRole = principal.getUserRole();
        } else {
            callingUser = null;
            callingRole = null;
        }

        for (final AuthorizationGuard guard : guards) {
            // skip if the guard is non-definitive and authorization had already been successful
            if (!guard.isDefinitive() && result != null && result.success) continue;
            final Collection<Class<? extends Annotation>> annotationClasses = guard.getAnnotationClasses();
            if (annotationClasses == null) continue;
            for (Class<? extends Annotation> annotationClass : annotationClasses) {
                if (annotationClass == null) continue;
                final Annotation annotation = invocation.getMethod().getAnnotation(annotationClass);
                if (annotation != null) {
                    final AuthorizationResult inspectionResult = guard.guardInvocation(invocation, annotation, callingRole, callingUser);
                    if (result == null) { // seed authorization outcome from the first result
                        result = inspectionResult;
                    } else if (inspectionResult != null && inspectionResult.success) { // allow the total outcome to be changed to success
                        result = inspectionResult;                                     // so when first authorization guard suggests failure
                                                                                       // other guards might allow the call (they are additive)
                    } else if (inspectionResult != null && !inspectionResult.success && guard.isDefinitive()) { // definitive guards must succeed every time
                        return result;
                    }
                }
            }
        }
        return result;
    }

    public void setGuards(AuthorizationGuard[] guards) {
        if (guards != null) {
            this.guards = guards;
        } else {
            this.guards = new AuthorizationGuard[]{};
        }
    }
}

@@ -1900,69 +1900,8 @@
 e %7B%0A
-        final Principal principal = principalProvider.get();%0A
     
@@ -1963,19 +1963,8 @@
 tion
-, principal
 );%0A%0A
@@ -2220,29 +2220,8 @@
 tion
-, Principal principal
 ) %7B%0A
@@ -2266,332 +2266,8 @@
 ull;
-%0A        final Key%3C? extends Principal%3E callingUser;%0A        final PrincipalRole callingRole;%0A%0A        if (principal != null) %7B%0A            callingUser = principal.getPrincipalKey();%0A            callingRole = principal.getUserRole();%0A        %7D else %7B%0A            callingUser = null;%0A            callingRole = null;%0A        %7D
 %0A%0A  
@@ -2937,32 +2937,536 @@
 tion != null) %7B%0A
+                    final Principal principal = principalProvider.get();%0A                    final Key%3C? extends Principal%3E callingUser;%0A                    final PrincipalRole callingRole;%0A                    if (principal != null) %7B%0A                        callingUser = principal.getPrincipalKey();%0A                        callingRole = principal.getUserRole();%0A                    %7D else %7B%0A                        callingUser = null;%0A                        callingRole = null;%0A                    %7D%0A
                 

FIX AuthorizationInterceptor should acces the principal lazily