diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..ec72aaf --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2025 STRATO GmbH +# SPDX-License-Identifier: AGPL-3.0-or-later + +# List files: simple line-based configuration files +*.list text eol=lf linguist-language=Shell diff --git a/.list-format-spec.md b/.list-format-spec.md new file mode 100644 index 0000000..02502ba --- /dev/null +++ b/.list-format-spec.md @@ -0,0 +1,108 @@ + + +# List File Format Specification + +## Overview + +Files with the `.list` extension in this project contain simple, line-based lists of items. This format is used for configuration purposes, particularly for managing app inclusion/exclusion lists. + +## Syntax Rules + +### Basic Format +- **One item per line**: Each line contains a single item (app name, identifier, etc.) +- **Case sensitive**: Item names are processed as-is +- **No quotes needed**: Items are plain text without quotes or delimiters + +### Comments +- **Shell-style comments**: Lines starting with `#` are treated as comments +- **Full-line comments only**: Inline comments (after an item) are NOT supported +- **Whitespace before `#`**: Leading whitespace before `#` is allowed + +### Empty Lines +- **Ignored**: Blank lines and lines with only whitespace are ignored +- **Used for readability**: Can be used to group related items visually + +### Example + +```list +# Core apps that should be disabled +dashboard +weather_status + +# Communication apps +mail +calendar + +# Optional: These are disabled for performance reasons +# circles +# federation +``` + +## Files Using This Format + +- **`disabled-apps.list`**: Nextcloud apps to disable during build +- **`enabled-core-apps.list`**: Nextcloud apps to check/re-enable if presently disabled +- **`always-enabled-apps.list`**: Nextcloud apps added to the `alwaysEnabled` array in `core/shipped.json`, so they cannot be disabled by administrators +- **`removed-apps.txt`**: Nextcloud apps to completely remove from package + +## Processing + +Scripts process these files using standard shell commands: +```bash +# Example: Read and filter +grep -v '^#' file.list | grep -v '^[[:space:]]*$' +``` + +This approach: +1. Removes comment lines (`^#`) +2. Removes empty lines (`^[[:space:]]*$`) +3. Preserves item names exactly as written + +## IDE Support + +### EditorConfig +The `.editorconfig` file in this directory configures basic formatting for `.list` files: +- Character encoding: UTF-8 +- Line endings: LF (Unix-style) +- Indentation: 2 spaces +- Final newline: Required + +### Syntax Highlighting + +For JetBrains IDEs (IntelliJ IDEA, WebStorm, PyCharm, etc.): +1. Go to **Settings** → **Editor** → **File Types** +2. Find or create **"Shell Script"** file type +3. Add `*.list` to the file name patterns +4. This enables shell-style comment highlighting + +For VS Code: +1. Add to `.vscode/settings.json`: +```json +{ + "files.associations": { + "*.list": "shellscript" + } +} +``` + +## Best Practices + +1. **Add comments**: Explain why items are included, especially for non-obvious choices +2. **Group logically**: Use empty lines to separate related groups of items +3. **Keep sorted**: Consider alphabetically sorting items within groups for easier maintenance +4. **Document changes**: When adding/removing items, document the reason in comments or commit messages +5. **Avoid duplicates**: Each item should appear only once in the file + +## Validation + +To validate a `.list` file: +```bash +# Check for duplicates +sort file.list | uniq -d + +# Preview processed output (what scripts will see) +grep -v '^#' file.list | grep -v '^[[:space:]]*$' +``` diff --git a/always-enabled-apps.list b/always-enabled-apps.list new file mode 100644 index 0000000..8f2f12e --- /dev/null +++ b/always-enabled-apps.list @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 STRATO GmbH +# SPDX-License-Identifier: AGPL-3.0-or-later + +################################################################################ +# Always Enabled Apps List +################################################################################ +# +# This file contains a list of Nextcloud apps that should be added to the +# 'alwaysEnabled' array in core/shipped.json, ensuring they cannot be +# disabled by administrators. +# +# Format: +# - One app name per line +# - Lines starting with # are comments +# - Empty lines are ignored +# - Whitespace is trimmed +# +# This list is currently empty: no HiDrive Next apps have been decided as +# always-enabled yet. Add entries here deliberately, on a case-by-case basis. +################################################################################ diff --git a/apps-disable.sh b/apps-disable.sh index f3ef491..642910d 100755 --- a/apps-disable.sh +++ b/apps-disable.sh @@ -1,22 +1,53 @@ #!/bin/sh +set -e # SPDX-FileCopyrightText: 2025 STRATO GmbH # # SPDX-License-Identifier: AGPL-3.0-or-later -# This script assumes to be located in /IONOS as submodule within the Nextcloud server -# repository. - -# Since this script modifies the shipped.json file, it should not be executed for every -# nextcloud pod in K8s. Also, since we do not use any pvc it would need to be applied for -# each nc-pod individually. Therefore this script should be executed during the image -# build. - -BDIR="$( dirname ${0} )" +################################################################################ +# HiDrive Next Apps Configuration Script +################################################################################ +# +# DESCRIPTION: +# This script manages Nextcloud app configurations by: +# 1. Removing specified apps from the shipped.json file (disabling them) +# 2. Adding specified apps to the alwaysEnabled array (forcing them enabled) +# +# It modifies the 'defaultEnabled' and 'alwaysEnabled' arrays in core/shipped.json +# to control which apps are shipped with the installation and which cannot be +# disabled by administrators. +# +# LOCATION: +# This script is located in /IONOS as a submodule within the Nextcloud +# server repository. +# +# EXECUTION CONTEXT: +# This script modifies the shipped.json file, so it should not be executed +# for every Nextcloud pod in K8s. Since we do not use any PVCs, it would +# need to be applied for each pod individually. Therefore this script should +# be executed during the image build. +# +# USAGE: +# ./apps-disable.sh +# +# The script reads app names from: +# - disabled-apps.list: Apps to remove from shipped.json (one per line) +# - always-enabled-apps.list: Apps to add to alwaysEnabled array (one per line) +# +# PREREQUISITES: +# - jq (JSON processor) must be installed +# - disabled-apps.list must exist in the same directory +# - always-enabled-apps.list is optional but will be processed if present +# - ../core/shipped.json must exist and be valid JSON +# +################################################################################ +# Configuration: Base directory and file paths +BDIR="$(dirname "${0}")" SHIPPED_JSON="${BDIR}/../core/shipped.json" - -. ${BDIR}/disabled-apps.inc.sh +DISABLED_APPS_FILE="${BDIR}/disabled-apps.list" +ALWAYS_ENABLED_APPS_FILE="${BDIR}/always-enabled-apps.list" # Log fatal error message and exit with failure code # Usage: log_fatal @@ -25,24 +56,77 @@ log_fatal() { exit 1 } +# Read app list from file, ignoring comments and empty lines +# Usage: read_app_list +read_app_list() { + _list_file="${1}" + if [ ! -f "${_list_file}" ]; then + echo "" + return + fi + grep -v '^[[:space:]]*#' "${_list_file}" | grep -v '^[[:space:]]*$' | tr '\n' ' ' +} + +# Remove an app from both defaultEnabled and alwaysEnabled arrays in shipped.json +# Usage: unship_app +unship_app() { + app="${1}" + temp_file="${SHIPPED_JSON}.tmp" + + if ! jq --arg app "${app}" \ + 'del(.defaultEnabled[] | select(. == $app)) | del(.alwaysEnabled[] | select(. == $app))' \ + "${SHIPPED_JSON}" > "${temp_file}"; then + log_fatal "Failed to process ${app} with jq" + fi + + mv "${temp_file}" "${SHIPPED_JSON}" + echo "Unshipped app '${app}'" +} + +# Add an app to the alwaysEnabled array in shipped.json if not already present +# Usage: ship_app +ship_app() { + app="${1}" + temp_file="${SHIPPED_JSON}.tmp" + + if ! jq --arg app "${app}" \ + 'if (.alwaysEnabled | index($app)) then . else .alwaysEnabled += [$app] end' \ + "${SHIPPED_JSON}" > "${temp_file}"; then + log_fatal "Failed to process ${app} with jq" + fi + + mv "${temp_file}" "${SHIPPED_JSON}" + echo "Shipped app '${app}' as always enabled" +} + main() { if ! which jq 2>&1 >/dev/null; then log_fatal "jq is required" fi - # alwaysEnabled should be the only attribute in this json file which really matters, - # since it is the only attribute, which is checked for which app can be disabled or not. + if [ ! -f "${SHIPPED_JSON}" ]; then + log_fatal "Shipped JSON file not found: ${SHIPPED_JSON}" + fi + + # NOTE: alwaysEnabled should be the only attribute in this json file which + # really matters, since it is the only attribute, which is checked for + # which app can be disabled or not. # defaultEnabled is only used during installation, but not for updates. - echo "Remove apps from 'shipped' list ..." + DISABLED_APPS=$(read_app_list "${DISABLED_APPS_FILE}") + ALWAYS_ENABLED_APPS=$(read_app_list "${ALWAYS_ENABLED_APPS_FILE}") + echo "Remove apps from 'shipped' list ..." for app in ${DISABLED_APPS}; do - echo "Unship app '${app}' ..." - cat ${SHIPPED_JSON} \ - | jq --arg toUnforce "${app}" 'del(.defaultEnabled[] | select(. == $toUnforce))' \ - | jq --arg toUnforce "${app}" 'del(.alwaysEnabled[] | select(. == $toUnforce))' > ${SHIPPED_JSON}.tmp \ - && mv ${SHIPPED_JSON}.tmp ${SHIPPED_JSON} + unship_app "${app}" done + + if [ -n "${ALWAYS_ENABLED_APPS}" ]; then + echo "Add apps to 'alwaysEnabled' list ..." + for app in ${ALWAYS_ENABLED_APPS}; do + ship_app "${app}" + done + fi } main diff --git a/apps-enable.sh b/apps-enable.sh index 68c09fb..d961191 100755 --- a/apps-enable.sh +++ b/apps-enable.sh @@ -11,8 +11,19 @@ BDIR="$( dirname "${0}" )" NEXTCLOUD_DIR="${BDIR}/.." -. ${BDIR}/enabled-core-apps.inc.sh -. ${BDIR}/disabled-apps.inc.sh +# Read app list from file, ignoring comments and empty lines +# Usage: read_app_list +read_app_list() { + _list_file="${1}" + if [ ! -f "${_list_file}" ]; then + echo "" + return + fi + grep -v '^[[:space:]]*#' "${_list_file}" | grep -v '^[[:space:]]*$' | tr '\n' ' ' +} + +ENABLED_CORE_APPS=$( read_app_list "${BDIR}/enabled-core-apps.list" ) +DISABLED_APPS=$( read_app_list "${BDIR}/disabled-apps.list" ) execute_occ_command() { php "${NEXTCLOUD_DIR}/occ" \ diff --git a/configure-object-store.sh b/configure-object-store.sh index d28bf1f..a8e3a61 100755 --- a/configure-object-store.sh +++ b/configure-object-store.sh @@ -13,6 +13,32 @@ log_fatal() { exit 1 } +# Log warning message +# Usage: log_warning +log_warning() { + echo "\033[1;33m[w] Warning: ${*}\033[0m" >/dev/stderr +} + +# Validate required environment variables +# Usage: validate_env_vars ... +# Returns: 0 if all variables are set, 1 otherwise +validate_env_vars() { + _validation_failed=false + + for _var in "${@}"; do + eval "_value=\${${_var}}" + if [ -z "${_value}" ]; then + log_warning "${_var} environment variable is not set" + _validation_failed=true + fi + done + + if [ "${_validation_failed}" = "true" ]; then + return 1 + fi + return 0 +} + write_config_file() { config="${NEXTCLOUD_ROOT_DIR}/config/object-store.config.php" @@ -63,28 +89,14 @@ main() { log_fatal "occ command not found, are you in Nextcloud's root dir?" fi - if [ -z "${ENC_OBJECT_STORAGE_BUCKET_NAME}" ]; then - log_fatal "ENC_OBJECT_STORAGE_BUCKET_NAME not set" - fi - - if [ -z "${ENC_OBJECT_STORAGE_ACCESS_KEY}" ]; then - log_fatal "ENC_OBJECT_STORAGE_ACCESS_KEY not set" - fi - - if [ -z "${ENC_OBJECT_STORAGE_SECRET}" ]; then - log_fatal "ENC_OBJECT_STORAGE_SECRET not set" - fi - - if [ -z "${ENC_OBJECT_STORAGE_REGION}" ]; then - log_fatal "ENC_OBJECT_STORAGE_REGION not set" - fi - - if [ -z "${ENC_OBJECT_STORAGE_HOSTNAME}" ]; then - log_fatal "ENC_OBJECT_STORAGE_HOSTNAME not set" - fi - - if [ -z "${ENC_OBJECT_STORAGE_PORT}" ]; then - log_fatal "ENC_OBJECT_STORAGE_PORT not set" + if ! validate_env_vars \ + ENC_OBJECT_STORAGE_BUCKET_NAME \ + ENC_OBJECT_STORAGE_ACCESS_KEY \ + ENC_OBJECT_STORAGE_SECRET \ + ENC_OBJECT_STORAGE_REGION \ + ENC_OBJECT_STORAGE_HOSTNAME \ + ENC_OBJECT_STORAGE_PORT; then + log_fatal "required object store environment variables are not set" fi if [ -n "${ENC_OBJECT_STORAGE_USE_SSL}" ] && [ "${ENC_OBJECT_STORAGE_USE_SSL}" != "true" ] && [ "${ENC_OBJECT_STORAGE_USE_SSL}" != "false" ]; then diff --git a/configure-user-oidc.sh b/configure-user-oidc.sh index ef7924d..3d6d0da 100755 --- a/configure-user-oidc.sh +++ b/configure-user-oidc.sh @@ -11,6 +11,32 @@ log_fatal() { exit 1 } +# Log warning message +# Usage: log_warning +log_warning() { + echo "\033[1;33m[w] Warning: ${*}\033[0m" >/dev/stderr +} + +# Validate required environment variables +# Usage: validate_env_vars ... +# Returns: 0 if all variables are set, 1 otherwise +validate_env_vars() { + _validation_failed=false + + for _var in "${@}"; do + eval "_value=\${${_var}}" + if [ -z "${_value}" ]; then + log_warning "${_var} environment variable is not set" + _validation_failed=true + fi + done + + if [ "${_validation_failed}" = "true" ]; then + return 1 + fi + return 0 +} + # Returns the end-session endpoint URI for the given INSTANCE_TYPE and MARKET. endsessionendpointuri() { case "${INSTANCE_TYPE}:${MARKET}" in @@ -47,7 +73,7 @@ configure_user_oidc() { if [ -z "${end_session_uri}" ] || [ -z "${post_logout_uri}" ]; then if [ "${INSTANCE_TYPE}" != "DEV" ]; then - fail "No logout URIs for INSTANCE_TYPE=${INSTANCE_TYPE} MARKET=${MARKET}" + log_fatal "No logout URIs for INSTANCE_TYPE=${INSTANCE_TYPE} MARKET=${MARKET}" fi fi @@ -98,42 +124,19 @@ main() { log_fatal "jq not found" fi - if [ -z "${ENC_OIDC_PROVIDER_IDENTIFIER}" ]; then - log_fatal "ENC_OIDC_PROVIDER_IDENTIFIER not set" - fi - - if [ -z "${ENC_OIDC_CLIENT_ID}" ]; then - log_fatal "ENC_OIDC_CLIENT_ID not set" - fi - - if [ -z "${ENC_OIDC_SECRET}" ]; then - log_fatal "ENC_OIDC_SECRET not set" - fi - - if [ -z "${ENC_OIDC_DISCOVERY_URI}" ]; then - log_fatal "ENC_OIDC_DISCOVERY_URI not set" - fi - - if [ -z "${ENC_OIDC_EXTRA_CLAIMS}" ]; then - log_fatal "ENC_OIDC_EXTRA_CLAIMS not set" - fi - - if [ -z "${ENC_OIDC_MAPPING_UID}" ]; then - log_fatal "ENC_OIDC_MAPPING_UID not set" - fi - - if [ -z "${ENC_OIDC_SCOPES}" ]; then - log_fatal "ENC_OIDC_SCOPES not set" - fi - - if [ -z "${INSTANCE_TYPE}" ]; then - fail "INSTANCE_TYPE not set" + if ! validate_env_vars \ + ENC_OIDC_PROVIDER_IDENTIFIER \ + ENC_OIDC_CLIENT_ID \ + ENC_OIDC_SECRET \ + ENC_OIDC_DISCOVERY_URI \ + ENC_OIDC_EXTRA_CLAIMS \ + ENC_OIDC_MAPPING_UID \ + ENC_OIDC_SCOPES \ + INSTANCE_TYPE \ + MARKET; then + log_fatal "required user_oidc environment variables are not set" fi INSTANCE_TYPE=$(printf '%s' "${INSTANCE_TYPE}" | tr '[:lower:]' '[:upper:]') - - if [ -z "${MARKET}" ]; then - fail "MARKET not set" - fi MARKET=$(printf '%s' "${MARKET}" | tr '[:lower:]' '[:upper:]') if ! configure_user_oidc; then diff --git a/configure.sh b/configure.sh index 7ee2f6c..164a119 100755 --- a/configure.sh +++ b/configure.sh @@ -40,22 +40,64 @@ readonly FAVICON_DIR readonly ADMIN_USERNAME=${ADMIN_USERNAME:-admin} readonly ADMIN_EMAIL=${ADMIN_EMAIL:-admin@example.net} -# Load disabled apps configuration -. "${SCRIPT_DIR}/disabled-apps.inc.sh" +# Read app list from file, ignoring comments and empty lines +# Usage: read_app_list +read_app_list() { + _list_file="${1}" + if [ ! -f "${_list_file}" ]; then + echo "" + return + fi + grep -v '^[[:space:]]*#' "${_list_file}" | grep -v '^[[:space:]]*$' | tr '\n' ' ' +} + +DISABLED_APPS=$( read_app_list "${SCRIPT_DIR}/disabled-apps.list" ) #=============================================================================== # Utility Functions #=============================================================================== +# +# OCC Helper Conventions — Sensitive Data +# ---------------------------------------- +# execute_occ_command [args...] +# General purpose. For commands that do NOT carry secrets. +# Use execute_occ_secret_command for any command where an argument may be sensitive. +# +# execute_occ_secret_command [args...] +# For OCC commands that carry secrets. Arguments are NOT logged on failure to +# prevent accidental secret exposure. Only the subcommand name is recorded. +# +# Rule: always call execute_occ_secret_command directly for sensitive commands. + # Execute NextCloud OCC command with error handling # Usage: execute_occ_command [args...] execute_occ_command() { + # Safety net: --secret/--sensitive means a secret is in the args; delegate to execute_occ_secret_command. + # Must run before any logging that would expose ${*}. + if echo "${*}" | grep -qE -- "--secret|--sensitive"; then + log_warning "execute_occ_command called with --secret/--sensitive; use execute_occ_secret_command instead. Delegating." + execute_occ_secret_command "${@}" + return $? + fi + if ! php occ "${@}"; then log_error "Failed to execute OCC command: ${*}" return 1 fi } +# Execute any OCC command that contains sensitive data. +# Arguments are NOT logged on failure to prevent accidental secret exposure. +# Only the subcommand name is recorded. +# Usage: execute_occ_secret_command [args...] +execute_occ_secret_command() { + if ! php occ "${@}"; then + log_error "Failed to execute sensitive OCC command: ${1} [args not logged]" + return 1 + fi +} + # Log error message to stderr # Usage: log_error log_error() { @@ -81,12 +123,36 @@ log_info() { echo "[i] ${*}" } +# Validate required environment variables +# Usage: validate_env_vars ... +# Returns: 0 if all variables are set, 1 otherwise +validate_env_vars() { + _validation_failed=false + + for _var in "${@}"; do + eval "_value=\${${_var}}" + if [ -z "${_value}" ]; then + log_warning "${_var} environment variable is not set" + _validation_failed=true + fi + done + + if [ "${_validation_failed}" = "true" ]; then + return 1 + fi + return 0 +} + # Check if required dependencies are available # Usage: check_dependencies check_dependencies() { if ! which php >/dev/null 2>&1; then log_fatal "php is required but not found in PATH" fi + + if ! which jq >/dev/null 2>&1; then + log_fatal "jq is required but not found in PATH" + fi } # Verify HiDrive Next installation status @@ -154,15 +220,14 @@ config_ui() { configure_ionos_processes_app() { log_info "Configuring nc_ionos_processes app..." - # Check required environment variables - if [ -z "${IONOS_PROCESSES_API_URL}" ] || [ -z "${IONOS_PROCESSES_USER}" ] || [ -z "${IONOS_PROCESSES_PASS}" ]; then - log_warning "IONOS_PROCESSES_API_URL, IONOS_PROCESSES_USER or IONOS_PROCESSES_PASS not set, skipping configuration of nc_ionos_processes app" + if ! validate_env_vars IONOS_PROCESSES_API_URL IONOS_PROCESSES_USER IONOS_PROCESSES_PASS; then + log_warning "skipping configuration of nc_ionos_processes app" return 0 fi execute_occ_command config:app:set --value "${IONOS_PROCESSES_API_URL}" --type string nc_ionos_processes ionos_mail_base_url execute_occ_command config:app:set --value "${IONOS_PROCESSES_USER}" --type string nc_ionos_processes basic_auth_user - execute_occ_command config:app:set --value "${IONOS_PROCESSES_PASS}" --sensitive --type string nc_ionos_processes basic_auth_pass + execute_occ_secret_command config:app:set --value "${IONOS_PROCESSES_PASS}" --sensitive --type string nc_ionos_processes basic_auth_pass } # Configure serverinfo app with authentication token @@ -175,7 +240,7 @@ configure_serverinfo_app() { return 0 fi - execute_occ_command config:app:set serverinfo token --value "${NC_APP_SERVERINFO_TOKEN}" + execute_occ_secret_command config:app:set serverinfo token --value "${NC_APP_SERVERINFO_TOKEN}" } # Configure notify_push app @@ -210,13 +275,8 @@ configure_app_notify_push() { configure_app_richdocuments() { execute_occ_command app:disable richdocuments - # Validate required environment variables - if ! [ "${COLLABORA_HOST}" ]; then - log_fatal "COLLABORA_HOST environment variable is not set" - fi - - if ! [ "${COLLABORA_EDIT_GROUPS}" ]; then - log_fatal "COLLABORA_EDIT_GROUPS environment variable is not set" + if ! validate_env_vars COLLABORA_HOST COLLABORA_EDIT_GROUPS; then + log_fatal "required Collabora environment variables are not set" fi # Configure and enable Collabora diff --git a/disabled-apps.inc.sh b/disabled-apps.list similarity index 65% rename from disabled-apps.inc.sh rename to disabled-apps.list index 5bec659..96ad070 100644 --- a/disabled-apps.inc.sh +++ b/disabled-apps.list @@ -1,10 +1,12 @@ # SPDX-FileCopyrightText: 2025 STRATO GmbH -# # SPDX-License-Identifier: AGPL-3.0-or-later -# List of apps to be disabled. -# Managed as include file so that it can be used in various scripts. -export DISABLED_APPS="activity +# List of apps to be disabled +# One app name per line +# Lines starting with # are comments and will be ignored +# Empty lines are also ignored + +activity circles comments contactsinteraction @@ -22,4 +24,4 @@ systemtags updatenotification user_status weather_status -workflowengine" +workflowengine diff --git a/enabled-core-apps.inc.sh b/enabled-core-apps.list similarity index 72% rename from enabled-core-apps.inc.sh rename to enabled-core-apps.list index 868a1fa..c25555e 100644 --- a/enabled-core-apps.inc.sh +++ b/enabled-core-apps.list @@ -1,10 +1,12 @@ # SPDX-FileCopyrightText: 2025 STRATO GmbH -# # SPDX-License-Identifier: AGPL-3.0-or-later # List of ./apps to be always enabled. -# Managed as include file so that it can be used in various scripts. -export ENABLED_CORE_APPS="admin_audit +# One app name per line +# Lines starting with # are comments and will be ignored +# Empty lines are also ignored + +admin_audit cloud_federation_api dav federatedfilesharing @@ -19,4 +21,3 @@ sharebymail theming twofactor_backupcodes webhook_listeners -"