/*
 * 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.storm.daemon.ui.filters;

import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.ResourceInfo;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.security.Principal;
import java.util.Map;
import net.minidev.json.JSONValue;
import org.apache.commons.codec.Charsets;
import org.apache.commons.io.IOUtils;
import org.apache.storm.DaemonConfig;
import org.apache.storm.daemon.StormCommon;
import org.apache.storm.daemon.common.JsonResponseBuilder;
import org.apache.storm.daemon.ui.UIHelpers;
import org.apache.storm.daemon.ui.resources.AuthNimbusOp;
import org.apache.storm.daemon.ui.resources.StormApiResource;
import org.apache.storm.generated.AuthorizationException;
import org.apache.storm.security.auth.IAuthorizer;
import org.apache.storm.security.auth.ReqContext;
import org.apache.storm.thrift.TException;
import org.apache.storm.utils.NimbusClient;
import org.apache.storm.utils.Utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

@Provider
public class AuthorizedUserFilter implements ContainerRequestFilter {

    public static final Logger LOG = LoggerFactory.getLogger(AuthorizedUserFilter.class);
    public static Map<String, Object> conf = Utils.readStormConfig();
    public static IAuthorizer uiImpersonationHandler;
    public static IAuthorizer uiAclHandler;

    @Context private ResourceInfo resourceInfo;

    static {
        try {
            uiImpersonationHandler = StormCommon.mkAuthorizationHandler(
                        (String) conf.get(DaemonConfig.NIMBUS_IMPERSONATION_AUTHORIZER), conf);
            uiAclHandler = StormCommon.mkAuthorizationHandler(
                    (String) conf.get(DaemonConfig.NIMBUS_AUTHORIZER), conf);
        } catch (IllegalAccessException | InstantiationException | ClassNotFoundException e) {
            LOG.error("Error initializing AuthorizedUserFilter: ", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * makeResponse.
     * @param ex ex
     * @param request request
     * @param statusCode statusCode
     * @return error response
     */
    public static Response makeResponse(Exception ex, ContainerRequestContext request, int statusCode) {
        String callback = null;

        if (request.getMediaType() != null && request.getMediaType().equals(MediaType.APPLICATION_JSON_TYPE)) {
            try {
                String json = IOUtils.toString(request.getEntityStream(), Charsets.UTF_8);
                InputStream in = IOUtils.toInputStream(json);
                request.setEntityStream(in);
                Map<String, Object> requestBody = (Map<String, Object>) JSONValue.parse(json);
                if (requestBody.containsKey(StormApiResource.callbackParameterName)) {
                    callback = String.valueOf(requestBody.get(StormApiResource.callbackParameterName));
                }
            } catch (IOException e) {
                LOG.error("Exception while trying to get callback ", e);
            }
        }
        return new JsonResponseBuilder().setData(
                        UIHelpers.exceptionToJson(ex, statusCode)).setCallback(callback)
                        .setStatus(statusCode).build();
    }

    @Override
    public void filter(ContainerRequestContext containerRequestContext) {
        AuthNimbusOp annotation = resourceInfo.getResourceMethod().getAnnotation(AuthNimbusOp.class);
        if (annotation == null) {
            return;
        }
        String op = annotation.value();
        if (op == null) {
            return;
        }

        Map topoConf = null;
        if (annotation.needsTopoId()) {
            final String topoId = containerRequestContext.getUriInfo().getPathParameters().get("id").get(0);
            try (NimbusClient nimbusClient = NimbusClient.Builder.withConf(conf).build()) {
                topoConf = (Map) JSONValue.parse(nimbusClient.getClient().getTopologyConf(topoId));
            } catch (AuthorizationException ae) {
                LOG.error("Nimbus isn't allowing {} to access the topology conf of {}. {}", ReqContext.context(), topoId, ae.get_msg());
                containerRequestContext.abortWith(makeResponse(ae, containerRequestContext, 403));
                return;
            } catch (TException e) {
                LOG.error("Unable to fetch topo conf for {} due to ", topoId, e);
                containerRequestContext.abortWith(
                    makeResponse(new IOException("Unable to fetch topo conf for topo id " + topoId, e),
                            containerRequestContext, 500)
                );
                return;
            }
        }

        ReqContext reqContext = ReqContext.context();

        if (reqContext.isImpersonating()) {
            if (uiImpersonationHandler != null) {
                if (!uiImpersonationHandler.permit(reqContext, op, topoConf)) {
                    Principal realPrincipal = reqContext.realPrincipal();
                    Principal principal = reqContext.principal();
                    String user = "unknown";
                    if (principal != null) {
                        user = principal.getName();
                    }
                    String realUser = "unknown";
                    if (realPrincipal != null) {
                        realUser = realPrincipal.getName();
                    }
                    InetAddress remoteAddress = reqContext.remoteAddress();

                    containerRequestContext.abortWith(
                            makeResponse(new AuthorizationException(
                                    "user '" + realUser +  "' is not authorized to impersonate user '"
                                            + user + "' from host '" + remoteAddress.toString() + "'. Please"
                                            + "see SECURITY.MD to learn how to configure impersonation ACL."
                            ), containerRequestContext, 401)
                    );
                    return;
                }
            } else {
                LOG.warn("Principal {} is trying to impersonate {} but {} is not configured. "
                                + "This is a potential security hole. Please see SECURITY.MD to learn how to "
                                + "configure an impersonation authorizer.",
                        reqContext.realPrincipal().toString(), reqContext.principal().toString(),
                        DaemonConfig.NIMBUS_IMPERSONATION_AUTHORIZER);
            }
        }

        if (uiAclHandler != null) {
            if (!uiAclHandler.permit(reqContext, op, topoConf)) {
                Principal principal = reqContext.principal();
                String user = "unknown";
                if (principal != null) {
                    user = principal.getName();
                }
                containerRequestContext.abortWith(
                        makeResponse(new AuthorizationException("UI request '" + op + "' for '"
                                        + user + "' user is not authorized"),
                                containerRequestContext, 403)
                );
                return;
            }
        }
    }
}
