diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d8d8ffc..7034ac1d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,48 @@ # Changelog +## 20241113 +## What's Changed +* add keycloak server +* add keycloak commands (down/login/restart/up) +* add saml states (enable-saml/disable-saml) +* add new state 'prevent-super-global-replacement' + +## 20241104 +## What's Changed +* update salt urls + +## 20241010 +## What's Changed +* execute proxy command only if instance is up +* add 'Options -Indexes' to default site + +## 20240930 +## What's Changed +* add captainhook states +* longer sleep between 'docker commit' and 'settinsg premissions' +* use correct pathes during import command + +## 20240926 +## What's Changed +* php: add version 8.3 + +## 20240902 +## What's Changed +* add support for ilias 10 exports +* also export branches with no matching doil repo + +## 20240807 +## What's Changed +* change path for minion_master.pub + +## 20240806 +## What's Changed +* move nodejs step before composer step and change execution dir in nodejs state + +## 20240801 +## What's Changed +* fix problems with the ILIAS database password + ## 20240628 ## What's Changed * use Salt Repos for salt-master and salt-minion diff --git a/CI/validate-sls-files.sh b/CI/validate-sls-files.sh index 5fb5f084..03678d51 100755 --- a/CI/validate-sls-files.sh +++ b/CI/validate-sls-files.sh @@ -21,9 +21,11 @@ do TMPFILE="${FILE}.tmp" touch $TMPFILE - echo "---" > ${TMPFILE} + echo "---" > ${TMPFILE} sed 's/{%.*%}//' ${FILE} >> ${TMPFILE} - sed 's/{{.*}}/bar/' ${TMPFILE} > ${TMPFILE}.tmp + sed '/%[A-Z].*%/d' ${TMPFILE} >> ${TMPFILE}.1 + sed 's/{{.*}}/bar/' ${TMPFILE}.1 > ${TMPFILE}.tmp + rm ${TMPFILE}.1 mv ${TMPFILE}.tmp ${TMPFILE} TEST=$(yamllint -c ./CI/sls-lint-rules.yml ${FILE}.tmp) diff --git a/README.md b/README.md index 2ead4a87..95964f91 100644 --- a/README.md +++ b/README.md @@ -308,8 +308,8 @@ users, so make sure to understand what you are doing. * `doil salt:login` logs the user into the main salt server * `doil salt:prune` prunes the main salt server -* `doil salt:start` starts the salt main server -* `doil salt:stop` stops the salt main server +* `doil salt:up` starts the salt main server +* `doil salt:down` stops the salt main server * `doil salt:restart` restarts the salt main server * `doil salt:states` to list the available states @@ -324,8 +324,8 @@ users, so make sure to understand what you are doing. * `doil proxy:login` logs the user into the proxy server * `doil proxy:prune` removes the configuration of the proxy server -* `doil proxy:start` starts the proxy server -* `doil proxy:stop` stops the proxy server +* `doil proxy:up` starts the proxy server +* `doil proxy:down` stops the proxy server * `doil proxy:restart` restarts the proxy server * `doil proxy:reload` reloads the configuration @@ -348,6 +348,7 @@ The state also sets up a cron job that regularly renews the certificates. After that please ensure to run `doil apply enable-https` on each doil ILIAS instance, so https take effect in ILIAS. + ### Mail Server The mailserver is available at `http://doil/mails` with following @@ -372,10 +373,63 @@ users, so make sure to understand what you are doing. * `doil mail:change-password` changes the default password for roundcube * `doil mail:login` logs the user into the mail server -* `doil mail:start` starts the mail server -* `doil mail:stop` stops the mail server +* `doil mail:up` starts the mail server +* `doil mail:down` stops the mail server * `doil mail:restart` restarts the mail server +### Keycloak Server + +The Keycloak server is an identity provider that allows you to log in to all +ILIAS instances managed by **doil** with one password. +This requires some settings in the doil.conf file. 'doil.conf' can be found +under setup/conf/doil.conf. The adjustments must be made before an update/install. + +The following settings are available: + +* `enable_keycloak=[true/false]` decides whether keycloak is installed during +an update/install [default:false] +* `keycloak_hostname=http://doil/keycloak` keycloak url, please pay attention to https/http +* `keycloak_new_admin_password=12345` admin password +* `keycloak_old_admin_password=admin` If the password is changed during an update, the old +password must be entered here. Please make sure to adjust it after the update. For the first +installation this has to be 'admin'. +* `keycloak_db_username=admin` database user name +* `keycloak_db_password=admin` database user password + +If you use keycloak, the salt state enable-saml must be called for existing ILIAS instances. +This is done using the 'doil apply ' command. +Newly created instances check whether keycloak is enabled and set up the instance directly. + +In order to use SAML for an Ilias instance, it must be ensured that a user is created in the +ILIAS interface and a user in the Keycloak interface. + +#### Create a user in Keycloak +* select tab 'users' from left menu +* click 'Add user' +* enter a Username +* enter an Email +* click 'Create' + +#### Cretae a user in ILIAS +* select tab 'Administration' from left menu +* select 'Users and Roles' +* select 'User Management' +* click 'Add User' +* fill in the required fields (username must be the same as in keycloak) +* set 'External Account' to the same email as in keycloak + +To be able to dive deeper into the inner workings of **doil** or customize it +to fit your workflow or requirements, **doil** provides commands to tamper with +the keycloak in the background. These commands will not be required by ordinary +users, so make sure to understand what you are doing. + +* `doil keycloak:login` logs the user into the keycloak server +* `doil keycloak:up` starts the keycloak server +* `doil keycloak:down` stops the keycloak server +* `doil keycloak:restart` restarts the keycloak server + +See `doil keycloak: --help` for more information + ### xdedug **doil** provides two options to enable xdebug for the given instance. @@ -450,4 +504,12 @@ If doil saved your precious time and brain power, please consider supporting publicly. * Reach out to [Richard](richard.klees@concepts-and-training.de) if you need more support than we can offer for free or want to get involved with **doil** - in other ways. \ No newline at end of file + in other ways. + +### Prevent Super Globals Replacement +Since ILIAS version 8 it is necessary to set the setting 'prevent_super_global_replacement = 1' in the +client.ini.php. **doil** offers a state for this. +```bash +doil apply prevent-super-global-replacement +``` +As of **doil** version 20241113, **doil** applies this state independently to newly created instances. \ No newline at end of file diff --git a/app/src/App.php b/app/src/App.php index d476909a..5022b887 100644 --- a/app/src/App.php +++ b/app/src/App.php @@ -9,7 +9,7 @@ class App extends Application { - const NAME = "Doil Version 20241010 - build 2024-10-10"; + const NAME = "Doil Version 20241113 - build 2024-11-13"; public function __construct(Command ...$commands) { diff --git a/app/src/Commands/Instances/ApplyCommand.php b/app/src/Commands/Instances/ApplyCommand.php index b3454cd1..f37e890c 100644 --- a/app/src/Commands/Instances/ApplyCommand.php +++ b/app/src/Commands/Instances/ApplyCommand.php @@ -33,7 +33,8 @@ class ApplyCommand extends Command "reactor", "change-roundcube-password", "nodejs", - "proxy-enable-https" + "proxy-enable-https", + "keycloak" ]; protected static $defaultName = "instances:apply"; diff --git a/app/src/Commands/Instances/CreateCommand.php b/app/src/Commands/Instances/CreateCommand.php index ddb7917b..c65ded68 100644 --- a/app/src/Commands/Instances/CreateCommand.php +++ b/app/src/Commands/Instances/CreateCommand.php @@ -31,18 +31,20 @@ class CreateCommand extends Command protected const GLOBAL_REPO_PATH = "/usr/local/share/doil/repositories"; protected const LOCAL_INSTANCES_PATH = "/.doil/instances"; protected const GLOBAL_INSTANCES_PATH = "/usr/local/share/doil/instances"; + protected const KEYCLOAK_PATH = "/usr/local/lib/doil/server/keycloak"; protected const BASIC_FOLDERS = [ "/conf", "/conf/salt", "/volumes/db", "/volumes/index", "/volumes/data", + "/volumes/cert", "/volumes/logs/error", "/volumes/logs/apache", "/volumes/etc/apache2", "/volumes/logs/xdebug", "/volumes/etc/php", - "/volumes/etc/mysql", + "/volumes/etc/mysql" ]; protected static $defaultName = "instances:create"; @@ -103,12 +105,14 @@ public function execute(InputInterface $input, OutputInterface $output) : int { $options = $this->gatherOptionData($input, $output); + $host = explode("=", $this->filesystem->getLineInFile("/etc/doil/doil.conf", "host"))[1]; $instance_path = $options["target"] . "/" . $options["name"]; $suffix = $options["global"] ? "global" : "local"; $instance_name = $options["name"] . "_" . $suffix; $instance_salt_name = $options["name"] . "." . $suffix; $user_name = $this->posix->getCurrentUserName(); $home_dir = $this->posix->getHomeDirectory($this->posix->getUserId()); + $keycloak = false; if ($this->filesystem->exists($instance_path)) { $this->writer->error( @@ -127,6 +131,10 @@ public function execute(InputInterface $input, OutputInterface $output) : int return Command::FAILURE; } + if ($this->filesystem->exists(self::KEYCLOAK_PATH)) { + $keycloak = true; + } + $this->writer->beginBlock($output, "Creating instance " . $options['name']); if (isset($options["repo_path"]) && ! $this->filesystem->exists($options["repo_path"])) { @@ -254,6 +262,11 @@ public function execute(InputInterface $input, OutputInterface $output) : int "%TPL_PROJECT_DOMAINNAME%", $suffix ); + $this->filesystem->replaceStringInFile( + $instance_path . "/docker-compose.yml", + "%TPL_HOST_DOMAIN%", + $host + ); $this->writer->endBlock(); // building minion image @@ -261,6 +274,7 @@ public function execute(InputInterface $input, OutputInterface $output) : int $this->docker->runContainer($instance_name); $usr_id = (string) $this->posix->getUserId(); $group_id = (string) $this->posix->getGroupId(); + $this->docker->executeDockerCommand($instance_name, "mkdir -p /var/ilias/cert"); $this->docker->executeDockerCommand($instance_name, "usermod -u $usr_id www-data"); $this->docker->executeDockerCommand($instance_name, "groupmod -g $group_id www-data"); $this->docker->executeDockerCommand($instance_name, "/etc/init.d/mariadb start"); @@ -286,22 +300,33 @@ public function execute(InputInterface $input, OutputInterface $output) : int // set grains $this->writer->beginBlock($output, "Setting up instance configuration"); $mysql_password = $this->generatePassword(16); + $cron_password = "not-needed"; if ($ilias_version < 9) { $cron_password = $this->generatePassword(16); } - $host = explode("=", $this->filesystem->getLineInFile("/etc/doil/doil.conf", "host")); + + if ($keycloak) { + $samlpass = $this->generatePassword(33); + $this->docker->setGrain($instance_salt_name, "samlpass", "$samlpass"); + sleep(1); + $samlsalt = $this->generatePassword(33); + $this->docker->setGrain($instance_salt_name, "samlsalt", "$samlsalt"); + sleep(1); + } + $this->docker->setGrain($instance_salt_name, "mpass", "$mysql_password"); sleep(1); $this->docker->setGrain($instance_salt_name, "cpass", "$cron_password"); sleep(1); - $this->docker->setGrain($instance_salt_name, "doil_domain", "http://" . $host[1] . "/" . $options["name"]); + $this->docker->setGrain($instance_salt_name, "doil_domain", "http://" . $host . "/" . $options["name"]); sleep(1); $this->docker->setGrain($instance_salt_name, "doil_project_name", $options["name"]); sleep(1); $this->docker->setGrain($instance_salt_name, "doil_host_system", "linux"); sleep(1); $this->docker->setGrain($instance_salt_name, "ilias_version", $ilias_version); + sleep(1); $this->docker->executeDockerCommand("doil_saltmain", "salt \"" . $instance_salt_name . "\" saltutil.refresh_grains"); $this->writer->endBlock(); @@ -378,6 +403,20 @@ public function execute(InputInterface $input, OutputInterface $output) : int $this->docker->applyState($instance_salt_name, "enable-captainhook"); $this->writer->endBlock(); + if ($ilias_version >= 8.0) { + // apply prevent_super_global_replacement state + $this->writer->beginBlock($output, "Apply prevent_super_global_replacement state"); + $this->docker->applyState($instance_salt_name, "prevent-super-global-replacement"); + $this->writer->endBlock(); + } + + if ($keycloak) { + // apply enable-saml state + $this->writer->beginBlock($output, "Apply enable-saml state"); + $this->docker->applyState($instance_salt_name, "enable-saml"); + $this->writer->endBlock(); + } + // apply access state $this->writer->beginBlock($output, "Apply access state"); $this->docker->applyState($instance_salt_name, "access"); diff --git a/app/src/Commands/Instances/DeleteCommand.php b/app/src/Commands/Instances/DeleteCommand.php index 72cbffa2..35ef8247 100644 --- a/app/src/Commands/Instances/DeleteCommand.php +++ b/app/src/Commands/Instances/DeleteCommand.php @@ -20,7 +20,7 @@ class DeleteCommand extends Command { protected const SALT_MAIN = "/usr/local/lib/doil/server/salt/"; protected const POSTFIX = "/usr/local/lib/doil/server/mail/"; - protected const PROXY_PATH = "/usr/local/lib/doil/server/proxy/"; + protected const KEYCLOAK_PATH = "/usr/local/lib/doil/server/keycloak"; protected static $defaultName = "instances:delete"; protected static $defaultDescription = @@ -132,6 +132,7 @@ protected function deleteInstance( ) : int { $this->writer->beginBlock($output, "Delete instance $instance"); + $is_up = $this->docker->isInstanceUp($path); $instance_dir = $this->filesystem->readLink($path); $this->filesystem->remove($path); $this->filesystem->remove($instance_dir); @@ -139,13 +140,20 @@ protected function deleteInstance( $this->docker->removeContainer($instance . "_" . $suffix); $this->docker->executeCommand(self::SALT_MAIN, "doil_saltmain", "salt-key", "-d", "$instance.$suffix", "-y", "-q"); - if ($this->docker->isInstanceUp($path)) { + if ($is_up) { $this->docker->executeDockerCommand( "doil_proxy", "rm -f /etc/nginx/conf.d/sites/$instance.conf && /root/generate_index_html.sh" ); } + if ($this->filesystem->exists(self::KEYCLOAK_PATH)) { + $this->docker->executeDockerCommand( + "doil_keycloak", + "/root/delete_keycloak_client.sh $instance" + ); + } + if ($this->docker->hasVolume($instance)) { $this->docker->removeVolume($instance); } diff --git a/app/src/Commands/Instances/StatusCommand.php b/app/src/Commands/Instances/StatusCommand.php index 6ade7358..62bffb48 100644 --- a/app/src/Commands/Instances/StatusCommand.php +++ b/app/src/Commands/Instances/StatusCommand.php @@ -37,6 +37,7 @@ public function execute(InputInterface $input, OutputInterface $output) : int strstr($a, "doil_mail") || strstr($a, "doil_proxy") || strstr($a, "doil_saltmain") || + strstr($a, "doil_keycloak") || strstr($a, "_local") || strstr($a, "_global") ) { diff --git a/app/src/Commands/Keycloak/DownCommand.php b/app/src/Commands/Keycloak/DownCommand.php new file mode 100644 index 00000000..81695e7b --- /dev/null +++ b/app/src/Commands/Keycloak/DownCommand.php @@ -0,0 +1,54 @@ + - Extended GPL, see LICENSE */ + +namespace CaT\Doil\Commands\Keycloak; + +use CaT\Doil\Lib\Docker\Docker; +use CaT\Doil\Lib\ConsoleOutput\Writer; +use CaT\Doil\Lib\FileSystem\Filesystem; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class DownCommand extends Command +{ + protected const KEYCLOAK_PATH = "/usr/local/lib/doil/server/keycloak"; + + protected static $defaultName = "keycloak:down"; + protected static $defaultDescription = "Stops the keycloak server"; + + protected Docker $docker; + protected Writer $writer; + protected Filesystem $filesystem; + + public function __construct(Docker $docker, Writer $writer, Filesystem $filesystem) + { + $this->docker = $docker; + $this->writer = $writer; + $this->filesystem = $filesystem; + + parent::__construct(); + } + + protected function configure() : void + { + if (!$this->filesystem->exists(self::KEYCLOAK_PATH)) { + $this->setHidden(true); + } + } + + public function execute(InputInterface $input, OutputInterface $output) : int + { + if (! $this->docker->isInstanceUp(self::KEYCLOAK_PATH)) { + $output->writeln("Nothing to do. Keycloak is already down."); + return Command::SUCCESS; + } + + $this->writer->beginBlock($output, "Stop keycloak"); + $this->docker->stopContainerByDockerCompose(self::KEYCLOAK_PATH); + $this->writer->endBlock(); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/app/src/Commands/Keycloak/LoginCommand.php b/app/src/Commands/Keycloak/LoginCommand.php new file mode 100644 index 00000000..6a9d5862 --- /dev/null +++ b/app/src/Commands/Keycloak/LoginCommand.php @@ -0,0 +1,48 @@ + - Extended GPL, see LICENSE */ + +namespace CaT\Doil\Commands\Keycloak; + +use CaT\Doil\Lib\Docker\Docker; +use CaT\Doil\Lib\FileSystem\Filesystem; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class LoginCommand extends Command +{ + protected const KEYCLOAK_PATH = "/usr/local/lib/doil/server/keycloak"; + + protected static $defaultName = "keycloak:login"; + protected static $defaultDescription = "Login into the keycloak server"; + + protected Docker $docker; + protected Filesystem $filesystem; + + public function __construct(Docker $docker, Filesystem $filesystem) + { + $this->docker = $docker; + $this->filesystem = $filesystem; + + parent::__construct(); + } + + protected function configure() : void + { + if (!$this->filesystem->exists(self::KEYCLOAK_PATH)) { + $this->setHidden(true); + } + } + + + public function execute(InputInterface $input, OutputInterface $output) : int + { + if (! $this->docker->isInstanceUp(self::KEYCLOAK_PATH)) { + $this->docker->startContainerByDockerCompose(self::KEYCLOAK_PATH); + } + + $this->docker->loginIntoContainer(self::KEYCLOAK_PATH, "doil_keycloak"); + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/app/src/Commands/Keycloak/RestartCommand.php b/app/src/Commands/Keycloak/RestartCommand.php new file mode 100644 index 00000000..1d4362dc --- /dev/null +++ b/app/src/Commands/Keycloak/RestartCommand.php @@ -0,0 +1,55 @@ + - Extended GPL, see LICENSE */ + +namespace CaT\Doil\Commands\Keycloak; + +use CaT\Doil\Lib\Docker\Docker; +use CaT\Doil\Lib\ConsoleOutput\Writer; +use CaT\Doil\Lib\FileSystem\Filesystem; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class RestartCommand extends Command +{ + protected const KEYCLOAK_PATH = "/usr/local/lib/doil/server/keycloak"; + + protected static $defaultName = "keycloak:restart"; + protected static $defaultDescription = "Restarts the keycloak server"; + + protected Docker $docker; + protected Writer $writer; + protected Filesystem $filesystem; + + public function __construct(Docker $docker, Writer $writer, Filesystem $filesystem) + { + $this->docker = $docker; + $this->writer = $writer; + $this->filesystem = $filesystem; + + parent::__construct(); + } + + protected function configure() : void + { + if (!$this->filesystem->exists(self::KEYCLOAK_PATH)) { + $this->setHidden(true); + } + } + + public function execute(InputInterface $input, OutputInterface $output) : int + { + if (! $this->docker->isInstanceUp(self::KEYCLOAK_PATH)) { + $this->docker->startContainerByDockerCompose(self::KEYCLOAK_PATH); + return Command::SUCCESS; + } + + $this->writer->beginBlock($output, "Restart keycloak"); + $this->docker->stopContainerByDockerCompose(self::KEYCLOAK_PATH); + $this->docker->startContainerByDockerCompose(self::KEYCLOAK_PATH); + $this->writer->endBlock(); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/app/src/Commands/Keycloak/UpCommand.php b/app/src/Commands/Keycloak/UpCommand.php new file mode 100644 index 00000000..2744ee66 --- /dev/null +++ b/app/src/Commands/Keycloak/UpCommand.php @@ -0,0 +1,56 @@ + - Extended GPL, see LICENSE */ + +namespace CaT\Doil\Commands\Keycloak; + +use CaT\Doil\Lib\Docker\Docker; +use CaT\Doil\Lib\ConsoleOutput\Writer; +use CaT\Doil\Lib\FileSystem\Filesystem; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; + +class UpCommand extends Command +{ + protected const KEYCLOAK_PATH = "/usr/local/lib/doil/server/keycloak"; + + protected static $defaultName = "keycloak:up"; + protected static $defaultDescription = "Starts the keycloak server"; + + protected Docker $docker; + protected Writer $writer; + protected Filesystem $filesystem; + + public function __construct(Docker $docker, Writer $writer, Filesystem $filesystem) + { + $this->docker = $docker; + $this->writer = $writer; + $this->filesystem = $filesystem; + + parent::__construct(); + } + + protected function configure() : void + { + if (!$this->filesystem->exists(self::KEYCLOAK_PATH)) { + $this->setHidden(true); + } + } + + public function execute(InputInterface $input, OutputInterface $output) : int + { + if ($this->docker->isInstanceUp(self::KEYCLOAK_PATH)) { + $output->writeln("Nothing to do. Keycloak is already up."); + return Command::SUCCESS; + } + + $this->writer->beginBlock($output, "Start keycloak"); + + $this->docker->startContainerByDockerCompose(self::KEYCLOAK_PATH); + + $this->writer->endBlock(); + + return Command::SUCCESS; + } +} \ No newline at end of file diff --git a/app/src/Commands/Pack/ImportCommand.php b/app/src/Commands/Pack/ImportCommand.php index e6488832..d3081ee1 100644 --- a/app/src/Commands/Pack/ImportCommand.php +++ b/app/src/Commands/Pack/ImportCommand.php @@ -9,6 +9,7 @@ use CaT\Doil\Lib\Posix\Posix; use CaT\Doil\Lib\Docker\Docker; use CaT\Doil\Lib\ProjectConfig; +use CaT\Doil\Lib\ILIAS\IliasInfo; use CaT\Doil\Lib\ConsoleOutput\Writer; use CaT\Doil\Commands\Repo\RepoManager; use CaT\Doil\Lib\FileSystem\Filesystem; @@ -24,6 +25,7 @@ class ImportCommand extends Command { + protected const KEYCLOAK_PATH = "/usr/local/lib/doil/server/keycloak"; protected static $defaultName = "pack:import"; protected static $defaultDescription = "With this command doil is able to import an archive of doilpack into an ILIAS installation. " . @@ -36,13 +38,15 @@ class ImportCommand extends Command protected Filesystem $filesystem; protected RepoManager $repo_manager; protected Writer $writer; + protected IliasInfo $ilias_info; public function __construct( Docker $docker, Posix $posix, Filesystem $filesystem, RepoManager $repo_manager, - Writer $writer + Writer $writer, + IliasInfo $ilias_info ) { parent::__construct(); @@ -51,6 +55,7 @@ public function __construct( $this->filesystem = $filesystem; $this->repo_manager = $repo_manager; $this->writer = $writer; + $this->ilias_info = $ilias_info; } public function configure() : void @@ -318,6 +323,21 @@ public function execute(InputInterface $input, OutputInterface $output) : int sleep(10); + $ilias_version = $this->ilias_info->getIliasVersion($path); + if ($ilias_version >= 8.0) { + // apply prevent_super_global_replacement state + $this->writer->beginBlock($output, "Apply prevent_super_global_replacement state"); + $this->docker->applyState($instance . "." . $suffix, "prevent-super-global-replacement"); + $this->writer->endBlock(); + } + + if ($this->filesystem->exists(self::KEYCLOAK_PATH)) { + // apply enable-saml state + $this->writer->beginBlock($output, "Apply enable-saml state"); + $this->docker->applyState($instance . "." . $suffix, "enable-saml"); + $this->writer->endBlock(); + } + $this->writer->beginBlock($output, "Setting permissions"); $this->docker->applyState($instance . "." . $suffix, "access"); $this->writer->endBlock(); diff --git a/app/src/Commands/Pack/PackCreateCommand.php b/app/src/Commands/Pack/PackCreateCommand.php index 90d08b96..e5990a90 100644 --- a/app/src/Commands/Pack/PackCreateCommand.php +++ b/app/src/Commands/Pack/PackCreateCommand.php @@ -31,12 +31,14 @@ class PackCreateCommand extends Command protected const GLOBAL_REPO_PATH = "/usr/local/share/doil/repositories"; protected const LOCAL_INSTANCES_PATH = "/.doil/instances"; protected const GLOBAL_INSTANCES_PATH = "/usr/local/share/doil/instances"; + protected const KEYCLOAK_PATH = "/usr/local/lib/doil/server/keycloak"; protected const BASIC_FOLDERS = [ "/conf", "/conf/salt", "/volumes/db", "/volumes/index", "/volumes/data", + "/volumes/cert", "/volumes/logs/error", "/volumes/logs/apache", "/volumes/etc/apache2", @@ -104,12 +106,14 @@ public function execute(InputInterface $input, OutputInterface $output) : int { $options = $this->gatherOptionData($input, $output); + $host = explode("=", $this->filesystem->getLineInFile("/etc/doil/doil.conf", "host"))[1]; $instance_path = $options["target"] . "/" . $options["name"]; $suffix = $options["global"] ? "global" : "local"; $instance_name = $options["name"] . "_" . $suffix; $instance_salt_name = $options["name"] . "." . $suffix; $user_name = $this->posix->getCurrentUserName(); $home_dir = $this->posix->getHomeDirectory($this->posix->getUserId()); + $keycloak = false; if ($this->filesystem->exists($instance_path)) { $this->writer->error( @@ -128,6 +132,10 @@ public function execute(InputInterface $input, OutputInterface $output) : int return Command::FAILURE; } + if ($this->filesystem->exists(self::KEYCLOAK_PATH)) { + $keycloak = true; + } + $this->writer->beginBlock($output, "Creating instance " . $options['name']); if (isset($options["repo_path"]) && ! $this->filesystem->exists($options["repo_path"])) { @@ -255,6 +263,11 @@ public function execute(InputInterface $input, OutputInterface $output) : int "%TPL_PROJECT_DOMAINNAME%", $suffix ); + $this->filesystem->replaceStringInFile( + $instance_path . "/docker-compose.yml", + "%TPL_HOST_DOMAIN%", + $host + ); $this->writer->endBlock(); // building minion image @@ -291,12 +304,20 @@ public function execute(InputInterface $input, OutputInterface $output) : int if ($ilias_version < 9) { $cron_password = $this->generatePassword(16); } - $host = explode("=", $this->filesystem->getLineInFile("/etc/doil/doil.conf", "host")); + + if ($keycloak) { + $samlpass = $this->generatePassword(33); + $samlsalt = $this->generatePassword(33); + $this->docker->setGrain($instance_salt_name, "samlpass", "$samlpass"); + $this->docker->setGrain($instance_salt_name, "samlsalt", "$samlsalt"); + sleep(1); + } + $this->docker->setGrain($instance_salt_name, "mpass", "${mysql_password}"); sleep(1); $this->docker->setGrain($instance_salt_name, "cpass", "${cron_password}"); sleep(1); - $doil_domain = "http://" . $host[1] . "/" . $options["name"]; + $doil_domain = "http://" . $host . "/" . $options["name"]; $this->docker->setGrain($instance_salt_name, "doil_domain", "${doil_domain}"); sleep(1); $this->docker->setGrain($instance_salt_name, "doil_project_name", "${options['name']}"); diff --git a/app/src/Lib/Docker/DockerShell.php b/app/src/Lib/Docker/DockerShell.php index 22d48435..f97738e3 100644 --- a/app/src/Lib/Docker/DockerShell.php +++ b/app/src/Lib/Docker/DockerShell.php @@ -7,25 +7,29 @@ use CaT\Doil\Lib\Posix\Posix; use CaT\Doil\Lib\SymfonyShell; use CaT\Doil\Lib\Logger\LoggerFactory; +use CaT\Doil\Lib\FileSystem\Filesystem; class DockerShell implements Docker { use SymfonyShell; protected const SALT = "/usr/local/lib/doil/server/salt"; + protected const KEYCLOAK = "/usr/local/lib/doil/server/keycloak"; protected const PROXY = "/usr/local/lib/doil/server/proxy"; protected const MAIL = "/usr/local/lib/doil/server/mail"; - public function __construct(LoggerFactory $logger, Posix $posix) + public function __construct(LoggerFactory $logger, Posix $posix, Filesystem $filesystem) { $this->logger = $logger; $this->posix = $posix; + $this->filesystem = $filesystem; } protected array $systems = [ self::SALT, self::PROXY, - self::MAIL + self::MAIL, + self::KEYCLOAK ]; public function startContainerByDockerCompose(string $path) : void @@ -575,6 +579,9 @@ protected function startDoilSystemsIfNeeded() : void if (! $this->isInstanceUp(self::SALT)) { $this->startContainerByDockerComposeWithForceRecreate(self::SALT); } + if ($this->filesystem->exists(self::KEYCLOAK . "/docker-compose.yml") && ! $this->isInstanceUp(self::KEYCLOAK)) { + $this->startContainerByDockerComposeWithForceRecreate(self::KEYCLOAK); + } if (! $this->isInstanceUp(self::PROXY)) { $this->startContainerByDockerComposeWithForceRecreate(self::PROXY); } diff --git a/app/src/cli.php b/app/src/cli.php index f53b5e19..b5bb858b 100644 --- a/app/src/cli.php +++ b/app/src/cli.php @@ -12,6 +12,7 @@ use CaT\Doil\Commands\Proxy; use CaT\Doil\Lib\Git\GitShell; use CaT\Doil\Lib\ProjectConfig; +use CaT\Doil\Commands\Keycloak; use CaT\Doil\Commands\Instances; use CaT\Doil\Lib\ILIAS\IliasInfo; use CaT\Doil\Lib\Posix\PosixShell; @@ -46,6 +47,10 @@ function buildContainerForApp() : Container $c["command.instances.restart"], $c["command.instances.status"], $c["command.instances.up"], + $c["command.keycloak.down"], + $c["command.keycloak.login"], + $c["command.keycloak.restart"], + $c["command.keycloak.up"], $c["command.mail.change.password"], $c["command.mail.down"], $c["command.mail.login"], @@ -83,7 +88,8 @@ function buildContainerForApp() : Container $c["docker.shell"] = function($c) { return new DockerShell( $c["logger"], - $c["posix.shell"] + $c["posix.shell"], + $c["filesystem.shell"] ); }; @@ -249,6 +255,37 @@ function buildContainerForApp() : Container ); }; + $c["command.keycloak.down"] = function($c) { + return new Keycloak\DownCommand( + $c["docker.shell"], + $c["command.writer"], + $c["filesystem.shell"] + ); + }; + + $c["command.keycloak.login"] = function($c) { + return new Keycloak\LoginCommand( + $c["docker.shell"], + $c["filesystem.shell"] + ); + }; + + $c["command.keycloak.restart"] = function($c) { + return new Keycloak\RestartCommand( + $c["docker.shell"], + $c["command.writer"], + $c["filesystem.shell"] + ); + }; + + $c["command.keycloak.up"] = function($c) { + return new Keycloak\UpCommand( + $c["docker.shell"], + $c["command.writer"], + $c["filesystem.shell"] + ); + }; + $c["command.mail.change.password"] = function($c) { return new Mail\ChangePasswordCommand( $c["docker.shell"], @@ -303,7 +340,8 @@ function buildContainerForApp() : Container $c["posix.shell"], $c["filesystem.shell"], $c["repo.manager"], - $c["command.writer"] + $c["command.writer"], + $c["ilias.info"] ); }; diff --git a/app/tests/Commands/Instances/CreateCommandTest.php b/app/tests/Commands/Instances/CreateCommandTest.php index a381f175..36f99e1d 100644 --- a/app/tests/Commands/Instances/CreateCommandTest.php +++ b/app/tests/Commands/Instances/CreateCommandTest.php @@ -330,9 +330,9 @@ public function test_execute() : void ; $filesystem - ->expects($this->exactly(3)) + ->expects($this->exactly(4)) ->method("exists") - ->willReturn(false, true, true) + ->willReturn(false, true, false, true) ; $filesystem ->expects($this->once()) diff --git a/app/tests/Commands/Instances/DeleteCommandTest.php b/app/tests/Commands/Instances/DeleteCommandTest.php index 27d22399..7cc456e6 100644 --- a/app/tests/Commands/Instances/DeleteCommandTest.php +++ b/app/tests/Commands/Instances/DeleteCommandTest.php @@ -160,10 +160,9 @@ public function test_execute() : void $command->setApplication($app); $filesystem - ->expects($this->exactly(1)) + ->expects($this->exactly(2)) ->method("exists") - ->with("/usr/local/share/doil/instances/master") - ->willReturn(true) + ->willReturn(true, false) ; $posix diff --git a/app/tests/Commands/Pack/ImportCommandTest.php b/app/tests/Commands/Pack/ImportCommandTest.php index 3be4ba31..b21c78fa 100644 --- a/app/tests/Commands/Pack/ImportCommandTest.php +++ b/app/tests/Commands/Pack/ImportCommandTest.php @@ -6,6 +6,7 @@ use CaT\Doil\Lib\Posix\Posix; use PHPUnit\Framework\TestCase; use CaT\Doil\Lib\Docker\Docker; +use CaT\Doil\Lib\ILIAS\IliasInfo; use CaT\Doil\Lib\FileSystem\Filesystem; use CaT\Doil\Commands\Repo\RepoManager; use CaT\Doil\Lib\ConsoleOutput\CommandWriter; @@ -20,8 +21,9 @@ public function test_execute_without_instance_param() : void $filesystem = $this->createMock(Filesystem::class); $repo_manager = $this->createMock(RepoManager::class); $writer = new CommandWriter(); + $ilias_info = $this->createMock(IliasInfo::class); - $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer); + $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer, $ilias_info); $tester = new CommandTester($command); $this->expectException(RuntimeException::class); @@ -36,8 +38,9 @@ public function test_execute_without_package_param() : void $filesystem = $this->createMock(Filesystem::class); $repo_manager = $this->createMock(RepoManager::class); $writer = new CommandWriter(); + $ilias_info = $this->createMock(IliasInfo::class); - $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer); + $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer, $ilias_info); $tester = new CommandTester($command); $this->expectException(RuntimeException::class); @@ -52,8 +55,9 @@ public function test_execute_with_empty_package_param() : void $filesystem = $this->createMock(Filesystem::class); $repo_manager = $this->createMock(RepoManager::class); $writer = new CommandWriter(); + $ilias_info = $this->createMock(IliasInfo::class); - $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer); + $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer, $ilias_info); $tester = new CommandTester($command); $this->expectException(RuntimeException::class); @@ -68,8 +72,9 @@ public function test_execute_with_non_existing_package() : void $filesystem = $this->createMock(Filesystem::class); $repo_manager = $this->createMock(RepoManager::class); $writer = new CommandWriter(); + $ilias_info = $this->createMock(IliasInfo::class); - $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer); + $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer, $ilias_info); $tester = new CommandTester($command); $filesystem @@ -91,8 +96,9 @@ public function test_execute_with_empty_instance_param() : void $filesystem = $this->createMock(Filesystem::class); $repo_manager = $this->createMock(RepoManager::class); $writer = new CommandWriter(); + $ilias_info = $this->createMock(IliasInfo::class); - $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer); + $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer, $ilias_info); $tester = new CommandTester($command); $this->expectException(RuntimeException::class); @@ -107,8 +113,9 @@ public function test_execute_with_wrong_chars_in_instance_param() : void $filesystem = $this->createMock(Filesystem::class); $repo_manager = $this->createMock(RepoManager::class); $writer = new CommandWriter(); + $ilias_info = $this->createMock(IliasInfo::class); - $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer); + $command = new ImportCommand($docker, $posix, $filesystem, $repo_manager, $writer, $ilias_info); $tester = new CommandTester($command); $this->expectException(RuntimeException::class); diff --git a/setup/conf/doil.conf b/setup/conf/doil.conf index dd185cc6..116486d4 100755 --- a/setup/conf/doil.conf +++ b/setup/conf/doil.conf @@ -1,4 +1,10 @@ group=doil host=doil mail_password=ilias -global_instances_path=/srv/instances \ No newline at end of file +global_instances_path=/srv/instances +enable_keycloak=false +keycloak_hostname=http://doil/keycloak +keycloak_new_admin_password=admin +keycloak_old_admin_password=admin +keycloak_db_username=admin +keycloak_db_password=admin \ No newline at end of file diff --git a/setup/install.sh b/setup/install.sh index 69364d6a..9cc131ab 100755 --- a/setup/install.sh +++ b/setup/install.sh @@ -73,8 +73,8 @@ then fi doil_status_okay -doil_status_send_message "Copy doil system" -doil_system_copy_doil +doil_status_send_message "Setting up basic configuration" +doil_system_setup_config if [[ $? -ne 0 ]] then doil_status_failed @@ -82,8 +82,10 @@ then fi doil_status_okay -doil_status_send_message "Setting up basic configuration" -doil_system_setup_config +ENABLE_KEYCLOAK=$(doil_get_conf enable_keycloak) + +doil_status_send_message "Copy doil system" +doil_system_copy_doil "$ENABLE_KEYCLOAK" if [[ $? -ne 0 ]] then doil_status_failed @@ -91,7 +93,6 @@ then fi doil_status_okay - doil_status_send_message "Setting up IP" doil_system_setup_ip if [[ $? -ne 0 ]] @@ -149,6 +150,14 @@ then doil_system_install_proxyserver doil_status_okay + # start keycloak server + if [[ "$ENABLE_KEYCLOAK" == true ]] + then + doil_status_send_message "Installing keycloak server" + doil_system_install_keycloakserver + doil_status_okay + fi + # start mail server doil_status_send_message "Installing mail server" doil_system_install_mailserver diff --git a/setup/stack/config/master.cnf b/setup/stack/config/master.cnf index 7a49c248..77f5b635 100755 --- a/setup/stack/config/master.cnf +++ b/setup/stack/config/master.cnf @@ -678,6 +678,8 @@ file_roots: - /srv/salt/states/php8.3 ilias: - /srv/salt/states/ilias + keycloak: + - /srv/salt/states/keycloak compile-skins: - /srv/salt/states/compile-skins composer: @@ -712,6 +714,12 @@ file_roots: - /srv/salt/states/enable-captainhook disable-captainhook: - /srv/salt/states/disable-captainhook + enable-saml: + - /srv/salt/states/enable-saml + disable-saml: + - /srv/salt/states/disable-saml + prevent-super-global-replacement: + - /srv/salt/states/prevent-super-global-replacement # The master_roots setting configures a master-only copy of the file_roots dictionary, diff --git a/setup/stack/states/disable-https/https/init.sls b/setup/stack/states/disable-https/https/init.sls index 2ff7ddd4..1171c34d 100644 --- a/setup/stack/states/disable-https/https/init.sls +++ b/setup/stack/states/disable-https/https/init.sls @@ -13,6 +13,12 @@ rewrite_ilias_config: - watch: - pkg: apt_add_jq_tool +{%- if salt['file.file_exists']('/var/ilias/data/ilias/auth/saml/config/config.php') %} +rewrite_saml_config: + cmd.run: + - name: sed -i -e "s/'https:/'http:/g" /var/ilias/data/ilias/auth/saml/config/config.php +{%- endif %} + {% if ilias_version | int < 10 %} update_ilias_lt_10: cmd.wait: diff --git a/setup/stack/states/disable-saml/description.txt b/setup/stack/states/disable-saml/description.txt new file mode 100644 index 00000000..759144d0 --- /dev/null +++ b/setup/stack/states/disable-saml/description.txt @@ -0,0 +1 @@ +description = ATTENTION: This will delete existing SAML configurations. Disables saml login in ILIAS and removes the instance from keycloak. \ No newline at end of file diff --git a/setup/stack/states/disable-saml/saml/DeleteIdp.php.j2 b/setup/stack/states/disable-saml/saml/DeleteIdp.php.j2 new file mode 100644 index 00000000..99d9f31f --- /dev/null +++ b/setup/stack/states/disable-saml/saml/DeleteIdp.php.j2 @@ -0,0 +1,30 @@ +setLoginFormStatus(false); + +$idps = ilSamlIdp::getAllIdps(); + +if (count($idps) > 0) { + foreach ($idps as $idp) { + if ($idp->getEntityId() == "{{ keycloak_host_name }}/realms/master") { + $factory = new ilSamlAuthFactory(); + $samlAuth = $factory->auth(); + $idpDisco = $samlAuth->getIdpDiscovery(); + $idpDisco->deleteIdpMetadata($idp->getIdpId()); + $idp->delete(); + } + } +} + + diff --git a/setup/stack/states/disable-saml/saml/deleteInstanceFromKeycloak.php.j2 b/setup/stack/states/disable-saml/saml/deleteInstanceFromKeycloak.php.j2 new file mode 100644 index 00000000..1e21cf77 --- /dev/null +++ b/setup/stack/states/disable-saml/saml/deleteInstanceFromKeycloak.php.j2 @@ -0,0 +1,44 @@ + 0) { + curl_reset($ch); + $id = array_pop($client)["id"]; + $delete_url = $KC_HOST . "/admin/realms/master/clients/" . $id; + curl_setopt($ch, CURLOPT_URL, $delete_url); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE"); + curl_exec($ch); +} \ No newline at end of file diff --git a/setup/stack/states/disable-saml/saml/init.sls b/setup/stack/states/disable-saml/saml/init.sls new file mode 100644 index 00000000..1bf28c08 --- /dev/null +++ b/setup/stack/states/disable-saml/saml/init.sls @@ -0,0 +1,73 @@ +{% set doil_domain = salt['grains.get']('doil_domain') %} +{% set ilias_version = salt['grains.get']('ilias_version', '9') %} + +/var/ilias/data/ilias/auth/saml/config/authsources.php: + file.absent + +/var/ilias/data/ilias/auth/saml/config/config.php: + file.absent + +/var/ilias/cert/saml.crt: + file.absent + +/var/ilias/cert/saml.pem: + file.absent + +{% if ilias_version | int < 10 %} +/var/www/html/DeleteIdp.php: + file.managed: + - source: salt://saml/DeleteIdp.php.j2 + - template: jinja + - context: + ilias_http_path: {{ doil_domain }} + keycloak_host_name: %TPL_KEYCLOAK_HOSTNAME% + - user: root + - group: root + - mode: 644 + +delete_ilias_idp_lt10: + cmd.run: + - name: php DeleteIdp.php; rm DeleteIdp.php + - cwd: /var/www/html + - watch: + - file: /var/www/html/DeleteIdp.php +{% else %} +/var/www/html/public/DeleteIdp.php: + file.managed: + - source: salt://saml/DeleteIdp.php.j2 + - template: jinja + - context: + ilias_http_path: {{ doil_domain }} + keycloak_host_name: %TPL_KEYCLOAK_HOSTNAME% + - user: root + - group: root + - mode: 644 + +delete_ilias_idp: + cmd.run: + - name: php DeleteIdp.php; rm DeleteIdp.php + - cwd: /var/www/html/public + - watch: + - file: /var/www/html/public/DeleteIdp.php +{% endif %} + + +/root/deleteInstanceFromKeycloak.php: + file.managed: + - source: salt://saml/deleteInstanceFromKeycloak.php.j2 + - template: jinja + - context: + server_host_name: %TPL_KEYCLOAK_HOSTNAME% + admin_password: %TPL_ADMIN_PASSWORD% + doil_domain: {{ doil_domain }} + - user: root + - group: root + - mode: 744 + +deleteInstanceFromKeycloak: + cmd.run: + - name: php deleteInstanceFromKeycloak.php + - cwd: /root + - runas: root + - require: + - file: /root/deleteInstanceFromKeycloak.php \ No newline at end of file diff --git a/setup/stack/states/disable-saml/top.sls b/setup/stack/states/disable-saml/top.sls new file mode 100644 index 00000000..eba26282 --- /dev/null +++ b/setup/stack/states/disable-saml/top.sls @@ -0,0 +1,3 @@ +disable-saml: + '*': + - saml diff --git a/setup/stack/states/enable-https/https/init.sls b/setup/stack/states/enable-https/https/init.sls index 51086630..8452ccb9 100644 --- a/setup/stack/states/enable-https/https/init.sls +++ b/setup/stack/states/enable-https/https/init.sls @@ -13,6 +13,12 @@ rewrite_ilias_config: - watch: - pkg: apt_add_jq_tool +{%- if salt['file.file_exists']('/var/ilias/data/ilias/auth/saml/config/config.php') %} +rewrite_saml_config: + cmd.run: + - name: sed -i -e "s/'http:/'https:/g" /var/ilias/data/ilias/auth/saml/config/config.php +{%- endif %} + {% if ilias_version | int < 10 %} update_ilias_lt_10: cmd.wait: diff --git a/setup/stack/states/enable-saml/description.txt b/setup/stack/states/enable-saml/description.txt new file mode 100644 index 00000000..eabf1bb7 --- /dev/null +++ b/setup/stack/states/enable-saml/description.txt @@ -0,0 +1 @@ +description = ATTENTION: This will overwrite potentially existing SAML configurations. Enables saml login in ILIAS and adds the instance to keycloak. \ No newline at end of file diff --git a/setup/stack/states/enable-saml/saml/SetIdp.php.j2 b/setup/stack/states/enable-saml/saml/SetIdp.php.j2 new file mode 100644 index 00000000..2150c552 --- /dev/null +++ b/setup/stack/states/enable-saml/saml/SetIdp.php.j2 @@ -0,0 +1,40 @@ +auth(); + +$idps = ilSamlIdp::getAllIdps(); +if (count($idps) > 0) { + foreach ($idps as $idp) { + if ($idp->getEntityId() == "{{ keycloak_host_name }}/realms/master") { + $idpDisco = $samlAuth->getIdpDiscovery(); + $idpDisco->deleteIdpMetadata($idp->getIdpId()); + $idp->delete(); + } + } +} + +ilSamlSettings::getInstance()->setLoginFormStatus(true); +$idp = new ilSamlIdp(); +$idp->setActive(true); +$idp->setEntityId("{{ keycloak_host_name }}/realms/master"); +$idp->setLocalLocalAuthenticationStatus(true); +$idp->setUidClaim("email"); +$idp->setSynchronizationStatus(true); +$idp->setLoginClaim("email"); +$idp->setDefaultRoleId(2); +$idp->setAccountMigrationStatus(false); +$idp->persist(); +$idpDisco = $samlAuth->getIdpDiscovery(); +$idpDisco->storeIdpMetadata($idp->getIdpId(), '{{ idp_meta }}'); diff --git a/setup/stack/states/enable-saml/saml/SetIdpV10.php.j2 b/setup/stack/states/enable-saml/saml/SetIdpV10.php.j2 new file mode 100644 index 00000000..7427aa59 --- /dev/null +++ b/setup/stack/states/enable-saml/saml/SetIdpV10.php.j2 @@ -0,0 +1,28 @@ +setLoginFormStatus(true); +$idp = new ilSamlIdp(); +$idp->setActive(true); +$idp->setEntityId("{{ keycloak_host_name }}/realms/master"); +$idp->setLocalLocalAuthenticationStatus(true); +$idp->setUidClaim("email"); +$idp->setSynchronizationStatus(true); +$idp->setLoginClaim("email"); +$idp->setDefaultRoleId(2); +$idp->setAccountMigrationStatus(false) +$idp->persist(); +$factory = new ilSamlAuthFactory(); +$samlAuth = $factory->auth(); +$idpDisco = $samlAuth->getIdpDiscovery(); +$idpDisco->storeIdpMetadata($idp->getIdpId(), '{{ idp_meta }}'); diff --git a/setup/stack/states/enable-saml/saml/addInstanceToKeycloak.php.j2 b/setup/stack/states/enable-saml/saml/addInstanceToKeycloak.php.j2 new file mode 100644 index 00000000..52daa5b4 --- /dev/null +++ b/setup/stack/states/enable-saml/saml/addInstanceToKeycloak.php.j2 @@ -0,0 +1,120 @@ + false,\n"; + if ($value) { + $replacement = "\t'admin.protectmetadata' => true,\n"; + } + $data = file("/var/ilias/data/ilias/auth/saml/config/config.php"); + $data = array_map(function($d) use ($replacement) { + return strpos($d, "admin.protectmetadata") ? $replacement : $d; + }, $data); + + file_put_contents("/var/ilias/data/ilias/auth/saml/config/config.php", $data); +} + +$KC_HOST = "{{ server_host_name }}"; + +$ch = curl_init(); + +// get the token for the keycloak rest api +$token_url = $KC_HOST . "/realms/master/protocol/openid-connect/token"; +$auth_data = "client_id=admin-cli&username=admin&password={{ admin_password }}&grant_type=password"; +curl_setopt($ch, CURLOPT_URL, $token_url); +curl_setopt($ch, CURLOPT_POSTFIELDS, $auth_data); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +$response = curl_exec($ch); +$access_token = json_decode($response, true)["access_token"]; +$header = [ + "Authorization: bearer " . $access_token, + "content-type: application/json" +]; + +curl_reset($ch); + +// add a client-scope to keycloak if not already exists +$client_scope_url = $KC_HOST . "/admin/realms/master/client-scopes"; +curl_setopt($ch, CURLOPT_URL, $client_scope_url); +curl_setopt($ch, CURLOPT_HTTPHEADER, $header); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +$response = json_decode(curl_exec($ch), true); +$scope_exists = array_filter($response, function($a) { + if ($a["name"] == "MapEmail") { + return true; + } + return false; +}); +if (count($scope_exists) == 0) { + curl_reset($ch); + $scope_json = '{"name":"MapEmail","description":"","protocol":"saml","attributes":{"include.in.token.scope":"false","display.on.consent.screen":"true","gui.order":"","consent.screen.text":""},"protocolMappers":[{"name":"X500 email","protocol":"saml","protocolMapper":"saml-user-property-mapper","consentRequired":false,"config":{"attribute.nameformat":"Basic","user.attribute":"email","friendly.name":"email","attribute.name":"email"}}]}'; + + curl_setopt($ch, CURLOPT_URL, $client_scope_url); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_POSTFIELDS, $scope_json); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_exec($ch); +} + +curl_reset($ch); + +// get the saml metadata for the instance that should be added +$meta_url = "{{ meta_url }}"; +$converter_url = $KC_HOST . "/admin/realms/master/client-description-converter"; +curl_setopt($ch, CURLOPT_URL, $meta_url); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +setProtectMetaData(false); +$xml = curl_exec($ch); +setProtectMetaData(true); + +curl_reset($ch); + +// convert the saml metadata to json, so keycloak can work with +curl_setopt($ch, CURLOPT_URL, $converter_url); +curl_setopt($ch, CURLOPT_HTTPHEADER, $header); +curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +$json = curl_exec($ch); + +// add the field "defaultClientScopes" to the metadata +$meta_json = json_decode($json, true); +$client_id = $meta_json["clientId"]; +$meta_json["defaultClientScopes"] = ["MapEmail"]; +$meta_json = json_encode($meta_json); + +curl_reset($ch); + +// delete potentially existing client +$clients_url = $KC_HOST . "/admin/realms/master/clients"; +curl_setopt($ch, CURLOPT_URL, $clients_url); +curl_setopt($ch, CURLOPT_HTTPHEADER, $header); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +$response = curl_exec($ch); +$clients = json_decode($response, true); +$client = array_filter($clients, function($a) use ($client_id) { + if ($a["clientId"] == $client_id) { + return true; + } + return false; +}); +if (count($client) > 0) { + curl_reset($ch); + $id = array_pop($client)["id"]; + $delete_url = $KC_HOST . "/admin/realms/master/clients/" . $id; + curl_setopt($ch, CURLOPT_URL, $delete_url); + curl_setopt($ch, CURLOPT_HTTPHEADER, $header); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE"); + curl_exec($ch); +} + +curl_reset($ch); + +// add the client to keycloak +curl_setopt($ch, CURLOPT_URL, $clients_url); +curl_setopt($ch, CURLOPT_HTTPHEADER, $header); +curl_setopt($ch, CURLOPT_POSTFIELDS, $meta_json); +curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); +curl_exec($ch); + +curl_close($ch); diff --git a/setup/stack/states/enable-saml/saml/authsources.php.j2 b/setup/stack/states/enable-saml/saml/authsources.php.j2 new file mode 100644 index 00000000..a8de08d9 --- /dev/null +++ b/setup/stack/states/enable-saml/saml/authsources.php.j2 @@ -0,0 +1,16 @@ + array( + 'core:AdminPassword', + ), + 'default-sp' => array( + 'RelayState' => '{{ ilias_http_path }}/saml.php', + 'saml:SP', + 'privatekey' => 'saml.pem', + 'certificate' => 'saml.crt', + 'entityID' => '{{ ilias_http_path }}/module.php/saml/sp/metadata.php/default-sp', + 'idp' => $idpentityid, + 'discoURL' => NULL, + ), +); diff --git a/setup/stack/states/enable-saml/saml/config.php.j2 b/setup/stack/states/enable-saml/saml/config.php.j2 new file mode 100644 index 00000000..a20943e7 --- /dev/null +++ b/setup/stack/states/enable-saml/saml/config.php.j2 @@ -0,0 +1,109 @@ + 'xml', 'file' => $file]; +} + +$config = [ + 'baseurlpath' => '{{ ilias_http_path }}/', + 'certdir' => '/var/ilias/cert/', + 'loggingdir' => '/var/ilias/logs', + 'datadir' => 'data/', + 'application' => [ 'baseURL' => '{{ ilias_http_path }}/' ], + 'tempdir' => '/tmp/simplesaml', + 'debug' => null, + 'showerrors' => true, + 'errorreporting' => true, + 'debug.validatexml' => false, + 'auth.adminpassword' => '{{ samlpass }}', + 'admin.protectindexpage' => true, + 'admin.protectmetadata' => false, + 'secretsalt' => '{{ samlsecretsalt }}', + 'technicalcontact_name' => 'Administrator', + 'technicalcontact_email' => 'root@ilias.de', + 'timezone' => null, + 'logging.level' => SimpleSAML\Logger::DEBUG, + 'logging.handler' => 'file', + 'logging.facility' => defined('LOG_LOCAL5') ? constant('LOG_LOCAL5') : LOG_USER, + 'logging.processname' => 'simplesamlphp', + 'logging.logfile' => 'simplesamlphp.log', + 'statistics.out' => [], + 'enable.saml20-idp' => false, + 'enable.shib13-idp' => false, + 'enable.adfs-idp' => false, + 'enable.wsfed-sp' => false, + 'enable.authmemcookie' => false, + 'session.duration' => 8 * (60 * 60), // 8 hours. + 'session.datastore.timeout' => (4 * 60 * 60), // 4 hours + 'session.state.timeout' => (60 * 60), // 1 hour + 'session.cookie.name' => 'SimpleSAMLSessionID', + 'session.cookie.lifetime' => 0, + 'session.cookie.path' => '/{{ host_name }}/', + 'session.cookie.domain' => null, + 'session.cookie.secure' => false, + 'session.disable_fallback' => false, + 'enable.http_post' => false, + 'session.phpsession.cookiename' => 'SAMLSESSID', + 'session.phpsession.savepath' => null, + 'session.phpsession.httponly' => false, + 'session.authtoken.cookiename' => 'SimpleSAMLAuthToken', + 'session.rememberme.enable' => false, + 'session.rememberme.checked' => false, + 'session.rememberme.lifetime' => (14 * 86400), + 'language.available' => [ + 'en', 'no', 'nn', 'se', 'da', 'de', 'sv', 'fi', 'es', 'fr', 'it', 'nl', 'lb', 'cs', + 'sl', 'lt', 'hr', 'hu', 'pl', 'pt', 'pt-br', 'tr', 'ja', 'zh', 'zh-tw', 'ru', 'et', + 'he', 'id', 'sr', 'lv', 'ro', 'eu' + ], + 'language.rtl' => ['ar', 'dv', 'fa', 'ur', 'he'], + 'language.default' => 'en', + 'language.parameter.name' => 'language', + 'language.parameter.setcookie' => true, + 'language.cookie.name' => 'language', + 'language.cookie.domain' => null, + 'language.cookie.path' => '/', + 'language.cookie.lifetime' => (60 * 60 * 24 * 900), + 'attributes.extradictionary' => null, + 'theme.use' => 'default', + 'default-wsfed-idp' => 'urn:federation:pingfederate:localhost', + 'idpdisco.enableremember' => true, + 'idpdisco.rememberchecked' => true, + 'idpdisco.validate' => true, + 'idpdisco.extDiscoveryStorage' => null, + 'idpdisco.layout' => 'dropdown', + 'shib13.signresponse' => true, + 'authproc.idp' => [ + 30 => 'core:LanguageAdaptor', + 45 => [ + 'class' => 'core:StatisticsWithAttribute', + 'attributename' => 'realm', + 'type' => 'saml20-idp-SSO', + ], + 50 => 'core:AttributeLimit', + 99 => 'core:LanguageAdaptor', + ], + 'authproc.sp' => [ + 90 => 'core:LanguageAdaptor', + ], + 'metadata.sources' => $metadataSources, + 'store.type' => 'sql', + 'store.sql.dsn' => 'mysql:host=127.0.0.1;dbname=ilias', + 'store.sql.username' => "ilias", + 'store.sql.password' => '{{ mpass }}', + 'store.sql.prefix' => 'simpleSAMLphp', + 'memcache_store.servers' => [ + [ + ['hostname' => 'localhost'], + ], + ], + 'memcache_store.expires' => 36 * (60 * 60), + 'metadata.sign.enable' => false, + 'metadata.sign.privatekey' => null, + 'metadata.sign.privatekey_pass' => null, + 'metadata.sign.certificate' => null, + 'proxy' => null, + 'trusted.url.domains' => null, +]; diff --git a/setup/stack/states/enable-saml/saml/init.sls b/setup/stack/states/enable-saml/saml/init.sls new file mode 100644 index 00000000..743b6a8c --- /dev/null +++ b/setup/stack/states/enable-saml/saml/init.sls @@ -0,0 +1,110 @@ +{% set doil_domain = salt['grains.get']('doil_domain') %} +{% set host_name = salt['grains.get']('host') %} +{% set mpass = salt['grains.get']('mpass') %} +{% set samlpass = salt['grains.get']('samlpass', 'abcdef123456!!$') %} +{% set samlsalt = salt['grains.get']('samlsalt', 'mc5tbaeuwn8mpxfx07sxq2wv2vi4utsw') %} +{% set ilias_version = salt['grains.get']('ilias_version', '9') %} +{% if ilias_version | int < 10 %} + {% set meta_url = '/Services/Saml/lib/metadata.php?client_id=ilias' %} +{% else %} + {% set meta_url = '/metadata.php?client_id=ilias' %} +{% endif %} + +/var/ilias/data/ilias/auth/saml/config/authsources.php: + file.managed: + - source: salt://saml/authsources.php.j2 + - makedirs: True + - template: jinja + - context: + ilias_http_path: {{ doil_domain }} + - user: root + - group: root + - mode: 644 + +/var/ilias/data/ilias/auth/saml/config/config.php: + file.managed: + - source: salt://saml/config.php.j2 + - makedirs: True + - template: jinja + - context: + ilias_http_path: {{ doil_domain }} + samlsecretsalt: {{ samlsalt }} + mpass: {{ mpass }} + samlpass: {{ samlpass }} + host_name: {{ host_name }} + - user: root + - group: root + - mode: 644 + +/var/ilias/cert: + file.directory: + - user: www-data + - group: www-data + - dir_mode: 755 + +install_certs: + cmd.run: + - name: openssl req -newkey rsa:3072 -new -x509 -days 3652 -nodes -out saml.crt -keyout saml.pem -batch && chown -R www-data:www-data . + - cwd: /var/ilias/cert + - require: + - file: /var/ilias/cert + +{% if ilias_version | int < 10 %} +/var/www/html/SetIdp.php: + file.managed: + - source: salt://saml/SetIdp.php.j2 + - template: jinja + - context: + ilias_http_path: {{ doil_domain }} + idp_meta: %TPL_IDP_META% + keycloak_host_name: %TPL_KEYCLOAK_HOSTNAME% + - user: root + - group: root + - mode: 644 + +init_ilias_idp_lt10: + cmd.run: + - name: php SetIdp.php; rm SetIdp.php + - cwd: /var/www/html + - require: + - file: /var/www/html/SetIdp.php +{% else %} +/var/www/html/public/SetIdpV10.php: + file.managed: + - source: salt://saml/SetIdpV10.php.j2 + - template: jinja + - context: + ilias_http_path: {{ doil_domain }} + idp_meta: %TPL_IDP_META% + keycloak_host_name: %TPL_KEYCLOAK_HOSTNAME% + - user: root + - group: root + - mode: 644 + +init_ilias_idp: + cmd.run: + - name: php SetIdpV10.php; rm SetIdpV10.php + - cwd: /var/www/html/public + - require: + - file: /var/www/html/public/SetIdpV10.php +{% endif %} + +/root/addInstanceToKeycloak.php: + file.managed: + - source: salt://saml/addInstanceToKeycloak.php.j2 + - template: jinja + - context: + server_host_name: %TPL_KEYCLOAK_HOSTNAME% + admin_password: %TPL_ADMIN_PASSWORD% + meta_url: {{ doil_domain }}{{ meta_url }} + - user: root + - group: root + - mode: 744 + +addInstanceToKeycloak: + cmd.run: + - name: php addInstanceToKeycloak.php + - cwd: /root + - runas: root + - require: + - file: /root/addInstanceToKeycloak.php diff --git a/setup/stack/states/enable-saml/top.sls b/setup/stack/states/enable-saml/top.sls new file mode 100644 index 00000000..542181b9 --- /dev/null +++ b/setup/stack/states/enable-saml/top.sls @@ -0,0 +1,3 @@ +enable-saml: + '*': + - saml diff --git a/setup/stack/states/ilias/ilias/init.sls b/setup/stack/states/ilias/ilias/init.sls index 0bdc59f6..8ade947c 100755 --- a/setup/stack/states/ilias/ilias/init.sls +++ b/setup/stack/states/ilias/ilias/init.sls @@ -15,6 +15,13 @@ - mode: 755 - makedirs: True +/var/ilias/cert: + file.directory: + - user: www-data + - group: www-data + - mode: 755 + - makedirs: True + /var/ilias/logs: file.directory: - user: www-data diff --git a/setup/stack/states/keycloak/keycloak/delete_keycloak_client.sh.j2 b/setup/stack/states/keycloak/keycloak/delete_keycloak_client.sh.j2 new file mode 100644 index 00000000..e84019cc --- /dev/null +++ b/setup/stack/states/keycloak/keycloak/delete_keycloak_client.sh.j2 @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +/opt/keycloak/bin/kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password {{ new_admin_password }} +KEYCLOAK_CLIENT_ID=$(/opt/keycloak/bin/kcadm.sh get clients | jq ".[] | select(.clientId|test(\".*/doil/${1}/.*\")).id" | tr -d '"') +if [ "${KEYCLOAK_CLIENT_ID}" == "" ] +then + exit 0 +fi +/opt/keycloak/bin/kcadm.sh delete clients/"${KEYCLOAK_CLIENT_ID}" \ No newline at end of file diff --git a/setup/stack/states/keycloak/keycloak/init.sls b/setup/stack/states/keycloak/keycloak/init.sls new file mode 100644 index 00000000..37c9d57a --- /dev/null +++ b/setup/stack/states/keycloak/keycloak/init.sls @@ -0,0 +1,31 @@ +# salt 'doil.keycloak' state.highstate saltenv=keycloak +# salt-run state.event pretty=True + +init_keycloak_api: + cmd.run: + - name: /opt/keycloak/bin/kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password %TPL_OLD_ADMIN_PASSWORD% + +change_admin_password: + cmd.run: + - name: /opt/keycloak/bin/kcadm.sh set-password -r master --username admin --new-password %TPL_NEW_ADMIN_PASSWORD% + +# Please leave this in as an example for default user creation +# {% if salt["cmd.run"]("/opt/keycloak/bin/kcadm.sh get users -r master -q q=username:doil") == "[ ]" %} +# create_doil_user: +# cmd.run: +# - name: /opt/keycloak/bin/kcadm.sh create users -r master -s username=doil -s enabled=true +# {% endif %} +# +# change_doil_password: +# cmd.run: +# - name: /opt/keycloak/bin/kcadm.sh set-password -r master --username doil --new-password %TPL_USR_PASSWORD% + +/root/delete_keycloak_client.sh: + file.managed: + - source: salt://keycloak/delete_keycloak_client.sh.j2 + - template: jinja + - context: + new_admin_password: %TPL_NEW_ADMIN_PASSWORD% + - user: root + - group: root + - mode: 744 \ No newline at end of file diff --git a/setup/stack/states/keycloak/top.sls b/setup/stack/states/keycloak/top.sls new file mode 100644 index 00000000..7e4a8274 --- /dev/null +++ b/setup/stack/states/keycloak/top.sls @@ -0,0 +1,3 @@ +keycloak: + '*': + - keycloak diff --git a/setup/stack/states/prevent-super-global-replacement/description.txt b/setup/stack/states/prevent-super-global-replacement/description.txt new file mode 100644 index 00000000..9fe64fbb --- /dev/null +++ b/setup/stack/states/prevent-super-global-replacement/description.txt @@ -0,0 +1 @@ +description = Add 'prevent_super_global_replacement = "1"' to the ILIAS client.ini.php. Only applicable to ILIAS version >= 8. \ No newline at end of file diff --git a/setup/stack/states/prevent-super-global-replacement/prevent-super-global-replacement/init.sls b/setup/stack/states/prevent-super-global-replacement/prevent-super-global-replacement/init.sls new file mode 100755 index 00000000..53093f6e --- /dev/null +++ b/setup/stack/states/prevent-super-global-replacement/prevent-super-global-replacement/init.sls @@ -0,0 +1,17 @@ +{% set ilias_version = salt['grains.get']('ilias_version', '9') %} + +{% if ilias_version | int >= 8 %} +{% if ilias_version | int < 10 %} +/var/www/html/data/ilias/client.ini.php: + file.blockreplace: + - marker_start: '[server]' + - marker_end: '[client]' + - content: "start = \"./login.php\"\nprevent_super_global_replacement = \"1\"\n\n" +{% else %} +/var/www/html/public/data/ilias/client.ini.php: + file.blockreplace: + - marker_start: '[server]' + - marker_end: '[client]' + - content: "start = \"./login.php\"\nprevent_super_global_replacement = \"1\"\n\n" +{% endif %} +{% endif %} \ No newline at end of file diff --git a/setup/stack/states/prevent-super-global-replacement/top.sls b/setup/stack/states/prevent-super-global-replacement/top.sls new file mode 100755 index 00000000..b82be02b --- /dev/null +++ b/setup/stack/states/prevent-super-global-replacement/top.sls @@ -0,0 +1,3 @@ +prevent-super-global-replacement: + '*': + - prevent-super-global-replacement diff --git a/setup/system.sh b/setup/system.sh index 6cc92f34..6dc6ca98 100644 --- a/setup/system.sh +++ b/setup/system.sh @@ -69,7 +69,6 @@ function doil_system_remove() { if [ ! -z ${ALL} ] then GLOBAL_INSTANCES_PATH=$(doil_get_conf global_instances_path) - HOST=$(doil_get_conf host) if [ -d "${GLOBAL_INSTANCES_PATH}" ] then rm -rf "${GLOBAL_INSTANCES_PATH}" @@ -130,7 +129,7 @@ function doil_system_remove_networks() { } function doil_system_remove_volumes() { - docker volume prune -f >/dev/null 2>&1 + docker volume prune -af >/dev/null 2>&1 } function doil_system_remove_user_doil_folders() { @@ -138,6 +137,7 @@ function doil_system_remove_user_doil_folders() { } function doil_system_remove_hosts_entry() { + HOST=$(doil_get_conf "host=") sed -i "/172.24.0.254 ${HOST}/d" /etc/hosts } @@ -206,6 +206,17 @@ function doil_system_create_folder() { } function doil_system_copy_doil() { + enable_keycloak="$1" + if [[ "$enable_keycloak" == true ]] + then + cp -r ${SCRIPT_DIR}/templates/keycloak /usr/local/lib/doil/server/ + else + if [ -d /usr/local/lib/doil/server/keycloak ] + then + rm -rf /usr/local/lib/doil/server/keycloak + fi + fi + cp ${SCRIPT_DIR}/doil.sh /usr/local/bin/doil cp -r ${SCRIPT_DIR}/templates/mail /usr/local/lib/doil/server/ cp -r ${SCRIPT_DIR}/templates/proxy /usr/local/lib/doil/server/ @@ -246,7 +257,7 @@ function doil_system_setup_ip() { IPEXIST=$(grep "172.24.0.254" /etc/hosts) if [[ -z ${IPEXIST} ]] then - HOST=$(doil_get_conf host) + HOST=$(doil_get_conf "host=") printf "172.24.0.254 ${HOST}" >> "/etc/hosts" fi return 0 @@ -354,12 +365,47 @@ function doil_system_install_saltserver() { docker commit doil_saltmain doil_saltmain:stable 2>&1 > /var/log/doil/stream.log } +function doil_system_install_keycloakserver() { + cd /usr/local/lib/doil/server/keycloak + + KEYCLOAK_HOSTNAME=$(printf '%s\n' "$(doil_get_conf keycloak_hostname)" | sed -e 's/[\/&]/\\&/g') + KEYCLOAK_NEW_ADMIN_PASSWORD=$(doil_get_conf keycloak_new_admin_password) + KEYCLOAK_OLD_ADMIN_PASSWORD=$(doil_get_conf keycloak_old_admin_password) + # Please leave this in as an example for default user creation + # KEYCLOAK_USR_PASSWORD=$(doil_get_conf keycloak_usr_password) + KEYCLOAK_DB_USERNAME=$(doil_get_conf keycloak_db_username) + KEYCLOAK_DB_PASSWORD=$(doil_get_conf keycloak_db_password) + + sed -i "s/%TPL_SERVER_HOSTNAME%/${KEYCLOAK_HOSTNAME}/g" "/usr/local/lib/doil/server/keycloak/conf/keycloak-startup.conf" + sed -i "s/%TPL_DB_USERNAME%/${KEYCLOAK_DB_USERNAME}/g" "/usr/local/lib/doil/server/keycloak/conf/keycloak-startup.conf" + sed -i "s/%TPL_DB_PASSWORD%/${KEYCLOAK_DB_PASSWORD}/g" "/usr/local/lib/doil/server/keycloak/conf/keycloak-startup.conf" + + sed -i "s/%TPL_DB_USERNAME%/${KEYCLOAK_DB_USERNAME}/g" "/usr/local/lib/doil/server/keycloak/conf/init.sql" + sed -i "s/%TPL_DB_PASSWORD%/${KEYCLOAK_DB_PASSWORD}/g" "/usr/local/lib/doil/server/keycloak/conf/init.sql" + + sed -i "s/%TPL_NEW_ADMIN_PASSWORD%/${KEYCLOAK_NEW_ADMIN_PASSWORD}/g" "/usr/local/share/doil/stack/states/keycloak/keycloak/init.sls" + sed -i "s/%TPL_OLD_ADMIN_PASSWORD%/${KEYCLOAK_OLD_ADMIN_PASSWORD}/g" "/usr/local/share/doil/stack/states/keycloak/keycloak/init.sls" + # Please leave this in as an example for default user creation + # sed -i "s/%TPL_USR_PASSWORD%/${KEYCLOAK_USR_PASSWORD}/g" "/usr/local/share/doil/stack/states/keycloak/keycloak/init.sls" + + BUILD=$(docker compose up -d 2>&1 > /var/log/doil/stream.log) 2>&1 > /var/log/doil/stream.log + sleep 120 + docker exec -i doil_saltmain bash -c "salt 'doil.keycloak' state.highstate saltenv=keycloak" 2>&1 > /var/log/doil/stream.log + docker commit doil_keycloak doil_keycloak:stable 2>&1 > /var/log/doil/stream.log + IDP_META=$(docker exec -i doil_saltmain bash -c "salt 'doil.keycloak' http.query http://localhost:8080/realms/master/protocol/saml/descriptor --out=raw | cut -d \"'\" -f6") + sed -i "s|%TPL_IDP_META%|${IDP_META}|g" "/usr/local/share/doil/stack/states/enable-saml/saml/init.sls" + sed -i "s|%TPL_KEYCLOAK_HOSTNAME%|${KEYCLOAK_HOSTNAME}|g" "/usr/local/share/doil/stack/states/enable-saml/saml/init.sls" + sed -i "s/%TPL_ADMIN_PASSWORD%/${KEYCLOAK_NEW_ADMIN_PASSWORD}/g" "/usr/local/share/doil/stack/states/enable-saml/saml/init.sls" + sed -i "s|%TPL_KEYCLOAK_HOSTNAME%|${KEYCLOAK_HOSTNAME}|g" "/usr/local/share/doil/stack/states/disable-saml/saml/init.sls" + sed -i "s/%TPL_ADMIN_PASSWORD%/${KEYCLOAK_NEW_ADMIN_PASSWORD}/g" "/usr/local/share/doil/stack/states/disable-saml/saml/init.sls" +} + function doil_system_install_proxyserver() { cd /usr/local/lib/doil/server/proxy - NAME=$(cat /etc/doil/doil.conf | grep "host" | cut -d '=' -f 2-) + NAME=$(printf '%s\n' $(doil_get_conf host=) | sed -e 's/[\/&]/\\&/g') sed -i "s/%TPL_SERVER_NAME%/${NAME}/g" "/usr/local/lib/doil/server/proxy/conf/nginx/local.conf" BUILD=$(docker compose up -d 2>&1 > /var/log/doil/stream.log) 2>&1 > /var/log/doil/stream.log - sleep 10 + sleep 20 docker exec -i doil_saltmain bash -c "salt 'doil.proxy' state.highstate saltenv=proxyservices" 2>&1 > /var/log/doil/stream.log docker commit doil_proxy doil_proxy:stable 2>&1 > /var/log/doil/stream.log } @@ -367,7 +413,7 @@ function doil_system_install_proxyserver() { function doil_system_install_mailserver() { cd /usr/local/lib/doil/server/mail BUILD=$(docker compose up -d 2>&1 > /var/log/doil/stream.log) 2>&1 > /var/log/doil/stream.log - sleep 10 + sleep 20 docker exec -i doil_saltmain bash -c "salt 'doil.mail' state.highstate saltenv=mailservices" 2>&1 > /var/log/doil/stream.log PASSWORD=$(doil_get_conf mail_password) if [[ "${PASSWORD}" != "ilias" ]] diff --git a/setup/templates/keycloak/Dockerfile b/setup/templates/keycloak/Dockerfile new file mode 100644 index 00000000..1ea186cc --- /dev/null +++ b/setup/templates/keycloak/Dockerfile @@ -0,0 +1,46 @@ +FROM debian:12 + +RUN apt-get update && apt-get install -y supervisor +RUN apt-get update && apt-get install -y vim jq nano less virt-what net-tools procps curl unzip mariadb-server python3-dev default-libmysqlclient-dev build-essential pkg-config + +RUN curl -LO https://download.java.net/java/GA/jdk21/fd2272bbf8e04c3dbaee13770090416c/35/GPL/openjdk-21_linux-x64_bin.tar.gz +RUN echo "$(curl -s https://download.java.net/java/GA/jdk21/fd2272bbf8e04c3dbaee13770090416c/35/GPL/openjdk-21_linux-x64_bin.tar.gz.sha256) openjdk-21_linux-x64_bin.tar.gz" | sha256sum -c +RUN tar xzf openjdk-21_linux-x64_bin.tar.gz +RUN rm openjdk-21_linux-x64_bin.tar.gz +ENV PATH="$PATH:/jdk-21/bin" +ENV JAVA_HOME="/jdk-21" + +RUN curl -fsSL -o keycloak-25.0.5.zip https://github.com/keycloak/keycloak/releases/download/25.0.5/keycloak-25.0.5.zip +RUN echo "$(curl -fsSL https://github.com/keycloak/keycloak/releases/download/25.0.5/keycloak-25.0.5.zip.sha1) keycloak-25.0.5.zip" | sha1sum -c +RUN unzip keycloak-25.0.5.zip +RUN mv keycloak-25.0.5 /opt/keycloak +RUN rm keycloak-25.0.5.zip + +RUN curl -fsSL https://packages.broadcom.com/artifactory/api/security/keypair/SaltProjectKey/public | tee /etc/apt/keyrings/salt-archive-keyring.pgp +RUN curl -fsSL https://github.com/saltstack/salt-install-guide/releases/latest/download/salt.sources | tee /etc/apt/sources.list.d/salt.sources +RUN apt-get update && apt-get install -y salt-minion + +RUN /opt/saltstack/salt/bin/python3 -m pip install --upgrade pip +RUN /opt/saltstack/salt/bin/python3 -m pip install mysql +RUN apt-get update && apt-get remove -y python3-dev default-libmysqlclient-dev build-essential pkg-config + +COPY conf/keycloak-startup.conf /etc/supervisor/conf.d +COPY conf/mysql.conf /etc/supervisor/conf.d +COPY conf/salt-minion.conf /etc/supervisor/conf.d +COPY conf/mysql_starter.sh /root/ +COPY conf/startup.conf /etc/supervisor/conf.d +COPY conf/minion.cnf /etc/salt/minion +COPY conf/salt-startup.sh /root +COPY conf/init.sql /root + +RUN chmod +x /root/mysql_starter.sh +RUN chmod +x /root/salt-startup.sh + +COPY conf/run-supervisor.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/run-supervisor.sh + +RUN echo "keycloak:x:10001" >> /etc/group && \ + echo "keycloak:x:10001:10001:keycloak user:/opt/keycloak:/sbin/nologin" >> /etc/passwd +RUN chown -cR keycloak:keycloak /opt/keycloak + +CMD ["/usr/local/bin/run-supervisor.sh"] \ No newline at end of file diff --git a/setup/templates/keycloak/conf/init.sql b/setup/templates/keycloak/conf/init.sql new file mode 100644 index 00000000..c39e8206 --- /dev/null +++ b/setup/templates/keycloak/conf/init.sql @@ -0,0 +1,5 @@ +CREATE DATABASE dbkc; + +CREATE USER '%TPL_DB_USERNAME%'@'localhost' IDENTIFIED BY '%TPL_DB_PASSWORD%'; +GRANT ALL PRIVILEGES ON dbkc.* TO '%TPL_DB_USERNAME%'@'localhost'; +FLUSH PRIVILEGES; \ No newline at end of file diff --git a/setup/templates/keycloak/conf/keycloak-startup.conf b/setup/templates/keycloak/conf/keycloak-startup.conf new file mode 100755 index 00000000..6c4b1d13 --- /dev/null +++ b/setup/templates/keycloak/conf/keycloak-startup.conf @@ -0,0 +1,8 @@ +[program:keycloak] +command=./opt/keycloak/bin/kc.sh start --hostname %TPL_SERVER_HOSTNAME% --http-enabled true +autostart=true +autorestart=false +user=keycloak +startsecs=0 +priority=200 +environment=KC_HOSTNAME="%TPL_SERVER_HOSTNAME%",KEYCLOAK_ADMIN="admin",KEYCLOAK_ADMIN_PASSWORD="admin",KC_DB="mariadb",KC_DB_USERNAME="%TPL_DB_USERNAME%",KC_DB_PASSWORD="%TPL_DB_PASSWORD%",KC_DB_URL="jdbc:mariadb://localhost:3306/dbkc?characterEncoding=UTF-8" \ No newline at end of file diff --git a/setup/templates/keycloak/conf/minion.cnf b/setup/templates/keycloak/conf/minion.cnf new file mode 100755 index 00000000..22d5555e --- /dev/null +++ b/setup/templates/keycloak/conf/minion.cnf @@ -0,0 +1,888 @@ +##### Primary configuration settings ##### +########################################## +# This configuration file is used to manage the behavior of the Salt Minion. +# With the exception of the location of the Salt Master Server, values that are +# commented out but have an empty line after the comment are defaults that need +# not be set in the config. If there is no blank line after the comment, the +# value is presented as an example and is not the default. + +# Per default the minion will automatically include all config files +# from minion.d/*.conf (minion.d is a directory in the same directory +# as the main minion config file). +#default_include: minion.d/*.conf + +# Set the location of the salt master server. If the master server cannot be +# resolved, then the minion will fail to start. +master: 172.20.0.254 + +# Set http proxy information for the minion when doing requests +#proxy_host: +#proxy_port: +#proxy_username: +#proxy_password: + +# If multiple masters are specified in the 'master' setting, the default behavior +# is to always try to connect to them in the order they are listed. If random_master is +# set to True, the order will be randomized instead. This can be helpful in distributing +# the load of many minions executing salt-call requests, for example, from a cron job. +# If only one master is listed, this setting is ignored and a warning will be logged. +# NOTE: If master_type is set to failover, use master_shuffle instead. +#random_master: False + +# Use if master_type is set to failover. +#master_shuffle: False + +# Minions can connect to multiple masters simultaneously (all masters +# are "hot"), or can be configured to failover if a master becomes +# unavailable. Multiple hot masters are configured by setting this +# value to "str". Failover masters can be requested by setting +# to "failover". MAKE SURE TO SET master_alive_interval if you are +# using failover. +# Setting master_type to 'disable' let's you have a running minion (with engines and +# beacons) without a master connection +# master_type: str + +# Poll interval in seconds for checking if the master is still there. Only +# respected if master_type above is "failover". To disable the interval entirely, +# set the value to -1. (This may be necessary on machines which have high numbers +# of TCP connections, such as load balancers.) +# master_alive_interval: 30 + +# If the minion is in multi-master mode and the master_type configuration option +# is set to "failover", this setting can be set to "True" to force the minion +# to fail back to the first master in the list if the first master is back online. +#master_failback: False + +# If the minion is in multi-master mode, the "master_type" configuration is set to +# "failover", and the "master_failback" option is enabled, the master failback +# interval can be set to ping the top master with this interval, in seconds. +#master_failback_interval: 0 + +# Set whether the minion should connect to the master via IPv6: +#ipv6: False + +# Set the number of seconds to wait before attempting to resolve +# the master hostname if name resolution fails. Defaults to 30 seconds. +# Set to zero if the minion should shutdown and not retry. +# retry_dns: 30 + +# Set the number of times to attempt to resolve +# the master hostname if name resolution fails. Defaults to None, +# which will attempt the resolution indefinitely. +# retry_dns_count: 3 + +# Set the port used by the master reply and authentication server. +#master_port: 4506 + +# The user to run salt. +#user: root + +# The user to run salt remote execution commands as via sudo. If this option is +# enabled then sudo will be used to change the active user executing the remote +# command. If enabled the user will need to be allowed access via the sudoers +# file for the user that the salt minion is configured to run as. The most +# common option would be to use the root user. If this option is set the user +# option should also be set to a non-root user. If migrating from a root minion +# to a non root minion the minion cache should be cleared and the minion pki +# directory will need to be changed to the ownership of the new user. +#sudo_user: root + +# Specify the location of the daemon process ID file. +#pidfile: /var/run/salt-minion.pid + +# The root directory prepended to these options: pki_dir, cachedir, log_file, +# sock_dir, pidfile. +#root_dir: / + +# The path to the minion's configuration file. +#conf_file: /etc/salt/minion + +# The directory to store the pki information in +#pki_dir: /etc/salt/pki/minion + +# Explicitly declare the id for this minion to use, if left commented the id +# will be the hostname as returned by the python call: socket.getfqdn() +# Since salt uses detached ids it is possible to run multiple minions on the +# same machine but with different ids, this can be useful for salt compute +# clusters. +#id: salt-minion-test + +# Cache the minion id to a file when the minion's id is not statically defined +# in the minion config. Defaults to "True". This setting prevents potential +# problems when automatic minion id resolution changes, which can cause the +# minion to lose connection with the master. To turn off minion id caching, +# set this config to ``False``. +#minion_id_caching: True + +# Append a domain to a hostname in the event that it does not exist. This is +# useful for systems where socket.getfqdn() does not actually result in a +# FQDN (for instance, Solaris). +#append_domain: + +# Custom static grains for this minion can be specified here and used in SLS +# files just like all other grains. This example sets 4 custom grains, with +# the 'roles' grain having two values that can be matched against. +#grains: +# roles: +# - webserver +# - memcache +# deployment: datacenter4 +# cabinet: 13 +# cab_u: 14-15 +# +# Where cache data goes. +# This data may contain sensitive data and should be protected accordingly. +#cachedir: /var/cache/salt/minion + +# Append minion_id to these directories. Helps with +# multiple proxies and minions running on the same machine. +# Allowed elements in the list: pki_dir, cachedir, extension_modules +# Normally not needed unless running several proxies and/or minions on the same machine +# Defaults to ['cachedir'] for proxies, [] (empty list) for regular minions +#append_minionid_config_dirs: + +# Verify and set permissions on configuration directories at startup. +#verify_env: True + +# The minion can locally cache the return data from jobs sent to it, this +# can be a good way to keep track of jobs the minion has executed +# (on the minion side). By default this feature is disabled, to enable, set +# cache_jobs to True. +#cache_jobs: False + +# Set the directory used to hold unix sockets. +#sock_dir: /var/run/salt/minion + +# The minion can take a while to start up when lspci and/or dmidecode is used +# to populate the grains for the minion. Set this to False if you do not need +# GPU hardware grains for your minion. +# enable_gpu_grains: True + +# Set the default outputter used by the salt-call command. The default is +# "nested". +#output: nested + +# To set a list of additional directories to search for salt outputters, set the +# outputter_dirs option. +#outputter_dirs: [] + +# By default output is colored. To disable colored output, set the color value +# to False. +#color: True + +# Do not strip off the colored output from nested results and state outputs +# (true by default). +# strip_colors: False + +# Backup files that are replaced by file.managed and file.recurse under +# 'cachedir'/file_backup relative to their original location and appended +# with a timestamp. The only valid setting is "minion". Disabled by default. +# +# Alternatively this can be specified for each file in state files: +# /etc/ssh/sshd_config: +# file.managed: +# - source: salt://ssh/sshd_config +# - backup: minion +# +#backup_mode: minion + +# When waiting for a master to accept the minion's public key, salt will +# continuously attempt to reconnect until successful. This is the time, in +# seconds, between those reconnection attempts. +#acceptance_wait_time: 10 + +# If this is nonzero, the time between reconnection attempts will increase by +# acceptance_wait_time seconds per iteration, up to this maximum. If this is +# set to zero, the time between reconnection attempts will stay constant. +#acceptance_wait_time_max: 0 + +# If the master rejects the minion's public key, retry instead of exiting. +# Rejected keys will be handled the same as waiting on acceptance. +#rejected_retry: False + +# When the master key changes, the minion will try to re-auth itself to receive +# the new master key. In larger environments this can cause a SYN flood on the +# master because all minions try to re-auth immediately. To prevent this and +# have a minion wait for a random amount of time, use this optional parameter. +# The wait-time will be a random number of seconds between 0 and the defined value. +#random_reauth_delay: 60 + + +# To avoid overloading a master when many minions startup at once, a randomized +# delay may be set to tell the minions to wait before connecting to the master. +# This value is the number of seconds to choose from for a random number. For +# example, setting this value to 60 will choose a random number of seconds to delay +# on startup between zero seconds and sixty seconds. Setting to '0' will disable +# this feature. +#random_startup_delay: 0 + +# When waiting for a master to accept the minion's public key, salt will +# continuously attempt to reconnect until successful. This is the timeout value, +# in seconds, for each individual attempt. After this timeout expires, the minion +# will wait for acceptance_wait_time seconds before trying again. Unless your master +# is under unusually heavy load, this should be left at the default. +#auth_timeout: 60 + +# Number of consecutive SaltReqTimeoutError that are acceptable when trying to +# authenticate. +#auth_tries: 7 + +# The number of attempts to connect to a master before giving up. +# Set this to -1 for unlimited attempts. This allows for a master to have +# downtime and the minion to reconnect to it later when it comes back up. +# In 'failover' mode, it is the number of attempts for each set of masters. +# In this mode, it will cycle through the list of masters for each attempt. +# +# This is different than auth_tries because auth_tries attempts to +# retry auth attempts with a single master. auth_tries is under the +# assumption that you can connect to the master but not gain +# authorization from it. master_tries will still cycle through all +# the masters in a given try, so it is appropriate if you expect +# occasional downtime from the master(s). +#master_tries: 1 + +# If authentication fails due to SaltReqTimeoutError during a ping_interval, +# cause sub minion process to restart. +#auth_safemode: False + +# Ping Master to ensure connection is alive (minutes). +#ping_interval: 0 + +# To auto recover minions if master changes IP address (DDNS) +# auth_tries: 10 +# auth_safemode: False +# ping_interval: 2 +# +# Minions won't know master is missing until a ping fails. After the ping fail, +# the minion will attempt authentication and likely fails out and cause a restart. +# When the minion restarts it will resolve the masters IP and attempt to reconnect. + +# If you don't have any problems with syn-floods, don't bother with the +# three recon_* settings described below, just leave the defaults! +# +# The ZeroMQ pull-socket that binds to the masters publishing interface tries +# to reconnect immediately, if the socket is disconnected (for example if +# the master processes are restarted). In large setups this will have all +# minions reconnect immediately which might flood the master (the ZeroMQ-default +# is usually a 100ms delay). To prevent this, these three recon_* settings +# can be used. +# recon_default: the interval in milliseconds that the socket should wait before +# trying to reconnect to the master (1000ms = 1 second) +# +# recon_max: the maximum time a socket should wait. each interval the time to wait +# is calculated by doubling the previous time. if recon_max is reached, +# it starts again at recon_default. Short example: +# +# reconnect 1: the socket will wait 'recon_default' milliseconds +# reconnect 2: 'recon_default' * 2 +# reconnect 3: ('recon_default' * 2) * 2 +# reconnect 4: value from previous interval * 2 +# reconnect 5: value from previous interval * 2 +# reconnect x: if value >= recon_max, it starts again with recon_default +# +# recon_randomize: generate a random wait time on minion start. The wait time will +# be a random value between recon_default and recon_default + +# recon_max. Having all minions reconnect with the same recon_default +# and recon_max value kind of defeats the purpose of being able to +# change these settings. If all minions have the same values and your +# setup is quite large (several thousand minions), they will still +# flood the master. The desired behavior is to have timeframe within +# all minions try to reconnect. +# +# Example on how to use these settings. The goal: have all minions reconnect within a +# 60 second timeframe on a disconnect. +# recon_default: 1000 +# recon_max: 59000 +# recon_randomize: True +# +# Each minion will have a randomized reconnect value between 'recon_default' +# and 'recon_default + recon_max', which in this example means between 1000ms +# 60000ms (or between 1 and 60 seconds). The generated random-value will be +# doubled after each attempt to reconnect. Lets say the generated random +# value is 11 seconds (or 11000ms). +# reconnect 1: wait 11 seconds +# reconnect 2: wait 22 seconds +# reconnect 3: wait 33 seconds +# reconnect 4: wait 44 seconds +# reconnect 5: wait 55 seconds +# reconnect 6: wait time is bigger than 60 seconds (recon_default + recon_max) +# reconnect 7: wait 11 seconds +# reconnect 8: wait 22 seconds +# reconnect 9: wait 33 seconds +# reconnect x: etc. +# +# In a setup with ~6000 thousand hosts these settings would average the reconnects +# to about 100 per second and all hosts would be reconnected within 60 seconds. +# recon_default: 100 +# recon_max: 5000 +# recon_randomize: False +# +# +# The loop_interval sets how long in seconds the minion will wait between +# evaluating the scheduler and running cleanup tasks. This defaults to 1 +# second on the minion scheduler. +#loop_interval: 1 + +# Some installations choose to start all job returns in a cache or a returner +# and forgo sending the results back to a master. In this workflow, jobs +# are most often executed with --async from the Salt CLI and then results +# are evaluated by examining job caches on the minions or any configured returners. +# WARNING: Setting this to False will **disable** returns back to the master. +#pub_ret: True + + +# The grains can be merged, instead of overridden, using this option. +# This allows custom grains to defined different subvalues of a dictionary +# grain. By default this feature is disabled, to enable set grains_deep_merge +# to ``True``. +#grains_deep_merge: False + +# The grains_refresh_every setting allows for a minion to periodically check +# its grains to see if they have changed and, if so, to inform the master +# of the new grains. This operation is moderately expensive, therefore +# care should be taken not to set this value too low. +# +# Note: This value is expressed in __minutes__! +# +# A value of 10 minutes is a reasonable default. +# +# If the value is set to zero, this check is disabled. +#grains_refresh_every: 1 + +# Cache grains on the minion. Default is False. +#grains_cache: False + +# Cache rendered pillar data on the minion. Default is False. +# This may cause 'cachedir'/pillar to contain sensitive data that should be +# protected accordingly. +#minion_pillar_cache: False + +# Grains cache expiration, in seconds. If the cache file is older than this +# number of seconds then the grains cache will be dumped and fully re-populated +# with fresh data. Defaults to 5 minutes. Will have no effect if 'grains_cache' +# is not enabled. +# grains_cache_expiration: 300 + +# Determines whether or not the salt minion should run scheduled mine updates. +# Defaults to "True". Set to "False" to disable the scheduled mine updates +# (this essentially just does not add the mine update function to the minion's +# scheduler). +#mine_enabled: True + +# Determines whether or not scheduled mine updates should be accompanied by a job +# return for the job cache. Defaults to "False". Set to "True" to include job +# returns in the job cache for mine updates. +#mine_return_job: False + +# Example functions that can be run via the mine facility +# NO mine functions are established by default. +# Note these can be defined in the minion's pillar as well. +#mine_functions: +# test.ping: [] +# network.ip_addrs: +# interface: eth0 +# cidr: '10.0.0.0/8' + +# The number of minutes between mine updates. +#mine_interval: 60 + +# Windows platforms lack posix IPC and must rely on slower TCP based inter- +# process communications. Set ipc_mode to 'tcp' on such systems +#ipc_mode: ipc + +# Overwrite the default tcp ports used by the minion when in tcp mode +#tcp_pub_port: 4510 +#tcp_pull_port: 4511 + +# Passing very large events can cause the minion to consume large amounts of +# memory. This value tunes the maximum size of a message allowed onto the +# minion event bus. The value is expressed in bytes. +#max_event_size: 1048576 + +# To detect failed master(s) and fire events on connect/disconnect, set +# master_alive_interval to the number of seconds to poll the masters for +# connection events. +# +#master_alive_interval: 30 + +# The minion can include configuration from other files. To enable this, +# pass a list of paths to this option. The paths can be either relative or +# absolute; if relative, they are considered to be relative to the directory +# the main minion configuration file lives in (this file). Paths can make use +# of shell-style globbing. If no files are matched by a path passed to this +# option then the minion will log a warning message. +# +# Include a config file from some other path: +# include: /etc/salt/extra_config +# +# Include config from several files and directories: +#include: +# - /etc/salt/extra_config +# - /etc/roles/webserver + +# The syndic minion can verify that it is talking to the correct master via the +# key fingerprint of the higher-level master with the "syndic_finger" config. +#syndic_finger: '' +# +# +# +##### Minion module management ##### +########################################## +# Disable specific modules. This allows the admin to limit the level of +# access the master has to the minion. The default here is the empty list, +# below is an example of how this needs to be formatted in the config file +#disable_modules: +# - cmdmod +# - test +#disable_returners: [] + +# This is the reverse of disable_modules. The default, like disable_modules, is the empty list, +# but if this option is set to *anything* then *only* those modules will load. +# Note that this is a very large hammer and it can be quite difficult to keep the minion working +# the way you think it should since Salt uses many modules internally itself. At a bare minimum +# you need the following enabled or else the minion won't start. +#whitelist_modules: +# - cmdmod +# - test +# - config + +# Modules can be loaded from arbitrary paths. This enables the easy deployment +# of third party modules. Modules for returners and minions can be loaded. +# Specify a list of extra directories to search for minion modules and +# returners. These paths must be fully qualified! +#module_dirs: [] +#returner_dirs: [] +#states_dirs: [] +#render_dirs: [] +#utils_dirs: [] +# +# A module provider can be statically overwritten or extended for the minion +# via the providers option, in this case the default module will be +# overwritten by the specified module. In this example the pkg module will +# be provided by the yumpkg5 module instead of the system default. +#providers: +# pkg: yumpkg5 +# +# Enable Cython modules searching and loading. (Default: False) +#cython_enable: False +# +# Specify a max size (in bytes) for modules on import. This feature is currently +# only supported on *nix operating systems and requires psutil. +# modules_max_memory: -1 + + +##### State Management Settings ##### +########################################### +# The state management system executes all of the state templates on the minion +# to enable more granular control of system state management. The type of +# template and serialization used for state management needs to be configured +# on the minion, the default renderer is yaml_jinja. This is a yaml file +# rendered from a jinja template, the available options are: +# yaml_jinja +# yaml_mako +# yaml_wempy +# json_jinja +# json_mako +# json_wempy +# +#renderer: yaml_jinja +# +# The failhard option tells the minions to stop immediately after the first +# failure detected in the state execution. Defaults to False. +#failhard: False +# +# Reload the modules prior to a highstate run. +#autoload_dynamic_modules: True +# +# clean_dynamic_modules keeps the dynamic modules on the minion in sync with +# the dynamic modules on the master, this means that if a dynamic module is +# not on the master it will be deleted from the minion. By default, this is +# enabled and can be disabled by changing this value to False. +#clean_dynamic_modules: True +# +# Normally, the minion is not isolated to any single environment on the master +# when running states, but the environment can be isolated on the minion side +# by statically setting it. Remember that the recommended way to manage +# environments is to isolate via the top file. +#environment: None +# +# Isolates the pillar environment on the minion side. This functions the same +# as the environment setting, but for pillar instead of states. +#pillarenv: None +# +# Set this option to True to force the pillarenv to be the same as the +# effective saltenv when running states. Note that if pillarenv is specified, +# this option will be ignored. +#pillarenv_from_saltenv: False +# +# Set this option to 'True' to force a 'KeyError' to be raised whenever an +# attempt to retrieve a named value from pillar fails. When this option is set +# to 'False', the failed attempt returns an empty string. Default is 'False'. +#pillar_raise_on_missing: False +# +# If using the local file directory, then the state top file name needs to be +# defined, by default this is top.sls. +#state_top: top.sls +# +# Run states when the minion daemon starts. To enable, set startup_states to: +# 'highstate' -- Execute state.highstate +# 'sls' -- Read in the sls_list option and execute the named sls files +# 'top' -- Read top_file option and execute based on that file on the Master +#startup_states: '' +# +# List of states to run when the minion starts up if startup_states is 'sls': +#sls_list: +# - edit.vim +# - hyper +# +# Top file to execute if startup_states is 'top': +#top_file: '' + +# Automatically aggregate all states that have support for mod_aggregate by +# setting to True. Or pass a list of state module names to automatically +# aggregate just those types. +# +# state_aggregate: +# - pkg +# +#state_aggregate: False + +##### File Directory Settings ##### +########################################## +# The Salt Minion can redirect all file server operations to a local directory, +# this allows for the same state tree that is on the master to be used if +# copied completely onto the minion. This is a literal copy of the settings on +# the master but used to reference a local directory on the minion. + +# Set the file client. The client defaults to looking on the master server for +# files, but can be directed to look at the local file directory setting +# defined below by setting it to "local". Setting a local file_client runs the +# minion in masterless mode. +#file_client: remote + +# The file directory works on environments passed to the minion, each environment +# can have multiple root directories, the subdirectories in the multiple file +# roots cannot match, otherwise the downloaded files will not be able to be +# reliably ensured. A base environment is required to house the top file. +# Example: +# file_roots: +# base: +# - /srv/salt/ +# dev: +# - /srv/salt/dev/services +# - /srv/salt/dev/states +# prod: +# - /srv/salt/prod/services +# - /srv/salt/prod/states +# +#file_roots: +# base: +# - /srv/salt + +# Uncomment the line below if you do not want the file_server to follow +# symlinks when walking the filesystem tree. This is set to True +# by default. Currently this only applies to the default roots +# fileserver_backend. +#fileserver_followsymlinks: False +# +# Uncomment the line below if you do not want symlinks to be +# treated as the files they are pointing to. By default this is set to +# False. By uncommenting the line below, any detected symlink while listing +# files on the Master will not be returned to the Minion. +#fileserver_ignoresymlinks: True +# +# By default, the Salt fileserver recurses fully into all defined environments +# to attempt to find files. To limit this behavior so that the fileserver only +# traverses directories with SLS files and special Salt directories like _modules, +# enable the option below. This might be useful for installations where a file root +# has a very large number of files and performance is negatively impacted. Default +# is False. +#fileserver_limit_traversal: False + +# The hash_type is the hash to use when discovering the hash of a file on +# the local fileserver. The default is sha256, but md5, sha1, sha224, sha384 +# and sha512 are also supported. +# +# WARNING: While md5 and sha1 are also supported, do not use them due to the +# high chance of possible collisions and thus security breach. +# +# Warning: Prior to changing this value, the minion should be stopped and all +# Salt caches should be cleared. +#hash_type: sha256 + +# The Salt pillar is searched for locally if file_client is set to local. If +# this is the case, and pillar data is defined, then the pillar_roots need to +# also be configured on the minion: +#pillar_roots: +# base: +# - /srv/pillar + +# Set a hard-limit on the size of the files that can be pushed to the master. +# It will be interpreted as megabytes. Default: 100 +#file_recv_max_size: 100 +# +# +###### Security settings ##### +########################################### +# Enable "open mode", this mode still maintains encryption, but turns off +# authentication, this is only intended for highly secure environments or for +# the situation where your keys end up in a bad state. If you run in open mode +# you do so at your own risk! +#open_mode: False + +# The size of key that should be generated when creating new keys. +#keysize: 2048 + +# Enable permissive access to the salt keys. This allows you to run the +# master or minion as root, but have a non-root group be given access to +# your pki_dir. To make the access explicit, root must belong to the group +# you've given access to. This is potentially quite insecure. +#permissive_pki_access: False + +# The state_verbose and state_output settings can be used to change the way +# state system data is printed to the display. By default all data is printed. +# The state_verbose setting can be set to True or False, when set to False +# all data that has a result of True and no changes will be suppressed. +#state_verbose: True + +# The state_output setting controls which results will be output full multi line +# full, terse - each state will be full/terse +# mixed - only states with errors will be full +# changes - states with changes and errors will be full +# full_id, mixed_id, changes_id and terse_id are also allowed; +# when set, the state ID will be used as name in the output +#state_output: full + +# The state_output_diff setting changes whether or not the output from +# successful states is returned. Useful when even the terse output of these +# states is cluttering the logs. Set it to True to ignore them. +#state_output_diff: False + +# The state_output_profile setting changes whether profile information +# will be shown for each state run. +#state_output_profile: True + +# Fingerprint of the master public key to validate the identity of your Salt master +# before the initial key exchange. The master fingerprint can be found by running +# "salt-key -f master.pub" on the Salt master. +#master_finger: '' + +# Use TLS/SSL encrypted connection between master and minion. +# Can be set to a dictionary containing keyword arguments corresponding to Python's +# 'ssl.wrap_socket' method. +# Default is None. +#ssl: +# keyfile: +# certfile: +# ssl_version: PROTOCOL_TLSv1_2 + +# Grains to be sent to the master on authentication to check if the minion's key +# will be accepted automatically. Needs to be configured on the master. +#autosign_grains: +# - uuid +# - server_id + + +###### Reactor Settings ##### +########################################### +# Define a salt reactor. See https://docs.saltstack.com/en/latest/topics/reactor/ +#reactor: [] + +#Set the TTL for the cache of the reactor configuration. +#reactor_refresh_interval: 60 + +#Configure the number of workers for the runner/wheel in the reactor. +#reactor_worker_threads: 10 + +#Define the queue size for workers in the reactor. +#reactor_worker_hwm: 10000 + + +###### Thread settings ##### +########################################### +# Disable multiprocessing support, by default when a minion receives a +# publication a new process is spawned and the command is executed therein. +# +# WARNING: Disabling multiprocessing may result in substantial slowdowns +# when processing large pillars. See https://github.com/saltstack/salt/issues/38758 +# for a full explanation. +#multiprocessing: True + +# Limit the maximum amount of processes or threads created by salt-minion. +# This is useful to avoid resource exhaustion in case the minion receives more +# publications than it is able to handle, as it limits the number of spawned +# processes or threads. -1 is the default and disables the limit. +#process_count_max: -1 + + +##### Logging settings ##### +########################################## +# The location of the minion log file +# The minion log can be sent to a regular file, local path name, or network +# location. Remote logging works best when configured to use rsyslogd(8) (e.g.: +# ``file:///dev/log``), with rsyslogd(8) configured for network logging. The URI +# format is: ://:/ +#log_file: /var/log/salt/minion +#log_file: file:///dev/log +#log_file: udp://loghost:10514 +# +#log_file: /var/log/salt/minion +#key_logfile: /var/log/salt/key + +# The level of messages to send to the console. +# One of 'garbage', 'trace', 'debug', info', 'warning', 'error', 'critical'. +# +# The following log levels are considered INSECURE and may log sensitive data: +# ['garbage', 'trace', 'debug'] +# +# Default: 'warning' +#log_level: warning + +# The level of messages to send to the log file. +# One of 'garbage', 'trace', 'debug', info', 'warning', 'error', 'critical'. +# If using 'log_granular_levels' this must be set to the highest desired level. +# Default: 'warning' +#log_level_logfile: + +# The date and time format used in log messages. Allowed date/time formatting +# can be seen here: http://docs.python.org/library/time.html#time.strftime +#log_datefmt: '%H:%M:%S' +#log_datefmt_logfile: '%Y-%m-%d %H:%M:%S' + +# The format of the console logging messages. Allowed formatting options can +# be seen here: http://docs.python.org/library/logging.html#logrecord-attributes +# +# Console log colors are specified by these additional formatters: +# +# %(colorlevel)s +# %(colorname)s +# %(colorprocess)s +# %(colormsg)s +# +# Since it is desirable to include the surrounding brackets, '[' and ']', in +# the coloring of the messages, these color formatters also include padding as +# well. Color LogRecord attributes are only available for console logging. +# +#log_fmt_console: '%(colorlevel)s %(colormsg)s' +#log_fmt_console: '[%(levelname)-8s] %(message)s' +# +#log_fmt_logfile: '%(asctime)s,%(msecs)03d [%(name)-17s][%(levelname)-8s] %(message)s' + +# This can be used to control logging levels more specificically. This +# example sets the main salt library at the 'warning' level, but sets +# 'salt.modules' to log at the 'debug' level: +# log_granular_levels: +# 'salt': 'warning' +# 'salt.modules': 'debug' +# +#log_granular_levels: {} + +# To diagnose issues with minions disconnecting or missing returns, ZeroMQ +# supports the use of monitor sockets to log connection events. This +# feature requires ZeroMQ 4.0 or higher. +# +# To enable ZeroMQ monitor sockets, set 'zmq_monitor' to 'True' and log at a +# debug level or higher. +# +# A sample log event is as follows: +# +# [DEBUG ] ZeroMQ event: {'endpoint': 'tcp://127.0.0.1:4505', 'event': 512, +# 'value': 27, 'description': 'EVENT_DISCONNECTED'} +# +# All events logged will include the string 'ZeroMQ event'. A connection event +# should be logged as the minion starts up and initially connects to the +# master. If not, check for debug log level and that the necessary version of +# ZeroMQ is installed. +# +#zmq_monitor: False + +# Number of times to try to authenticate with the salt master when reconnecting +# to the master +#tcp_authentication_retries: 5 + +###### Module configuration ##### +########################################### +# Salt allows for modules to be passed arbitrary configuration data, any data +# passed here in valid yaml format will be passed on to the salt minion modules +# for use. It is STRONGLY recommended that a naming convention be used in which +# the module name is followed by a . and then the value. Also, all top level +# data must be applied via the yaml dict construct, some examples: +# +# You can specify that all modules should run in test mode: +#test: True +# +# A simple value for the test module: +#test.foo: foo +# +# A list for the test module: +#test.bar: [baz,quo] +# +# A dict for the test module: +#test.baz: {spam: sausage, cheese: bread} +# +# +###### Update settings ###### +########################################### +# Using the features in Esky, a salt minion can both run as a frozen app and +# be updated on the fly. These options control how the update process +# (saltutil.update()) behaves. +# +# The url for finding and downloading updates. Disabled by default. +#update_url: False +# +# The list of services to restart after a successful update. Empty by default. +#update_restart_services: [] + + +###### Keepalive settings ###### +############################################ +# ZeroMQ now includes support for configuring SO_KEEPALIVE if supported by +# the OS. If connections between the minion and the master pass through +# a state tracking device such as a firewall or VPN gateway, there is +# the risk that it could tear down the connection the master and minion +# without informing either party that their connection has been taken away. +# Enabling TCP Keepalives prevents this from happening. + +# Overall state of TCP Keepalives, enable (1 or True), disable (0 or False) +# or leave to the OS defaults (-1), on Linux, typically disabled. Default True, enabled. +#tcp_keepalive: True + +# How long before the first keepalive should be sent in seconds. Default 300 +# to send the first keepalive after 5 minutes, OS default (-1) is typically 7200 seconds +# on Linux see /proc/sys/net/ipv4/tcp_keepalive_time. +#tcp_keepalive_idle: 300 + +# How many lost probes are needed to consider the connection lost. Default -1 +# to use OS defaults, typically 9 on Linux, see /proc/sys/net/ipv4/tcp_keepalive_probes. +#tcp_keepalive_cnt: -1 + +# How often, in seconds, to send keepalives after the first one. Default -1 to +# use OS defaults, typically 75 seconds on Linux, see +# /proc/sys/net/ipv4/tcp_keepalive_intvl. +#tcp_keepalive_intvl: -1 + + +###### Windows Software settings ###### +############################################ +# Location of the repository cache file on the master: +#win_repo_cachefile: 'salt://win/repo/winrepo.p' + + +###### Returner settings ###### +############################################ +# Default Minion returners. Can be a comma delimited string or a list: +# +#return: mysql +# +#return: mysql,slack,redis +# +#return: +# - mysql +# - hipchat +# - slack + + +###### Miscellaneous settings ###### +############################################ +# Default match type for filtering events tags: startswith, endswith, find, regex, fnmatch +#event_match_type: startswith diff --git a/setup/templates/keycloak/conf/mysql.conf b/setup/templates/keycloak/conf/mysql.conf new file mode 100755 index 00000000..9979c477 --- /dev/null +++ b/setup/templates/keycloak/conf/mysql.conf @@ -0,0 +1,6 @@ +[program:mysqld] +command=/root/mysql_starter.sh +autostart=true +autorestart=true +user=root +priority=10 diff --git a/setup/templates/keycloak/conf/mysql_starter.sh b/setup/templates/keycloak/conf/mysql_starter.sh new file mode 100644 index 00000000..d72e1056 --- /dev/null +++ b/setup/templates/keycloak/conf/mysql_starter.sh @@ -0,0 +1,5 @@ +#!/bin/bash + +chown -R mysql:mysql /run/mysqld +chown -R mysql:mysql /var/lib/mysql +/usr/bin/pidproxy /var/run/mysqld/mysqld.pid /usr/bin/mysqld_safe --datadir=/var/lib/mysql --pid-file=/var/run/mysqld/mysqld.pid \ No newline at end of file diff --git a/setup/templates/keycloak/conf/run-supervisor.sh b/setup/templates/keycloak/conf/run-supervisor.sh new file mode 100755 index 00000000..1cdef391 --- /dev/null +++ b/setup/templates/keycloak/conf/run-supervisor.sh @@ -0,0 +1,2 @@ +#!/bin/bash +/usr/bin/supervisord -n -c /etc/supervisor/supervisord.conf diff --git a/setup/templates/keycloak/conf/salt-minion.conf b/setup/templates/keycloak/conf/salt-minion.conf new file mode 100755 index 00000000..63765273 --- /dev/null +++ b/setup/templates/keycloak/conf/salt-minion.conf @@ -0,0 +1,6 @@ +[program:salt-minion] +command=salt-minion +autostart=true +autorestart=true +user=root +priority=100 diff --git a/setup/templates/keycloak/conf/salt-startup.sh b/setup/templates/keycloak/conf/salt-startup.sh new file mode 100755 index 00000000..0cc079c0 --- /dev/null +++ b/setup/templates/keycloak/conf/salt-startup.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# we need to remove the master pub key because the +# startup of this machine invokes new not accepted +# crypto stuff +if [ -f "/etc/salt/pki/minion/minion_master.pub" ] +then + rm /etc/salt/pki/minion/minion_master.pub +fi + +while ! pgrep -f "mariadb" > /dev/null; do + sleep 1 +done + +if [ -f "/root/init.sql" ] +then + mysql < /root/init.sql + rm /root/init.sql +fi \ No newline at end of file diff --git a/setup/templates/keycloak/conf/startup.conf b/setup/templates/keycloak/conf/startup.conf new file mode 100755 index 00000000..c85d2ec1 --- /dev/null +++ b/setup/templates/keycloak/conf/startup.conf @@ -0,0 +1,7 @@ +[program:startup] +command=./root/salt-startup.sh +autostart=true +autorestart=false +user=root +startsecs=0 +priority=50 diff --git a/setup/templates/keycloak/docker-compose.yml b/setup/templates/keycloak/docker-compose.yml new file mode 100644 index 00000000..218987b0 --- /dev/null +++ b/setup/templates/keycloak/docker-compose.yml @@ -0,0 +1,24 @@ +services: + doil_keycloak: + build: + context: . + dockerfile: Dockerfile + image: doil_keycloak:stable + container_name: doil_keycloak + hostname: doil + domainname: keycloak + networks: + doil_proxy: + ipv4_address: 172.24.0.250 + salt_saltnet: + volumes: + - keycloak_1:/var/lib/mysql + - admin:/root/.keycloak +networks: + doil_proxy: + external: true + salt_saltnet: + external: true +volumes: + keycloak_1: + admin: diff --git a/setup/templates/minion/docker-compose.yml b/setup/templates/minion/docker-compose.yml index 6baf7040..dcd0db81 100755 --- a/setup/templates/minion/docker-compose.yml +++ b/setup/templates/minion/docker-compose.yml @@ -7,6 +7,8 @@ services: container_name: %TPL_PROJECT_NAME%_%TPL_PROJECT_DOMAINNAME% hostname: %TPL_PROJECT_NAME% domainname: %TPL_PROJECT_DOMAINNAME% + extra_hosts: + - "%TPL_HOST_DOMAIN%:172.24.0.254" volumes: - type: bind source: ./volumes/mysql @@ -20,6 +22,9 @@ services: - type: bind source: ./volumes/data target: /var/ilias/data + - type: bind + source: ./volumes/cert + target: /var/ilias/cert - type: bind source: ./volumes/index target: /var/ilias/index diff --git a/setup/templates/proxy/conf/generate_index_html.sh b/setup/templates/proxy/conf/generate_index_html.sh index 402e892d..44430592 100755 --- a/setup/templates/proxy/conf/generate_index_html.sh +++ b/setup/templates/proxy/conf/generate_index_html.sh @@ -9,6 +9,7 @@ do RESULT=${RESULT}"
  • ${SERVER_NAME}/${NAME}
  • " done +KEYCLOAK="
  • ${SERVER_NAME}/keycloak
  • " MAIL="
  • ${SERVER_NAME}/mails
  • " cat << EOF > /tmp/index.html @@ -26,11 +27,16 @@ cat << EOF > /tmp/index.html +

    Keycloak Instance

    +

     

    +
      + $(echo "${KEYCLOAK}") +

    Mail Instance

    -

     

    -
      - $(echo "${MAIL}") -
    +

     

    +
      + $(echo "${MAIL}") +

    Doil Instances

     

      diff --git a/setup/templates/proxy/conf/nginx/local.conf b/setup/templates/proxy/conf/nginx/local.conf index 6bfeafed..62e92b2b 100755 --- a/setup/templates/proxy/conf/nginx/local.conf +++ b/setup/templates/proxy/conf/nginx/local.conf @@ -49,6 +49,17 @@ server { rewrite ^/mails/(.*) /mails/$1 break; } + location /keycloak/ { + proxy_pass http://172.24.0.250:8080/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $remote_addr; + proxy_pass_request_headers on; + proxy_set_header X-Forwarded-Proto http; + + rewrite ^/keycloak/(.*) /$1 break; + } + access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log warn; } diff --git a/setup/templates/proxy/conf/salt-startup.sh b/setup/templates/proxy/conf/salt-startup.sh old mode 100644 new mode 100755 diff --git a/setup/updates/update-20241113.sh b/setup/updates/update-20241113.sh new file mode 100644 index 00000000..21396e32 --- /dev/null +++ b/setup/updates/update-20241113.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash + +source ${SCRIPT_DIR}/updates/update.sh + +doil_update_20241113() { + +cat <