package com.github.thomasdarimont.keycloak.auth.requirerole;

import org.jboss.logging.Logger;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.events.Errors;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage;
import org.keycloak.models.utils.RoleUtils;
import org.keycloak.services.messages.Messages;

import java.util.Set;

import static org.keycloak.models.utils.KeycloakModelUtils.getRoleFromString;

/**
 * Simple {@link Authenticator} that checks of the user has a given {@link RoleModel Role}.
 */
public class RequireRoleAuthenticator implements Authenticator {

    private static final Logger LOG = Logger.getLogger(RequireRoleAuthenticator.class);

    @Override
    public void authenticate(AuthenticationFlowContext context) {

        AuthenticatorConfigModel configModel = context.getAuthenticatorConfig();

        String roleName = configModel.getConfig().get(RequireRoleAuthenticatorFactory.ROLE);
        RealmModel realm = context.getRealm();
        UserModel user = context.getUser();

        if (userHasRole(realm, user, roleName)) {
            context.success();
            return;
        }

        LOG.debugf("Access denied because of missing role. realm=%s username=%s role=%s", realm.getName(), user.getUsername(), roleName);
        context.getEvent().user(user);
        context.getEvent().error(Errors.NOT_ALLOWED);
        context.forkWithErrorMessage(new FormMessage(Messages.NO_ACCESS));
    }

    /**
     * @param realm
     * @param user
     * @param roleName
     * @return true if roleName is in any of all user role mappings including all groups of user
     */
    private boolean userHasRole(RealmModel realm, UserModel user, String roleName) {

        if (roleName == null) {
            return false;
        }

        LOG.debugf("Checking if user=%s has role=%s", user.getUsername(), roleName);
        RoleModel requiredRole = getRoleFromString(realm, roleName);

        // First perform cheap role check for direct or composite roles
        Set<RoleModel> directAssignedRoles = user.getRoleMappings();
        if (RoleUtils.hasRole(directAssignedRoles, requiredRole)) {
            return true;
        }

        // Next perform more expensive roles check for group membership role mappings
        Set<RoleModel> nestedAssignedRoles = RoleUtils.getDeepUserRoleMappings(user);
        if (RoleUtils.hasRole(nestedAssignedRoles, requiredRole)) {
            return true;
        }

        LOG.debugf("User does not have the required role. user=%s role=%s assignedRoles=%s", user.getUsername(), requiredRole, nestedAssignedRoles);
        return false;
    }


    @Override
    public boolean requiresUser() {
        return false;
    }

    @Override
    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
        return true;
    }

    @Override
    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
        // NOOP
    }

    @Override
    public void action(AuthenticationFlowContext context) {
        // NOOP
    }

    @Override
    public void close() {
        // NOOP
    }
}