Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
3c986e1
Add wildcard htpasswd support for multisite subdomains and alias domains
mrrobot47 Jan 8, 2026
e697bb2
Fix duplicate domain entries in htpasswd file generation
mrrobot47 Jan 8, 2026
5589921
Add wildcard and alias domain support to whitelist generation
mrrobot47 Jan 8, 2026
3e7027c
Skip *.site_url alias only for subdomain multisites
mrrobot47 Jan 8, 2026
63ac4fa
Add explicit handling for empty auth/whitelist arrays
mrrobot47 Jan 8, 2026
569ed56
Clean up wildcard files upfront to handle site type changes
mrrobot47 Jan 8, 2026
6bd97cc
refactor(auth): share site domain collection and file generation
mrrobot47 Sep 24, 2026
f2b9b11
fix(auth): don't generate _wildcard files for plain alias domains
mrrobot47 Sep 24, 2026
5a3cf0e
fix(auth): don't remove _wildcard files the site doesn't own
mrrobot47 Sep 24, 2026
5f12c49
fix(auth): remove site files when the site has no own entries
mrrobot47 Sep 24, 2026
dfa56f0
fix(auth): escape arguments of the htpasswd command
mrrobot47 Sep 24, 2026
852c10b
fix(auth): remove all of a site's auth and ACL files on delete
mrrobot47 Sep 24, 2026
e0778d3
feat(auth): sync auth files when alias domains change
mrrobot47 Sep 24, 2026
fa6c6b1
fix(auth): skip unsafe alias names and guard auth file removal
mrrobot47 Sep 24, 2026
a9572ff
perf(auth): write a site's htpasswd file once and copy it to its othe…
mrrobot47 Sep 24, 2026
3f59e38
fix(auth): keep htpasswd credentials out of ee.log
mrrobot47 Sep 24, 2026
88a1fb9
fix(auth): protect alias domains before the proxy serves them
mrrobot47 Sep 24, 2026
bbe88ac
fix(auth): reload the proxy on site cleanup only when something was r…
mrrobot47 Sep 24, 2026
dc373d8
refactor(auth): look up sites with Site::find() when regenerating glo…
mrrobot47 Sep 24, 2026
ced81fa
feat(auth): regenerate site auth files on upgrade
mrrobot47 Sep 24, 2026
6de4d78
refactor(auth): keep the global auth file names in one constant
mrrobot47 Sep 24, 2026
9b3cd1f
fix(auth): protect alias domains that contain underscores
mrrobot47 Sep 24, 2026
1a34106
fix(auth): replace htpasswd files atomically
mrrobot47 Sep 24, 2026
1ea9bef
fix(auth): keep htpasswd usernames out of ee.log
mrrobot47 Sep 24, 2026
3a5d2f6
refactor(auth): generate files for alias domains being added with the…
mrrobot47 Sep 24, 2026
510f8cc
fix(auth): copy a site's existing htpasswd file to its other domains …
mrrobot47 Sep 24, 2026
cf073e7
docs(auth): say the auth files migration needs the new nginx-proxy image
mrrobot47 Sep 24, 2026
cbf7d11
fix(auth): use site-command's alias domain rule
mrrobot47 Sep 24, 2026
d313f5e
fix(auth): keep the global htpasswd file until its replacement is wri…
mrrobot47 Sep 24, 2026
329faee
fix(auth): copy auth files to a site's other domains atomically
mrrobot47 Sep 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
},
"files": [
"auth-command.php",
"src/auth-utils.php"
"src/auth-utils.php",
"src/helper/hooks.php"
]
},
"extra": {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?php

namespace EE\Migration;

use EE;
use EE\Migration\Base;
use EE\Model\Site;
use function EE\Auth\Utils\generate_site_auth_files;
use function EE\Auth\Utils\generate_site_whitelist;

class RegenerateSiteAuthFiles extends Base {

private $sites;

public function __construct() {

parent::__construct();
$this->sites = Site::all();
if ( $this->is_first_execution || ! $this->sites ) {
$this->skip_this_migration = true;
}
}

/**
* Regenerate the htpasswd and ACL files of all sites.
*
* Older versions only wrote `htpasswd/<site>` and `<site>_acl`, leaving subdomains and alias domains unprotected, and kept files of sites without own entries.
*
* @throws EE\ExitException
*/
public function up() {

if ( $this->skip_this_migration ) {
EE::debug( 'Skipping site auth files regeneration migration as it is not needed.' );

return;
}

foreach ( $this->sites as $site ) {
try {
generate_site_auth_files( $site->site_url, $site );
generate_site_whitelist( $site->site_url, $site );
} catch ( \Throwable $e ) {
EE::warning( sprintf( 'Could not regenerate the auth files of %s: %s', $site->site_url, $e->getMessage() ) );
}
}

\EE\Site\Utils\reload_global_nginx_proxy();
}

/**
* Not reverted. The files need the nginx-proxy image of the same release: the previous image also applies `_wildcard.*` files to sibling sites.
*/
public function down() {
}
}
113 changes: 22 additions & 91 deletions src/Auth_Command.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@
use EE\Model\Auth;
use EE\Model\Whitelist;
use Symfony\Component\Filesystem\Filesystem;
use function EE\Auth\Utils\generate_site_auth_files;
use function EE\Auth\Utils\generate_site_whitelist;
use function EE\Auth\Utils\verify_htpasswd_is_present;
use function EE\Auth\Utils\write_htpasswd_file;
use function EE\Site\Utils\auto_site_name;
use function EE\Site\Utils\get_site_info;
use function EE\Site\Utils\is_reserved_proxy_file_name;
use function EE\Site\Utils\reload_global_nginx_proxy;

class Auth_Command extends EE_Command {
Expand Down Expand Up @@ -161,7 +165,7 @@ private function create_auth( array $assoc_args, bool $global, string $site_url
if ( 'default' === $site_url ) {
$this->generate_global_auth_files();
} else {
$this->generate_site_auth_files( $site_url );
generate_site_auth_files( $site_url, $this->site_data );
}

EE::log( 'Reloading global reverse proxy.' );
Expand Down Expand Up @@ -200,7 +204,7 @@ private function create_whitelist( string $site_url, string $ips ) {
if ( 'default' === $site_url ) {
$this->generate_global_whitelist();
} else {
$this->generate_site_whitelist( $site_url );
generate_site_whitelist( $site_url, $this->site_data );
}

reload_global_nginx_proxy();
Expand Down Expand Up @@ -255,24 +259,17 @@ private function generate_global_auth_files() {
$global_admin_tools_auth = Auth::get_global_admin_tools_auth();

if ( ! empty( $global_admin_tools_auth ) ) {
EE::exec( sprintf( 'docker exec %s htpasswd -bc /etc/nginx/htpasswd/default_admin_tools %s %s', EE_PROXY_TYPE, $global_admin_tools_auth->username, $global_admin_tools_auth->password ) );
write_htpasswd_file( 'default_admin_tools', $global_admin_tools_auth );
} else {
$this->fs->remove( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/default_admin_tools' );
$this->fs->remove( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/default' );
$auths = Auth::get_global_auths();

if ( empty( $auths ) ) {
$this->fs->remove( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/default_admin_tools' );
$this->fs->remove( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/default' );
$this->regen_admin_tools_auth();
} else {
foreach ( $auths as $key => $auth ) {
$flags = 'b';

if ( 0 === $key ) {
$flags = 'bc';
}

EE::exec( sprintf( 'docker exec %s htpasswd -%s /etc/nginx/htpasswd/default %s %s', EE_PROXY_TYPE, $flags, $auth->username, $auth->password ) );
}
} elseif ( write_htpasswd_file( 'default', $auths ) ) {
// Admin tools prefer default_admin_tools, so drop it only once `default` is written; on failure both files keep protecting.
$this->fs->remove( EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/default_admin_tools' );
}

$sites = array_unique(
Expand All @@ -283,34 +280,11 @@ private function generate_global_auth_files() {
);

foreach ( $sites as $site ) {
$this->generate_site_auth_files( $site );
}
}
}

/**
* Generates auth files for a site
*
* @param string $site_url URL of site
*
* @throws Exception
*/
private function generate_site_auth_files( string $site_url ) {
$site_auth_file = EE_ROOT_DIR . '/services/nginx-proxy/htpasswd/' . $site_url;
$this->fs->remove( $site_auth_file );

$auths = array_merge(
Auth::get_global_auths(),
Auth::where( 'site_url', $site_url )
);

foreach ( $auths as $key => $auth ) {
$flags = 'b';

if ( $key === 0 ) {
$flags = 'bc';
// The global files were handled above.
if ( ! is_reserved_proxy_file_name( $site ) ) {
generate_site_auth_files( $site, \EE\Model\Site::find( $site ) ?: null );
}
}
EE::exec( sprintf( 'docker exec %s htpasswd -%s /etc/nginx/htpasswd/%s %s %s', EE_PROXY_TYPE, $flags, $site_url, $auth->username, $auth->password ) );
}
}

Expand All @@ -320,7 +294,7 @@ private function generate_site_auth_files( string $site_url ) {
* @throws Exception
*/
private function generate_global_whitelist() {
$this->generate_site_whitelist( 'default' );
generate_site_whitelist( 'default' );

$sites = array_unique(
array_column(
Expand All @@ -333,54 +307,11 @@ private function generate_global_whitelist() {
}

foreach ( $sites as $site ) {
$this->generate_site_whitelist( $site );
generate_site_whitelist( $site, \EE\Model\Site::find( $site ) ?: null );
}

}

/**
* Generates site whitelist files
*
* @param string $site_url
*
* @throws Exception
*/
private function generate_site_whitelist( string $site_url ) {
$site_whitelist_file = EE_ROOT_DIR . '/services/nginx-proxy/vhost.d/' . $site_url . '_acl';
$this->fs->remove( $site_whitelist_file );

$whitelists = array_column(
'default' === $site_url ? Whitelist::get_global_ips() :
array_merge(
Whitelist::get_global_ips(),
Whitelist::where( 'site_url', $site_url )
),
'ip'
);

$this->put_ips_to_file( $site_whitelist_file, $whitelists );
}

/**
* Function to put list of ip's into a file.
*
* @param string $file Path of file to write ip's in.
* @param array $ips List of ip's.
*/
private function put_ips_to_file( string $file, array $ips ) {

if ( empty( $ips ) ) {
return;
}

$file_content = 'satisfy any;' . PHP_EOL;
foreach ( $ips as $ip ) {
$file_content .= "allow $ip;" . PHP_EOL;
}
$file_content .= 'deny all;';
$this->fs->dumpFile( $file, $file_content );
}

/**
* Updates http authentication password for a site.
*
Expand Down Expand Up @@ -453,7 +384,7 @@ private function update_auth( array $assoc_args, string $site_url ) {
if ( 'default' === $site_url ) {
$this->generate_global_auth_files();
} else {
$this->generate_site_auth_files( $site_url );
generate_site_auth_files( $site_url, $this->site_data );
}

EE::log( 'Reloading global reverse proxy.' );
Expand Down Expand Up @@ -499,7 +430,7 @@ private function update_whitelist( string $site_url, string $ips ) {
if ( 'default' === $site_url ) {
$this->generate_global_whitelist();
} else {
$this->generate_site_whitelist( $site_url );
generate_site_whitelist( $site_url, $this->site_data );
}

reload_global_nginx_proxy();
Expand Down Expand Up @@ -594,7 +525,7 @@ public function delete( $args, $assoc_args ) {
if ( 'default' === $site_url ) {
$this->generate_global_auth_files();
} else {
$this->generate_site_auth_files( $site_url );
generate_site_auth_files( $site_url, $this->site_data );
}

if ( $user ) {
Expand Down Expand Up @@ -646,7 +577,7 @@ public function delete( $args, $assoc_args ) {
if ( 'default' === $site_url ) {
$this->generate_global_whitelist();
} else {
$this->generate_site_whitelist( $site_url );
generate_site_whitelist( $site_url, $this->site_data );
}

reload_global_nginx_proxy();
Expand Down
Loading