Plugin Directory

Changeset 3492106


Ignore:
Timestamp:
03/26/2026 07:29:16 PM (2 days ago)
Author:
fernandot
Message:

Forzar cambio de claves por rol

Location:
vigilante
Files:
49 added
7 edited

Legend:

Unmodified
Added
Removed
  • vigilante/trunk/admin/class-admin-ajax.php

    r3489456 r3492106  
    12291229
    12301230    /**
     1231     * AJAX: Force password reset by role
     1232     * Resets passwords for all users with the selected roles
     1233     */
     1234    public function ajax_force_password_reset_by_role() {
     1235        check_ajax_referer( 'vigilante_admin_nonce', 'nonce' );
     1236
     1237        if ( ! current_user_can( 'manage_options' ) ) {
     1238            wp_send_json_error( __( 'Permission denied.', 'vigilante' ) );
     1239        }
     1240
     1241        $roles = isset( $_POST['roles'] ) ? array_map( 'sanitize_key', (array) $_POST['roles'] ) : array();
     1242
     1243        if ( empty( $roles ) ) {
     1244            wp_send_json_error( __( 'No roles selected.', 'vigilante' ) );
     1245        }
     1246
     1247        // Validate that submitted roles actually exist.
     1248        $wp_roles = wp_roles();
     1249        foreach ( $roles as $role ) {
     1250            if ( ! isset( $wp_roles->roles[ $role ] ) ) {
     1251                wp_send_json_error(
     1252                    sprintf(
     1253                        /* translators: %s: Role slug */
     1254                        __( 'Invalid role: %s', 'vigilante' ),
     1255                        $role
     1256                    )
     1257                );
     1258            }
     1259        }
     1260
     1261        $include_self    = ! empty( $_POST['include_self'] );
     1262        $current_user_id = get_current_user_id();
     1263
     1264        $user_security = new Vigilante_User_Security( $this->settings, $this->activity_log );
     1265
     1266        $results = $user_security->force_password_reset_by_roles(
     1267            $roles,
     1268            $current_user_id,
     1269            ! $include_self
     1270        );
     1271
     1272        // Log the action.
     1273        if ( $this->activity_log ) {
     1274            $reset_by_user = get_userdata( $current_user_id );
     1275            $role_names    = array();
     1276
     1277            foreach ( $roles as $role ) {
     1278                $role_names[] = isset( $wp_roles->roles[ $role ] )
     1279                    ? translate_user_role( $wp_roles->roles[ $role ]['name'] )
     1280                    : $role;
     1281            }
     1282
     1283            $this->activity_log->log(
     1284                'user',
     1285                'force_password_reset_by_role',
     1286                sprintf(
     1287                    /* translators: 1: Number of users, 2: Role names, 3: Admin username */
     1288                    __( 'Password reset forced for %1$d users (roles: %2$s) by %3$s', 'vigilante' ),
     1289                    $results['success'],
     1290                    implode( ', ', $role_names ),
     1291                    $reset_by_user ? $reset_by_user->user_login : __( 'System', 'vigilante' )
     1292                ),
     1293                array(
     1294                    'count'        => $results['success'],
     1295                    'roles'        => $roles,
     1296                    'reset_by'     => $current_user_id,
     1297                    'include_self' => $include_self,
     1298                ),
     1299                'warning'
     1300            );
     1301        }
     1302
     1303        $message = sprintf(
     1304            /* translators: %d: Number of users */
     1305            __( 'Password reset forced for %d user(s). Reset emails sent.', 'vigilante' ),
     1306            $results['success']
     1307        );
     1308
     1309        if ( $results['failed'] > 0 ) {
     1310            $message .= ' ' . sprintf(
     1311                /* translators: %d: Number of failures */
     1312                __( '%d failed.', 'vigilante' ),
     1313                $results['failed']
     1314            );
     1315        }
     1316
     1317        // Check if current user was included via role membership.
     1318        $resetting_self = false;
     1319        if ( $include_self ) {
     1320            $current_user   = wp_get_current_user();
     1321            $resetting_self = ! empty( array_intersect( $roles, $current_user->roles ) );
     1322        }
     1323
     1324        wp_send_json_success( array(
     1325            'message'        => $message,
     1326            'results'        => $results,
     1327            'resetting_self' => $resetting_self,
     1328        ) );
     1329    }
     1330
     1331    /**
    12311332     * AJAX: Approve pending user
    12321333     */
  • vigilante/trunk/admin/class-admin.php

    r3490838 r3492106  
    142142        add_action( 'wp_ajax_vigilante_force_password_reset', array( $this, 'ajax_force_password_reset' ) );
    143143        add_action( 'wp_ajax_vigilante_force_password_reset_all', array( $this, 'ajax_force_password_reset_all' ) );
     144        add_action( 'wp_ajax_vigilante_force_password_reset_by_role', array( $this, 'ajax_force_password_reset_by_role' ) );
    144145
    145146        // User approval AJAX handlers
     
    10361037                'warningResettingSelfAll'      => __( 'WARNING: You are including yourself. Your session will end immediately.', 'vigilante' ),
    10371038                'forceResetAll'                => __( 'Force Reset for ALL Users', 'vigilante' ),
     1039                // Role-based password reset strings
     1040                /* translators: %d: number of users */
     1041                'confirmForceResetByRole'      => __( 'Force password reset for %d user(s) with the selected roles? A password reset email will be sent to each user.', 'vigilante' ),
     1042                'noRolesSelected'              => __( 'Please select at least one role.', 'vigilante' ),
     1043                'forceResetByRole'             => __( 'Force Reset for Selected Roles', 'vigilante' ),
    10381044                // User approval strings
    10391045                'confirmApprove'               => __( 'Approve this user?', 'vigilante' ),
     
    28552861                </div>
    28562862
     2863                <!-- Reset by Role -->
     2864                <div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
     2865                    <h4><?php esc_html_e( 'Reset by role', 'vigilante' ); ?></h4>
     2866                    <p class="description"><?php esc_html_e( 'Select one or more roles to force a password reset for all users with those roles. Ideal for security incidents where you need to reset access quickly.', 'vigilante' ); ?></p>
     2867
     2868                    <?php
     2869                    $wp_roles      = wp_roles();
     2870                    $user_counts   = count_users();
     2871                    $avail_roles   = $user_counts['avail_roles'] ?? array();
     2872                    $current_user  = wp_get_current_user();
     2873                    $current_roles = $current_user->roles;
     2874                    ?>
     2875
     2876                    <fieldset class="vigilante-role-checkboxes" style="margin-top: 10px;">
     2877                        <?php foreach ( $wp_roles->roles as $role_slug => $role_data ) :
     2878                            $count = $avail_roles[ $role_slug ] ?? 0;
     2879                            if ( 0 === $count ) {
     2880                                continue;
     2881                            }
     2882                            $role_name = translate_user_role( $role_data['name'] );
     2883                        ?>
     2884                            <label style="display: block; margin-bottom: 6px;">
     2885                                <input type="checkbox"
     2886                                    class="vigilante-reset-role-checkbox"
     2887                                    value="<?php echo esc_attr( $role_slug ); ?>"
     2888                                    data-count="<?php echo absint( $count ); ?>">
     2889                                <?php
     2890                                printf(
     2891                                    /* translators: 1: Role name, 2: Number of users */
     2892                                    '%1$s <span class="description">(%2$d)</span>',
     2893                                    esc_html( $role_name ),
     2894                                    absint( $count )
     2895                                );
     2896                                ?>
     2897                                <?php if ( in_array( $role_slug, $current_roles, true ) ) : ?>
     2898                                    <em class="description"><?php esc_html_e( '(includes you)', 'vigilante' ); ?></em>
     2899                                <?php endif; ?>
     2900                            </label>
     2901                        <?php endforeach; ?>
     2902                    </fieldset>
     2903
     2904                    <div id="vigilante-reset-role-summary" style="display: none; margin-top: 10px;">
     2905                        <p>
     2906                            <span class="dashicons dashicons-groups" style="color: #2271b1;"></span>
     2907                            <strong id="vigilante-reset-role-count">0</strong>
     2908                            <?php esc_html_e( 'user(s) will be affected.', 'vigilante' ); ?>
     2909                        </p>
     2910                    </div>
     2911
     2912                    <div class="vigilante-password-reset-options" id="vigilante-reset-role-self-option" style="display: none; margin-top: 10px;">
     2913                        <label>
     2914                            <input type="checkbox" id="vigilante-reset-role-include-self" value="1">
     2915                            <?php esc_html_e( 'Include myself (your current session will end)', 'vigilante' ); ?>
     2916                        </label>
     2917                    </div>
     2918
     2919                    <p class="submit">
     2920                        <button type="button" id="vigilante-reset-by-role" class="button button-primary" disabled>
     2921                            <?php esc_html_e( 'Force Reset for Selected Roles', 'vigilante' ); ?>
     2922                        </button>
     2923                    </p>
     2924                </div>
     2925
    28572926                <!-- Reset All Users -->
    28582927                <div class="vigilante-password-reset-box" style="margin-top: 20px; padding-top: 20px; border-top: 1px solid #ddd;">
  • vigilante/trunk/assets/js/admin.js

    r3490838 r3492106  
    20862086                self.forceResetAllUsers();
    20872087            });
     2088
     2089            // Reset by role - checkbox change
     2090            $(document).on('change', '.vigilante-reset-role-checkbox', function() {
     2091                self.updateRoleResetSummary();
     2092            });
     2093
     2094            // Reset by role button
     2095            $(document).on('click', '#vigilante-reset-by-role', function(e) {
     2096                e.preventDefault();
     2097                self.forceResetByRoles();
     2098            });
    20882099        },
    20892100
     
    23012312                complete: function() {
    23022313                    $btn.prop('disabled', false).text(vigilanteAdmin.strings.forceResetAll || 'Force Reset for ALL Users');
     2314                }
     2315            });
     2316        },
     2317
     2318        /**
     2319         * Update role reset summary when checkboxes change
     2320         */
     2321        updateRoleResetSummary: function() {
     2322            var totalCount = 0;
     2323            var includesSelf = false;
     2324
     2325            $('.vigilante-reset-role-checkbox:checked').each(function() {
     2326                totalCount += parseInt($(this).data('count') || 0);
     2327                if ($(this).closest('label').find('em').length > 0) {
     2328                    includesSelf = true;
     2329                }
     2330            });
     2331
     2332            var $summary = $('#vigilante-reset-role-summary');
     2333            var $selfOption = $('#vigilante-reset-role-self-option');
     2334            var $btn = $('#vigilante-reset-by-role');
     2335
     2336            if (totalCount > 0) {
     2337                $('#vigilante-reset-role-count').text(totalCount);
     2338                $summary.show();
     2339                $btn.prop('disabled', false);
     2340            } else {
     2341                $summary.hide();
     2342                $btn.prop('disabled', true);
     2343            }
     2344
     2345            if (includesSelf) {
     2346                $selfOption.show();
     2347            } else {
     2348                $selfOption.hide();
     2349                $('#vigilante-reset-role-include-self').prop('checked', false);
     2350            }
     2351        },
     2352
     2353        /**
     2354         * Force reset for users with selected roles
     2355         */
     2356        forceResetByRoles: function() {
     2357            var self = this;
     2358            var roles = [];
     2359
     2360            $('.vigilante-reset-role-checkbox:checked').each(function() {
     2361                roles.push($(this).val());
     2362            });
     2363
     2364            if (roles.length === 0) {
     2365                self.showNotice('error', vigilanteAdmin.strings.noRolesSelected || 'Please select at least one role.');
     2366                return;
     2367            }
     2368
     2369            var totalCount = 0;
     2370            $('.vigilante-reset-role-checkbox:checked').each(function() {
     2371                totalCount += parseInt($(this).data('count') || 0);
     2372            });
     2373
     2374            var includeSelf = $('#vigilante-reset-role-include-self').is(':checked');
     2375
     2376            var confirmMsg = (vigilanteAdmin.strings.confirmForceResetByRole || 'Force password reset for %d user(s) with the selected roles? A password reset email will be sent to each user.').replace('%d', totalCount);
     2377            if (includeSelf) {
     2378                confirmMsg += '\n\n' + (vigilanteAdmin.strings.warningResettingSelfAll || 'WARNING: You are including yourself. Your session will end immediately.');
     2379            }
     2380
     2381            if (!confirm(confirmMsg)) {
     2382                return;
     2383            }
     2384
     2385            var $btn = $('#vigilante-reset-by-role');
     2386            $btn.prop('disabled', true).text(vigilanteAdmin.strings.processing || 'Processing...');
     2387
     2388            $.ajax({
     2389                url: vigilanteAdmin.ajaxUrl,
     2390                type: 'POST',
     2391                data: {
     2392                    action: 'vigilante_force_password_reset_by_role',
     2393                    nonce: vigilanteAdmin.nonce,
     2394                    roles: roles,
     2395                    include_self: includeSelf ? 1 : 0
     2396                },
     2397                success: function(response) {
     2398                    if (response.success) {
     2399                        self.showNotice('success', response.data.message);
     2400
     2401                        // Clear role checkboxes
     2402                        $('.vigilante-reset-role-checkbox').prop('checked', false);
     2403                        self.updateRoleResetSummary();
     2404
     2405                        if (response.data.resetting_self) {
     2406                            setTimeout(function() {
     2407                                window.location.href = vigilanteAdmin.logoutUrl || '/wp-login.php';
     2408                            }, 2000);
     2409                        } else {
     2410                            setTimeout(function() {
     2411                                location.reload();
     2412                            }, 1500);
     2413                        }
     2414                    } else {
     2415                        self.showNotice('error', response.data);
     2416                    }
     2417                },
     2418                error: function() {
     2419                    self.showNotice('error', vigilanteAdmin.strings.anErrorOccurred || 'An error occurred');
     2420                },
     2421                complete: function() {
     2422                    $btn.prop('disabled', false).text(vigilanteAdmin.strings.forceResetByRole || 'Force Reset for Selected Roles');
    23032423                }
    23042424            });
  • vigilante/trunk/includes/class-login-security.php

    r3482104 r3492106  
    842842            'two-factor',             // TOTP setup required
    843843            'grace period',           // TOTP grace period expired
     844            'Password reset required', // Force password reset by admin
    844845        );
    845846
  • vigilante/trunk/includes/class-user-security.php

    r3490838 r3492106  
    157157            add_action( 'login_message', array( $this, 'show_registration_pending_message' ) );
    158158        }
     159
     160        // Force password reset login message (always active, independent of settings)
     161        add_filter( 'authenticate', array( $this, 'check_force_reset_on_login' ), 30, 3 );
     162        add_action( 'after_password_reset', array( $this, 'clear_force_reset_meta' ), 10, 1 );
    159163    }
    160164
     
    823827        wp_set_password( wp_generate_password( 32, true, true ), $user_id );
    824828
     829        // Flag user so login page shows an informative message instead of generic error
     830        update_user_meta( $user_id, 'vigilante_force_reset_pending', time() );
     831
    825832        // Send the native WordPress password reset email
    826833        $email_sent = $this->send_native_reset_email( $user, $reset_key );
     
    942949    }
    943950
     951    /**
     952     * Force password reset for users with specific roles
     953     *
     954     * @param array $roles            Array of role slugs.
     955     * @param int   $reset_by_user_id User ID who initiated the reset.
     956     * @param bool  $exclude_current  Whether to exclude current user.
     957     * @return array Results with counts and affected roles.
     958     */
     959    public function force_password_reset_by_roles( $roles, $reset_by_user_id = 0, $exclude_current = true ) {
     960        if ( empty( $roles ) ) {
     961            return array(
     962                'success'     => 0,
     963                'failed'      => 0,
     964                'emails_sent' => 0,
     965                'total'       => 0,
     966                'roles'       => array(),
     967            );
     968        }
     969
     970        $user_ids = array();
     971
     972        foreach ( $roles as $role ) {
     973            $role_users = get_users( array(
     974                'role'   => $role,
     975                'fields' => 'ID',
     976            ) );
     977            $user_ids = array_merge( $user_ids, $role_users );
     978        }
     979
     980        // Remove duplicates (users with multiple roles).
     981        $user_ids = array_unique( array_map( 'absint', $user_ids ) );
     982
     983        // phpcs:disable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- Excluding single user is acceptable here.
     984        if ( $exclude_current && $reset_by_user_id ) {
     985            $user_ids = array_diff( $user_ids, array( $reset_by_user_id ) );
     986        }
     987        // phpcs:enable WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude
     988
     989        $results          = $this->force_password_reset_bulk( array_values( $user_ids ), $reset_by_user_id );
     990        $results['roles'] = $roles;
     991
     992        return $results;
     993    }
     994
     995    /**
     996     * Show informative message when a user with a forced reset tries to log in
     997     *
     998     * Hooked to 'authenticate' at priority 30 (after default password check at 20).
     999     * When login fails and the user has a pending forced reset, replaces the generic
     1000     * "incorrect password" error with an explanation and instructions.
     1001     *
     1002     * @param WP_User|WP_Error|null $user     User object, error, or null.
     1003     * @param string                $username Username or email.
     1004     * @param string                $password Password.
     1005     * @return WP_User|WP_Error|null
     1006     */
     1007    public function check_force_reset_on_login( $user, $username, $password ) {
     1008        // Only act on failed login attempts.
     1009        if ( ! is_wp_error( $user ) ) {
     1010            return $user;
     1011        }
     1012
     1013        // Only replace "incorrect_password" errors.
     1014        if ( ! in_array( 'incorrect_password', $user->get_error_codes(), true ) ) {
     1015            return $user;
     1016        }
     1017
     1018        // Resolve the user by login or email.
     1019        $login_user = get_user_by( 'login', $username );
     1020        if ( ! $login_user ) {
     1021            $login_user = get_user_by( 'email', $username );
     1022        }
     1023
     1024        if ( ! $login_user ) {
     1025            return $user;
     1026        }
     1027
     1028        // Check if this user has a pending forced reset.
     1029        $force_reset = get_user_meta( $login_user->ID, 'vigilante_force_reset_pending', true );
     1030        if ( ! $force_reset ) {
     1031            return $user;
     1032        }
     1033
     1034        // Skip brute force counter for this controlled rejection.
     1035        add_filter( 'vigilante_skip_failed_login_count', '__return_true' );
     1036
     1037        return new WP_Error(
     1038            'vigilante_force_reset',
     1039            __( '<strong>Password reset required:</strong> Your password has been reset by the site administrator for security reasons. Please check your email for a link to set a new password.', 'vigilante' )
     1040        );
     1041    }
     1042
     1043    /**
     1044     * Clear force reset meta after user successfully resets their password
     1045     *
     1046     * Hooked to 'after_password_reset'.
     1047     *
     1048     * @param WP_User $user User object.
     1049     */
     1050    public function clear_force_reset_meta( $user ) {
     1051        delete_user_meta( $user->ID, 'vigilante_force_reset_pending' );
     1052    }
     1053
    9441054    // =========================================================================
    9451055    // Registration Approval - Manual approval for new user registrations
  • vigilante/trunk/readme.txt

    r3490838 r3492106  
    55Tested up to: 6.9
    66Requires PHP: 7.4
    7 Stable tag: 1.7.2
     7Stable tag: 1.8.0
    88License: GPL v2 or later
    99License URI: https://www.gnu.org/licenses/gpl-2.0.html
     
    111111* Password expiration with configurable intervals
    112112* Password history - prevent reusing old passwords
    113 * Force password reset for all users (post-hack recovery)
     113* Force password reset — by specific users, by role, or all users (post-hack recovery)
    114114* Session limits - control concurrent logins per user
    115115* Session management - view and revoke active sessions
     
    374374
    375375== Changelog ==
     376
     377= 1.8.0 =
     378* New - Force password reset by role: select one or more roles to reset all their users at once, ideal for security incidents
     379* New - Informative login message when a user tries to log in after a forced password reset
    376380
    377381= 1.7.2 =
     
    606610== Upgrade Notice ==
    607611
     612= 1.8.0 =
     613Added role-based password reset tool for faster incident response and informative login message for users affected by a forced reset.
     614
    608615= 1.7.2 =
    609616Dashboard recommendations now link directly to settings. Security scan totals include ignored files count. Activity Log tab renamed to Security Audit. Firewall shows your current IP and cache compatibility notes.
  • vigilante/trunk/vigilante.php

    r3490838 r3492106  
    44 * Plugin URI: https://servicios.ayudawp.com
    55 * Description: Complete security solution for WordPress. Firewall, 2FA, security headers, login protection, file integrity monitoring, activity logging and more.
    6  * Version: 1.7.2
     6 * Version: 1.8.0
    77 * Author: Fernando Tellado
    88 * Author URI: https://ayudawp.com
     
    2525 * Plugin constants
    2626 */
    27 define( 'VIGILANTE_VERSION', '1.7.2' );
     27define( 'VIGILANTE_VERSION', '1.8.0' );
    2828define( 'VIGILANTE_PLUGIN_FILE', __FILE__ );
    2929define( 'VIGILANTE_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
Note: See TracChangeset for help on using the changeset viewer.