Просмотр исходного кода

Feature/newapisystem (#2535)

* Adds new API system

* Each key is composed of an identification hash and a secret hash;
* Permission system for keys with command limitation;
* API connection by non-admin users;
* Added /api/v2/ route to not conflict with the old version;
* Option to enable V2 in system settings;
* Management of keys by the panel;
* New keys are saved in the "data/access-keys" directory;
* API settings in "data/api".

* Changes to the new API system

* Merges API routes;
* Renames config index to API_SYSTEM;
* Updates locales;
* Validation to avoid creating a new key when refreshing the page;
* Adds key creation and deletion logs.

* Fixes for Shellcheck

* API changes and fixes

* Changes "disabled" to "readonly" in the fields of list_access_key.html;
* Initializes the api directory in the installer;
* Initialize api directory in 1.6.0 upgrade;
* Changes the API version in the installer;
* Change JS to hide the IPs field only when the 2 API versions are disabled;
* Removes $v_comment variable from the key creation page;
* Keep legacy api enabled for now
* Allow for coded disabled api without lookup if disabled
* Allow enable / disable legacy / new api seperate
* Allow changes to api to enable / disable api for legacy / new api
* Return access_key:access_secret_key for use in bash

Co-authored-by: Jaap Marcus <9754650+jaapmarcus@users.noreply.github.com>
João Henrique 3 лет назад
Родитель
Сommit
baaba4e736

+ 2 - 1
.gitignore

@@ -15,4 +15,5 @@ test/node_modules/
 npm-debug.log
 .phpunit.result.cache
 .vs
-.nova
+.nova
+/.idea/

+ 109 - 0
bin/v-add-access-key

@@ -0,0 +1,109 @@
+#!/bin/bash
+# info: generate access key
+# options: USER [PERMISSIONS] [COMMENT] [FORMAT]
+#
+# example: v-add-access-key admin v-purge-nginx-cache,v-list-mail-accounts comment json
+#
+# The "PERMISSIONS" argument is optional for the admin user only.
+# This function creates a key file in $HESTIA/data/access-keys/
+
+#----------------------------------------------------------#
+#                Variables & Functions                     #
+#----------------------------------------------------------#
+
+# Argument definition
+user=$1
+permissions=$2
+comment=$3
+format=${4-shell}
+
+# Includes
+# shellcheck source=/etc/hestiacp/hestia.conf
+source /etc/hestiacp/hestia.conf
+# shellcheck source=/usr/local/hestia/func/main.sh
+source $HESTIA/func/main.sh
+# load config file
+source_conf "$HESTIA/conf/hestia.conf"
+
+keygen() {
+    local LENGTH=${1:-20}
+    local USE_SPECIAL_CHARACTERS="${2:-no}"
+
+    local MATRIX='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+    if [[ "$USE_SPECIAL_CHARACTERS" == "yes" ]]; then
+        MATRIX+='_-+^~=%'
+    fi
+
+    local PASS N
+    while [ ${N:=1} -le $LENGTH ]; do
+        PASS="$PASS${MATRIX:$(($RANDOM%${#MATRIX})):1}"
+        let N+=1
+    done
+
+    echo "$PASS"
+}
+
+access_key_id="$(keygen)"
+secret_access_key="$(keygen 40 yes)"
+
+# Perform verification if read-only mode is enabled
+check_hestia_demo_mode
+
+# Remove whitespace and bin path from permissions
+permissions="$(cleanup_key_permissions "$permissions")"
+
+time_n_date=$(date +'%T %F')
+time=$(echo "$time_n_date" |cut -f 1 -d \ )
+date=$(echo "$time_n_date" |cut -f 2 -d \ )
+
+#----------------------------------------------------------#
+#                    Verifications                         #
+#----------------------------------------------------------#
+
+check_args '1' "$#" 'USER [PERMISSIONS] [COMMENT] [FORMAT]'
+is_format_valid 'user'
+is_object_valid 'user' 'USER' "$user"
+is_key_permissions_format_valid "$permissions" "$user"
+if [ -n "$comment" ]; then
+    is_format_valid 'comment'
+fi
+
+#----------------------------------------------------------#
+#                       Action                             #
+#----------------------------------------------------------#
+
+if [ ! -d "$HESTIA/data/access-keys/" ]; then
+    mkdir -p $HESTIA/data/access-keys/
+    chown root:root $HESTIA/data/access-keys/
+    chmod 750 $HESTIA/data/access-keys/
+fi
+
+if [[ -e "$HESTIA/data/access-keys/${access_key_id}" ]]; then
+    while [[ -e "$HESTIA/data/access-keys/${access_key_id}" ]]; do
+        access_key_id=$(keygen)
+    done
+fi
+
+echo "SECRET_ACCESS_KEY='$secret_access_key'" >"$HESTIA/data/access-keys/${access_key_id}"
+echo "USER='$user'" >>"$HESTIA/data/access-keys/${access_key_id}"
+echo "PERMISSIONS='$permissions'" >>"$HESTIA/data/access-keys/${access_key_id}"
+echo "COMMENT='$comment'" >>"$HESTIA/data/access-keys/${access_key_id}"
+echo "TIME='$time'" >>"$HESTIA/data/access-keys/${access_key_id}"
+echo "DATE='$date'" >>"$HESTIA/data/access-keys/${access_key_id}"
+# TODO Index reserved for future implementation
+echo "EXPIRES_IN=''" >>"$HESTIA/data/access-keys/${access_key_id}"
+echo "IP=''" >>"$HESTIA/data/access-keys/${access_key_id}"
+
+chmod 640 "$HESTIA/data/access-keys/${access_key_id}"
+
+$BIN/v-list-access-key "$access_key_id" "$format"
+
+#----------------------------------------------------------#
+#                       Hestia                             #
+#----------------------------------------------------------#
+
+# Logging
+log_history "Access key $access_key_id generated" "Warning" "$user" "API"
+log_event "$OK" "$ARGUMENTS"
+
+exit

+ 9 - 5
bin/v-change-sys-api

@@ -2,8 +2,10 @@
 # info: Enable / Disable API access 
 # options: STATUS 
 #
-# example: v-change-sys-api enable
-#          # Enable API
+# example: v-change-sys-api enable legacy
+#          # Enable legacy api currently default on most of api based systems
+# example: v-change-sys-api enable api
+#          # Enable api
 #
 # example: v-change-sys-api disable
 #          # Disable API
@@ -11,6 +13,7 @@
 # Enabled / Disable API
 
 status=$1
+version=$2
 
 # Includes
 # shellcheck source=/etc/hestiacp/hestia.conf
@@ -24,9 +27,8 @@ source_conf "$HESTIA/conf/hestia.conf"
 #                Variables & Functions                     #
 #----------------------------------------------------------#
 
-check_args '1' "$#" "STATUS"
+check_args '1' "$#" "STATUS" "VERSION"
 is_type_valid "enable,disable,remove" "$status"
-
 # Perform verification if read-only mode is enabled
 check_hestia_demo_mode
 
@@ -51,10 +53,12 @@ if [ "$status" = "enable" ]; then
         sed -i 's|die("Error: Disabled");|//die("Error: Disabled");|g' $HESTIA/web/api/index.php
         sed -i 's|////|//|g' $HESTIA/web/api/index.php
     fi
-    $HESTIA/bin/v-change-sys-config-value "API" "yes"
+    if [ "$version" = "legacy" ] || [ "$version" = "all" ]; then $HESTIA/bin/v-change-sys-config-value "API" "yes"; fi
+    if [ "$version" = "api" ] || [ "$version" = "all" ]; then $HESTIA/bin/v-change-sys-config-value "API_SYSTEM" "1"; fi
 else
     $HESTIA/bin/v-change-sys-config-value "API" "no"
     $HESTIA/bin/v-change-sys-config-value "API_ALLOWED_IP" ""
+    $HESTIA/bin/v-change-sys-config-value "API_SYSTEM" "0"
     if [ "$status" != "remove" ]; then
         sed -i 's|//die("Error: Disabled");|die("Error: Disabled");|g' $HESTIA/web/api/index.php
     fi

+ 114 - 0
bin/v-check-access-key

@@ -0,0 +1,114 @@
+#!/bin/bash
+# info: check access key
+# options: ACCESS_KEY_ID SECRET_ACCESS_KEY COMMAND [IP] [FORMAT]
+#
+# example: v-check-access-key key_id secret v-purge-nginx-cache 127.0.0.1 json
+#
+# * Checks if the key exists;
+# * Checks if the secret belongs to the key;
+# * Checks if the key user is suspended;
+# * Checks if the key has permission to run the command.
+
+#----------------------------------------------------------#
+#                Variables & Functions                     #
+#----------------------------------------------------------#
+
+access_key_id="$(basename "$1")"
+secret_access_key=$2
+hst_command=$3
+ip=${4-127.0.0.1}
+format=${5-shell}
+
+# Includes
+# shellcheck source=/etc/hestiacp/hestia.conf
+source /etc/hestiacp/hestia.conf
+# shellcheck source=/usr/local/hestia/func/main.sh
+source $HESTIA/func/main.sh
+# load config file
+source_conf "$HESTIA/conf/hestia.conf"
+
+# Perform verification if read-only mode is enabled
+check_hestia_demo_mode
+
+time_n_date=$(date +'%T %F')
+time=$(echo "$time_n_date" |cut -f 1 -d \ )
+date=$(echo "$time_n_date" |cut -f 2 -d \ )
+
+# JSON list function
+json_list() {
+    echo -n '{"USER": "'$user'"';
+
+    if [[ -n "$user_arg_pos" ]]; then
+        echo -n ', "USER_ARG_POSITION": '$user_arg_pos''
+    fi
+
+    echo '}'
+}
+
+# SHELL list function
+shell_list() {
+    echo "USER:               $user"
+    if [[ -n "$user_arg_pos" ]]; then
+        echo "USER_ARG_POSITION:  $user_arg_pos"
+    fi
+}
+
+# Callback to intercept invalid result validation
+abort_missmatch() {
+    echo "Error: $2"
+    echo "$date $time ${access_key_id:-api} $ip failed to login" >> $HESTIA/log/auth.log
+
+    # Add a log for user
+    if [[ "$1" == "$E_PASSWORD" && -n "$user" ]]; then
+        log_history "[$ip] $access_key_id $2" "Error" "$user" "API"
+    fi
+
+    if [[ "$1" == "$E_FORBIDEN" ]]; then
+        exit "$1"
+    fi
+
+    exit "$E_PASSWORD"
+}
+
+#----------------------------------------------------------#
+#                    Verifications                         #
+#----------------------------------------------------------#
+
+# Add a callback to intercept invalid "check_result" results
+CHECK_RESULT_CALLBACK="abort_missmatch"
+
+check_args '3' "$#" 'ACCESS_KEY_ID SECRET_ACCESS_KEY COMMAND [IP] [FORMAT]'
+is_format_valid 'access_key_id'
+is_object_valid 'key' 'KEY' "$access_key_id"
+is_format_valid 'secret_access_key'
+check_access_key_secret "$access_key_id" "$secret_access_key" user
+check_access_key_cmd "$access_key_id" "$hst_command" user_arg_pos
+
+# Check if key owner is active
+is_format_valid 'user'
+is_object_valid 'user' 'USER' "$user"
+export USER_DATA=$HESTIA/data/users/$user
+is_object_unsuspended 'user' 'USER' "$user"
+
+# Remove the check_result callback
+CHECK_RESULT_CALLBACK=""
+
+#----------------------------------------------------------#
+#                       Action                             #
+#----------------------------------------------------------#
+
+# Listing data
+case $format in
+    json)   json_list ;;
+    shell)  shell_list ;;
+esac
+
+#----------------------------------------------------------#
+#                       Hestia                             #
+#----------------------------------------------------------#
+
+# Logging
+log_history "[$ip] Access key $access_key_id successfully launched with command $hst_command" "Info" "$user" "API"
+echo "$date $time $access_key_id $ip $hst_command successfully launched" >> $HESTIA/log/auth.log
+
+exit

+ 57 - 0
bin/v-delete-access-key

@@ -0,0 +1,57 @@
+#!/bin/bash
+# info: delete access key
+# options: ACCESS_KEY_ID
+#
+# example: v-delete-access-key mykey
+#
+# This function removes a key from in $HESTIA/data/access-keys/
+
+#----------------------------------------------------------#
+#                Variables & Functions                     #
+#----------------------------------------------------------#
+
+# Includes
+# shellcheck source=/etc/hestiacp/hestia.conf
+source /etc/hestiacp/hestia.conf
+# shellcheck source=/usr/local/hestia/func/main.sh
+source $HESTIA/func/main.sh
+# load config file
+source_conf "$HESTIA/conf/hestia.conf"
+
+#----------------------------------------------------------#
+#                Variables & Functions                     #
+#----------------------------------------------------------#
+
+access_key_id=$1
+
+check_args '1' "$#" "ACCESS_KEY_ID"
+is_format_valid 'access_key_id'
+is_object_valid 'key' 'KEY' "$access_key_id"
+
+# Perform verification if read-only mode is enabled
+check_hestia_demo_mode
+
+#----------------------------------------------------------#
+#                       Action                             #
+#----------------------------------------------------------#
+
+if [ ! -d "$HESTIA/data/access-keys/" ]; then
+  exit "$E_NOTEXIST"
+fi
+
+if [[ -e "${HESTIA}/data/access-keys/${access_key_id}" ]]; then
+    source_conf "${HESTIA}/data/access-keys/${access_key_id}"
+    rm "${HESTIA}/data/access-keys/${access_key_id}"
+else
+    exit "$E_NOTEXIST"
+fi
+
+#----------------------------------------------------------#
+#                       Hestia                             #
+#----------------------------------------------------------#
+
+# Logging
+log_history "Access key $access_key_id deleted" "Info" "$USER" "API"
+log_event "$OK" "$ARGUMENTS"
+
+exit

+ 90 - 0
bin/v-list-access-key

@@ -0,0 +1,90 @@
+#!/bin/bash
+# info: list all API access keys
+# options: ACCESS_KEY_ID [FORMAT]
+#
+# example: v-list-access-key 1234567890ABCDefghij json
+
+#----------------------------------------------------------#
+#                Variables & Functions                     #
+#----------------------------------------------------------#
+
+# Argument definition
+access_key_id="$1"
+format="${2:-shell}"
+
+# Includes
+# shellcheck source=/etc/hestiacp/hestia.conf
+source /etc/hestiacp/hestia.conf
+# shellcheck source=/usr/local/hestia/func/main.sh
+source $HESTIA/func/main.sh
+# load config file
+source_conf "$HESTIA/conf/hestia.conf"
+
+# JSON list function
+json_list() {
+    local PERMISSIONS_ARR='[]'
+    if [[ -n "$PERMISSIONS" ]]; then
+        PERMISSIONS_ARR="[\"$(echo "$PERMISSIONS" | sed -E 's|,|", "|g')\"]"
+    fi
+
+    echo '{
+    "ACCESS_KEY_ID": "'$access_key_id'",
+    "SECRET_ACCESS_KEY": "'$SECRET_ACCESS_KEY'",
+    "USER": "'$USER'",
+    "PERMISSIONS": '$PERMISSIONS_ARR',
+    "COMMENT": "'$COMMENT'",
+    "TIME": "'$TIME'",
+    "DATE": "'$DATE'"
+}'
+}
+
+# SHELL list function
+shell_list() {
+    echo "ACCESS_KEY_ID:      $access_key_id"
+    echo "SECRET_ACCESS_KEY:  $SECRET_ACCESS_KEY"
+    echo "USER:               $USER"
+    echo "PERMISSIONS:        $PERMISSIONS"
+    echo "COMMENT:            $COMMENT"
+    echo "TIME:               $TIME"
+    echo "DATE:               $DATE"
+}
+
+plain_list(){
+    echo $access_key_id:$SECRET_ACCESS_KEY
+}
+
+
+#----------------------------------------------------------#
+#                    Verifications                         #
+#----------------------------------------------------------#
+
+check_args '1' "$#" 'ACCESS_KEY_ID [FORMAT]'
+is_format_valid 'access_key_id'
+is_object_valid 'key' 'KEY' "$access_key_id"
+
+#----------------------------------------------------------#
+#                       Action                             #
+#----------------------------------------------------------#
+
+# Avoid "USER" receive "root" in old keys
+USER="admin"
+PERMISSIONS=""
+COMMENT=""
+DATE=""
+TIME=""
+
+source_conf "${HESTIA}/data/access-keys/${access_key_id}"
+
+# Listing data
+case $format in
+    json)   json_list ;;
+    shell)  shell_list ;;
+    plain)  plain_list;;
+esac
+
+
+#----------------------------------------------------------#
+#                       Hestia                             #
+#----------------------------------------------------------#
+
+exit

+ 94 - 0
bin/v-list-access-keys

@@ -0,0 +1,94 @@
+#!/bin/bash
+# info: list all API access keys
+# options: [FORMAT]
+#
+# example: v-list-access-keys json
+
+#----------------------------------------------------------#
+#                Variables & Functions                     #
+#----------------------------------------------------------#
+
+# Argument definition
+user="$1"
+format="${2:-shell}"
+
+# Includes
+# shellcheck source=/etc/hestiacp/hestia.conf
+source /etc/hestiacp/hestia.conf
+# shellcheck source=/usr/local/hestia/func/main.sh
+source $HESTIA/func/main.sh
+# load config file
+source_conf "$HESTIA/conf/hestia.conf"
+
+# JSON list function
+json_list() {
+    echo -n '{'
+    local quote=''
+    local PERMISSIONS_ARR ACCESS_KEY_ID
+    for key_file in $HESTIA/data/access-keys/*; do
+        key_file="$(basename -- "$key_file")"
+        if [[ "$key_file" =~ ^[[:alnum:]]{20}$ ]]; then
+            USER="admin" SECRET_ACCESS_KEY="" PERMISSIONS="" DATE="" TIME="" COMMENT=""
+            source_conf "$HESTIA/data/access-keys/$key_file"
+            if [ "$user" = "$USER" ] || [ -z "$user" ]; then
+                PERMISSIONS_ARR='[]'
+                if [[ -n "$PERMISSIONS" ]]; then
+                    PERMISSIONS_ARR="[\"$(echo "$PERMISSIONS" | sed -E 's|,|", "|g')\"]"
+                fi
+    
+                ACCESS_KEY_ID="$(basename "$key_file")"
+    
+                echo -en "${quote:-\n}"
+                echo -n '    "'$ACCESS_KEY_ID'": {'
+                echo -n '"ACCESS_KEY_ID": "'${ACCESS_KEY_ID}'", '
+                echo -n '"USER": "'${USER}'", '
+                echo -n '"PERMISSIONS": '${PERMISSIONS_ARR}', '
+                echo -n '"COMMENT": "'${COMMENT}'", '
+                echo -n '"TIME": "'${TIME}'", '
+                echo -n '"DATE": "'${DATE}'"'
+                echo -n '}'
+                quote=",\n"
+            fi
+        fi
+    done
+
+    [[ -n "$quote" ]] && echo
+    echo -e '}'
+}
+
+# SHELL list function
+shell_list() {
+    list="ID\tUSER\tPERMISSIONS\tCOMMENT\tTIME\tDATE\n"
+    list+="--\t------\t----\t-----------\t-------\t----\t----\n"
+
+    for key_file in $HESTIA/data/access-keys/*; do
+        key_file="$(basename -- "$key_file")"
+        if [[ "$key_file" =~ ^[[:alnum:]]{20}$ ]]; then
+            USER="admin" SECRET_ACCESS_KEY="" PERMISSIONS="" DATE="" TIME="" COMMENT=""
+            source_conf "$HESTIA/data/access-keys/$key_file"
+            if [ "$user" = "$USER" ] || [ -z "$user" ]; then
+                ACCESS_KEY_ID="$(basename "$key_file")"
+                list+="${ACCESS_KEY_ID}\t${USER}\t${PERMISSIONS:--}\t${COMMENT:--}\t${TIME}\t${DATE}\n"
+            fi
+        fi
+    done
+
+    echo -e "$list" | column -t -s "	"
+}
+
+#----------------------------------------------------------#
+#                       Action                             #
+#----------------------------------------------------------#
+
+# Listing data
+case $format in
+    json)   json_list ;;
+    shell)  shell_list ;;
+esac
+
+
+#----------------------------------------------------------#
+#                       Hestia                             #
+#----------------------------------------------------------#
+
+exit

+ 71 - 0
bin/v-list-api

@@ -0,0 +1,71 @@
+#!/bin/bash
+# info: list api
+# options: API [FORMAT]
+#
+# example: v-list-api mail-accounts json
+
+#----------------------------------------------------------#
+#                Variables & Functions                     #
+#----------------------------------------------------------#
+
+# Argument definition
+api="$1"
+format="${2:-shell}"
+
+# Includes
+# shellcheck source=/etc/hestiacp/hestia.conf
+source /etc/hestiacp/hestia.conf
+# shellcheck source=/usr/local/hestia/func/main.sh
+source $HESTIA/func/main.sh
+# load config file
+source_conf "$HESTIA/conf/hestia.conf"
+
+# JSON list function
+json_list() {
+    local COMMANDS_ARR='[]'
+    if [[ -n "$COMMANDS" ]]; then
+        COMMANDS_ARR="[\"$(echo "$COMMANDS" | sed -E 's|,|", "|g')\"]"
+    fi
+
+    echo '{
+    "API": "'$api'",
+    "ROLE": "'$ROLE'",
+    "COMMANDS": '$COMMANDS_ARR'
+}'
+}
+
+# SHELL list function
+shell_list() {
+    echo "API:           $api"
+    echo "ROLE:          $ROLE"
+    echo "COMMANDS:      $COMMANDS"
+}
+
+#----------------------------------------------------------#
+#                    Verifications                         #
+#----------------------------------------------------------#
+
+check_args '1' "$#" 'API [FORMAT]'
+
+if [[ -z "$api" || ! -f "$HESTIA/data/api/${api}" ]]; then
+    check_result "$E_INVALID" "API $api doesn't exist"
+fi
+
+
+#----------------------------------------------------------#
+#                       Action                             #
+#----------------------------------------------------------#
+
+source_conf "${HESTIA}/data/api/${api}"
+
+# Listing data
+case $format in
+    json)   json_list ;;
+    shell)  shell_list ;;
+esac
+
+#----------------------------------------------------------#
+#                       Hestia                             #
+#----------------------------------------------------------#
+
+exit

+ 65 - 0
bin/v-list-apis

@@ -0,0 +1,65 @@
+#!/bin/bash
+# info: list available APIs
+# options: [FORMAT]
+#
+# example: v-list-apis json
+
+#----------------------------------------------------------#
+#                Variables & Functions                     #
+#----------------------------------------------------------#
+
+# Argument definition
+format="${1:-shell}"
+
+# Includes
+# shellcheck source=/etc/hestiacp/hestia.conf
+source /etc/hestiacp/hestia.conf
+# shellcheck source=/usr/local/hestia/func/main.sh
+source $HESTIA/func/main.sh
+# load config file
+source_conf "$HESTIA/conf/hestia.conf"
+
+# JSON list function
+json_list() {
+    echo '{'
+    local quote COMMANDS_ARR
+    for api in $HESTIA/data/api/*; do
+        api="$(basename -- "$api")"
+        source_conf $HESTIA/data/api/$api
+
+        COMMANDS_ARR='[]'
+        if [[ -n "$COMMANDS" ]]; then
+            COMMANDS_ARR="[\"$(echo "$COMMANDS" | sed -E 's|,|", "|g')\"]"
+        fi
+
+        echo -en "$quote"
+        echo -n '    "'$api'": {"COMMANDS": '${COMMANDS_ARR}', "ROLE": "'${ROLE}'"}'
+        quote=",\n"
+    done
+    echo -e '\n}'
+}
+
+# SHELL list function
+shell_list() {
+    list="API\tROLE\tCOMMANDS\n"
+    list+="---\t----\t--------\n"
+
+    for api in $HESTIA/data/api/*; do
+        api="$(basename -- "$api")"
+        source_conf $HESTIA/data/api/$api
+        list+="${api}\t${ROLE}\t${COMMANDS}\n"
+    done
+    echo -e "$list" | column -t -s "	"
+}
+
+# Listing data
+case $format in
+    json)   json_list ;;
+    shell)  shell_list ;;
+esac
+
+#----------------------------------------------------------#
+#                       Hestia                             #
+#----------------------------------------------------------#
+
+exit

+ 3 - 2
bin/v-list-sys-config

@@ -76,6 +76,7 @@ json_list() {
         "ENFORCE_SUBDOMAIN_OWNERSHIP": "'$ENFORCE_SUBDOMAIN_OWNERSHIP'",
         "DEBUG_MODE": "'$DEBUG_MODE'",
         "API": "'$API'",
+        "API_SYSTEM": "'$API_SYSTEM'",
         "API_ALLOWED_IP": "'$API_ALLOWED_IP'",
         "UPDATE_AVAILABLE": "'$UPDATE_AVAILABLE'",
         "PLUGIN_APP_INSTALLER": "'$PLUGIN_APP_INSTALLER'",
@@ -185,9 +186,9 @@ shell_list() {
     if [ -n "$API" ]; then
     echo "API enabled:             $API"
     echo "API allowed IP's:             $API_ALLOWED_IP"
-    
+
     fi
-    
+
     if [ -n "$SMTP_RELAY" ] && [ "$SMTP_RELAY" != 'false' ]; then
 	echo "SMTP Relay enabled:                $SMTP_RELAY"
 	echo "SMTP Relay Server:                 $SMTP_RELAY_HOST"

+ 273 - 45
func/main.sh

@@ -26,6 +26,7 @@ HESTIA_GIT_REPO="https://raw.githubusercontent.com/hestiacp/hestiacp"
 HESTIA_THEMES="$HESTIA/web/css/themes"
 HESTIA_THEMES_CUSTOM="$HESTIA/web/css/themes/custom"
 SCRIPT="$(basename $0)"
+CHECK_RESULT_CALLBACK=""
 
 # Return codes
 OK=0
@@ -57,7 +58,7 @@ detect_os() {
         if [ "$get_os_type" = "ubuntu" ]; then
             if [ -e '/usr/bin/lsb_release' ]; then
                 OS_VERSION="$(lsb_release -s -r)"
-                OS_TYPE='Ubuntu'            
+                OS_TYPE='Ubuntu'
             fi
         elif [ "$get_os_type" = "debian" ]; then
             OS_TYPE='Debian'
@@ -102,21 +103,23 @@ log_event() {
 
 # Log user history
 log_history() {
-    message=$1
+    message=${1//\'/\´} # Avoid single quotes broken the log
+    evt_level=${2:-$event_level}
     log_user=${3-$user}
+    evt_category=${4:-$event_category}
 
     # Set default event level and category if not specified
-    if [ -z "$event_level" ]; then
-        event_level="Info"
+    if [ -z "$evt_level" ]; then
+        evt_level="Info"
     fi
-    if [ -z "$event_category" ]; then
-        event_category="System"
+    if [ -z "$evt_category" ]; then
+        evt_category="System"
     fi
 
     # Log system events to system log file
     if [ "$log_user" = "system" ]; then
         log=$HESTIA/data/users/admin/system.log
-    else 
+    else
         if ! $BIN/v-list-user "$log_user" >/dev/null; then
             return $E_NOTEXIST
         fi
@@ -139,20 +142,21 @@ log_history() {
     fi
     curr_str=$(grep "ID=" $log | cut -f 2 -d \' | sort -n | tail -n1)
     id="$((curr_str +1))"
-    echo "ID='$id' DATE='$date' TIME='$time' LEVEL='$event_level' CATEGORY='$event_category' MESSAGE='$message'" >> $log
+    echo "ID='$id' DATE='$date' TIME='$time' LEVEL='$evt_level' CATEGORY='$evt_category' MESSAGE='$message'" >> $log
 }
 
 # Result checker
 check_result() {
     if [ $1 -ne 0 ]; then
-        echo "Error: $2"
-        if [ -n "$3" ]; then
-            log_event "$3" "$ARGUMENTS"
-            exit $3
+        local err_code="${3:-$1}"
+        if [[ -n "$CHECK_RESULT_CALLBACK" && "$(type -t "$CHECK_RESULT_CALLBACK")" == 'function' ]]; then
+            $CHECK_RESULT_CALLBACK "$err_code" "$2"
         else
-            log_event "$1" "$ARGUMENTS"
-            exit $1
+            echo "Error: $2"
+            log_event "$err_code" "$ARGUMENTS"
         fi
+
+        exit $err_code
     fi
 }
 
@@ -225,10 +229,10 @@ generate_password() {
 
 # Package existence check
 is_package_valid() {
-    if [ -z $1 ]; then 
+    if [ -z $1 ]; then
       if [ ! -e "$HESTIA/data/packages/$package.pkg" ]; then
       check_result "$E_NOTEXIST" "package $package doesn't exist"
-      fi    
+      fi
     else
       if [ ! -e "$HESTIA/data/packages/$1.pkg" ]; then
           check_result "$E_NOTEXIST" "package $1 doesn't exist"
@@ -291,6 +295,12 @@ is_object_valid() {
         if [ "$(dirname "$tstpath")" != "$(readlink -f "$HESTIA/data/users")" ] || [ ! -d "$HESTIA/data/users/$3" ]; then
             check_result "$E_NOTEXIST" "$1 $3 doesn't exist"
         fi
+    elif [ $2 = 'KEY' ]; then
+        local key="$(basename "$3")"
+
+        if [[ -z "$key" || ${#key} -lt 16 ]] || [[ ! -f "$HESTIA/data/access-keys/${key}" && ! -f "$HESTIA/data/access-keys/$key" ]]; then
+            check_result "$E_NOTEXIST" "$1 $3 doesn't exist"
+        fi
     else
         object=$(grep "$2='$3'" $HESTIA/data/users/$user/$1.conf)
         if [ -z "$object" ]; then
@@ -434,7 +444,7 @@ get_object_value() {
 }
 
 get_object_values() {
-    parse_object_kv_list $(grep "$2='$3'" $USER_DATA/$1.conf)   
+    parse_object_kv_list $(grep "$2='$3'" $USER_DATA/$1.conf)
 }
 
 # Update object value
@@ -561,7 +571,7 @@ recalc_user_disk_usage() {
         usage=0
         dusage=$(grep 'U_DISK=' $USER_DATA/web.conf |\
             awk -F "U_DISK='" '{print $2}' | cut -f 1 -d \')
-        for disk_usage in $dusage; do 
+        for disk_usage in $dusage; do
                 usage=$((usage + disk_usage))
         done
         d=$(grep "U_DISK_WEB='" $USER_DATA/user.conf | cut -f 2 -d \')
@@ -573,7 +583,7 @@ recalc_user_disk_usage() {
         usage=0
         dusage=$(grep 'U_DISK=' $USER_DATA/mail.conf |\
             awk -F "U_DISK='" '{print $2}' | cut -f 1 -d \')
-        for disk_usage in $dusage; do 
+        for disk_usage in $dusage; do
                 usage=$((usage + disk_usage))
         done
         d=$(grep "U_DISK_MAIL='" $USER_DATA/user.conf | cut -f 2 -d \')
@@ -585,7 +595,7 @@ recalc_user_disk_usage() {
         usage=0
         dusage=$(grep 'U_DISK=' $USER_DATA/db.conf |\
             awk -F "U_DISK='" '{print $2}' | cut -f 1 -d \')
-        for disk_usage in $dusage; do 
+        for disk_usage in $dusage; do
                 usage=$((usage + disk_usage))
         done
         d=$(grep "U_DISK_DB='" $USER_DATA/user.conf | cut -f 2 -d \')
@@ -603,7 +613,7 @@ recalc_user_bandwidth_usage() {
     usage=0
     bandwidth_usage=$(grep 'U_BANDWIDTH=' $USER_DATA/web.conf |\
         awk -F "U_BANDWIDTH='" '{print $2}'|cut -f 1 -d \')
-    for bandwidth in $bandwidth_usage; do 
+    for bandwidth in $bandwidth_usage; do
         usage=$((usage + bandwidth))
     done
     old=$(grep "U_BANDWIDTH='" $USER_DATA/user.conf | cut -f 2 -d \')
@@ -633,7 +643,7 @@ sync_cron_jobs() {
     else
         crontab="/var/spool/cron/$user"
     fi
-    
+
     # remove file if exists
     if [ -e "$crontab" ]; then
         rm -f $crontab
@@ -641,14 +651,14 @@ sync_cron_jobs() {
 
     # touch new crontab file
     touch $crontab
-        
+
     if [ "$CRON_REPORTS" = 'yes' ]; then
         echo "MAILTO=$CONTACT" > $crontab
         echo 'CONTENT_TYPE="text/plain; charset=utf-8"' >> $crontab
     else
         echo 'MAILTO=""' > $crontab
     fi
-    
+
     while read line; do
         parse_object_kv_list "$line"
         if [ "$SUSPENDED" = 'no' ]; then
@@ -722,11 +732,11 @@ is_ipv6_format_valid() {
     t_ip=$(echo $1 |awk -F / '{print $1}')
     t_cidr=$(echo $1 |awk -F / '{print $2}')
     valid_cidr=1
-    
+
     WORD="[0-9A-Fa-f]\{1,4\}"
     # flat address, no compressed words
     FLAT="^${WORD}\(:${WORD}\)\{7\}$"
-    
+
     COMP2="^\(${WORD}:\)\{1,1\}\(:${WORD}\)\{1,6\}$"
     COMP3="^\(${WORD}:\)\{1,2\}\(:${WORD}\)\{1,5\}$"
     COMP4="^\(${WORD}:\)\{1,3\}\(:${WORD}\)\{1,4\}$"
@@ -737,12 +747,12 @@ is_ipv6_format_valid() {
     EDGE_TAIL="^\(\(${WORD}:\)\{1,7\}\|:\):$"
     # leading :: edge case
     EDGE_LEAD="^:\(:${WORD}\)\{1,7\}$"
-   
+
     echo $t_ip | grep --silent "\(${FLAT}\)\|\(${COMP2}\)\|\(${COMP3}\)\|\(${COMP4}\)\|\(${COMP5}\)\|\(${COMP6}\)\|\(${COMP7}\)\|\(${EDGE_TAIL}\)\|\(${EDGE_LEAD}\)"
     if [ $? -ne 0 ]; then
         check_result "$E_INVALID" "invalid $object_name format :: $1"
     fi
-    
+
     if [ -n "$(echo $1|grep '/')" ]; then
         if [[ "$t_cidr" -lt 0 ]] || [[ "$t_cidr" -gt 128 ]]; then
             valid_cidr=0
@@ -806,7 +816,7 @@ is_ip46_format_valid() {
                 valid_cidr=0
             fi
         fi
-        
+
         if [ -n "$ipv6_valid" ] || [ "$valid_cidr" -eq 0 ]; then
             check_result "$E_INVALID" "invalid IP format :: $1"
         fi
@@ -972,7 +982,7 @@ is_fw_port_format_valid() {
 
 # Integer validator
 is_int_format_valid() {
-    if ! [[ "$1" =~ ^[0-9]+$ ]] ; then 
+    if ! [[ "$1" =~ ^[0-9]+$ ]] ; then
         check_result "$E_INVALID" "invalid $2 format :: $1"
     fi
 }
@@ -999,7 +1009,7 @@ is_cron_format_valid() {
     if [ "$2" = 'hour' ]; then
         limit=23
     fi
-    
+
     if [ "$2" = 'day' ]; then
         limit=31
     fi
@@ -1054,7 +1064,7 @@ is_object_format_valid() {
     fi
 }
 
-# Role validator 
+# Role validator
 is_role_valid (){
     if ! [[ "$1" =~ ^admin|user$ ]]; then
         check_result "$E_INVALID" "invalid $2 format :: $1"
@@ -1067,15 +1077,15 @@ is_password_format_valid() {
         check_result "$E_INVALID" "invalid password format :: $1"
     fi
 }
-# Missing function - 
-# Before: validate_format_shell 
+# Missing function -
+# Before: validate_format_shell
 # After: is_format_valid_shell
-is_format_valid_shell() {	
-    if [ -z "$(grep -w $1 /etc/shells)" ]; then	
-        echo "Error: shell $1 is not valid"	
-        log_event "$E_INVALID" "$EVENT"	
-        exit $E_INVALID	
-    fi	
+is_format_valid_shell() {
+    if [ -z "$(grep -w $1 /etc/shells)" ]; then
+        echo "Error: shell $1 is not valid"
+        log_event "$E_INVALID" "$EVENT"
+        exit $E_INVALID
+    fi
 }
 
 # Service name validator
@@ -1088,7 +1098,7 @@ is_service_format_valid() {
 is_hash_format_valid() {
   if ! [[ "$1" =~ ^[-_A-Za-z0-9]{1,32}$ ]]; then
         check_result "$E_INVALID" "invalid $2 format :: $1"
-    fi    
+    fi
 }
 
 # Format validation controller
@@ -1097,6 +1107,7 @@ is_format_valid() {
         eval arg=\$$arg_name
         if [ -n "$arg" ]; then
             case $arg_name in
+                access_key_id)  is_access_key_id_format_valid "$arg" "$arg_name";;
                 account)        is_user_format_valid "$arg" "$arg_name";;
                 action)         is_fw_action_format_valid "$arg";;
                 active)         is_boolean_format_valid "$arg" 'active' ;;
@@ -1159,14 +1170,15 @@ is_format_valid() {
                 proxy_ext)      is_extention_format_valid "$arg" ;;
                 quota)          is_int_format_valid "$arg" 'quota' ;;
                 rate)           is_int_format_valid "$arg" 'rate' ;;
-                                
+
                 record)         is_common_format_valid "$arg" 'record';;
                 restart)        is_restart_format_valid "$arg" 'restart' ;;
                 role)           is_role_valid "$arg" 'role' ;;
                 rtype)          is_dns_type_format_valid "$arg" ;;
                 rule)           is_int_format_valid "$arg" "rule id" ;;
                 service)        is_service_format_valid "$arg" "$arg_name" ;;
-                soa)            is_domain_format_valid "$arg" 'SOA' ;;	
+                secret_access_key) is_secret_access_key_format_valid "$arg" "$arg_name" ;;
+                soa)            is_domain_format_valid "$arg" 'SOA' ;;
                 #missing command: is_format_valid_shell
                 shell)          is_format_valid_shell "$arg" ;;
                 stats_pass)     is_password_format_valid "$arg" ;;
@@ -1181,6 +1193,113 @@ is_format_valid() {
     done
 }
 
+# Check access_key_id name
+# Don't work with legacy key format
+is_access_key_id_format_valid() {
+    local hash="$1"
+
+    # ACCESS_KEY_ID format validation
+    if ! [[ "$hash" =~ ^[[:alnum:]]{20}$ ]]; then
+        check_result "$E_INVALID" "invalid $2 format :: $hash"
+    fi
+}
+
+# SECRET_ACCESS_KEY format validation
+is_secret_access_key_format_valid() {
+    local hash="$1"
+
+    if ! [[ "$hash" =~ ^[[:alnum:]|_|\.|\+|/|\^|~|=|%|\-]{40}$ ]]; then
+        check_result "$E_INVALID" "invalid $2 format"
+    fi
+}
+
+# Checks if the secret belongs to the access key
+check_access_key_secret() {
+    local access_key_id="$(basename "$1")"
+    local secret_access_key=$2
+    local -n key_user=$3
+
+    if [[ -z "$access_key_id" || ! -f "$HESTIA/data/access-keys/${access_key_id}" ]]; then
+        check_result "$E_PASSWORD" "Access key $access_key_id doesn't exist"
+    fi
+
+    if [[ -z "$secret_access_key" ]]; then
+        check_result "$E_PASSWORD" "Secret key not provided for key $access_key_id"
+    elif ! [[ "$secret_access_key" =~ ^[[:alnum:]|_|\.|\+|/|\^|~|=|%|\-]{40}$ ]]; then
+        check_result "$E_PASSWORD" "Invalid secret key for key $access_key_id"
+    else
+        SECRET_ACCESS_KEY=""
+        source_conf "$HESTIA/data/access-keys/${access_key_id}";
+
+        if [[ -z "$SECRET_ACCESS_KEY" || "$SECRET_ACCESS_KEY" != "$secret_access_key" ]]; then
+            check_result "$E_PASSWORD" "Invalid secret key for key $access_key_id"
+        fi
+    fi
+
+    key_user="$USER"
+}
+
+# Checks if the key belongs to the user
+check_access_key_user() {
+    local access_key_id="$(basename "$1")"
+    local user=$2
+
+    if [[ -z "$access_key_id" || ! -f "$HESTIA/data/access-keys/${access_key_id}" ]]; then
+        check_result "$E_FORBIDEN" "Access key $access_key_id doesn't exist"
+    fi
+
+    if [[ -z "$user" ]]; then
+        check_result "$E_FORBIDEN" "User not provided"
+    else
+        USER=""
+        source_conf "$HESTIA/data/access-keys/${access_key_id}";
+
+        if [[ -z "$USER" || "$USER" != "$user" ]]; then
+            check_result "$E_FORBIDEN" "key $access_key_id does not belong to the user $user"
+        fi
+    fi
+}
+
+# Checks if the key is allowed to run the command
+check_access_key_cmd() {
+    local access_key_id="$(basename "$1")"
+    local cmd=$2
+    local -n user_arg_position=$3
+
+    if [[ -z "$access_key_id" || ! -f "$HESTIA/data/access-keys/${access_key_id}" ]]; then
+        check_result "$E_FORBIDEN" "Access key $access_key_id doesn't exist"
+    fi
+
+    if [[ -z "$cmd" ]]; then
+        check_result "$E_FORBIDEN" "Command not provided"
+    elif [[ ! -e "$BIN/$cmd" ]]; then
+        check_result "$E_FORBIDEN" "Command $cmd not found"
+    else
+        USER="" PERMISSIONS=""
+        source_conf "${HESTIA}/data/access-keys/${access_key_id}"
+
+        local allowed_commands
+        if [[ -n "$PERMISSIONS" ]]; then
+            allowed_commands="$(get_apis_commands "$PERMISSIONS")"
+            if [[ -z "$(echo ",${allowed_commands}," | grep ",${hst_command},")" ]]; then
+                check_result "$E_FORBIDEN" "Key $access_key_id don't have permission to run the command $hst_command"
+            fi
+        elif [[ -z "$PERMISSIONS" && "$USER" != "admin" ]]; then
+            check_result "$E_FORBIDEN" "Key $access_key_id don't have permission to run the command $hst_command"
+        fi
+
+        if [[ "$USER" == "admin" ]]; then
+            # Admin can run commands for any user
+            user_arg_position="0"
+        else
+            user_arg_position="$(search_command_arg_position "$hst_command" "USER")"
+            if ! [[ "$user_arg_position" =~ ^[0-9]+$ ]]; then
+                check_result "$E_FORBIDEN" "Command $hst_command not found"
+            fi
+        fi
+    fi
+}
+
 # Domain argument formatting
 format_domain() {
     if [[ "$domain" = *[![:ascii:]]* ]]; then
@@ -1364,8 +1483,8 @@ source_conf(){
       if [[ ! $lhs =~ ^\ *# && -n $lhs ]]; then
           rhs="${rhs%%^\#*}"   # Del in line right comments
           rhs="${rhs%%*( )}"   # Del trailing spaces
-          rhs="${rhs%\'*}"     # Del opening string quotes 
-          rhs="${rhs#\'*}"     # Del closing string quotes 
+          rhs="${rhs%\'*}"     # Del opening string quotes
+          rhs="${rhs#\'*}"     # Del closing string quotes
           declare -g $lhs="$rhs"
       fi
   done < $1
@@ -1392,3 +1511,112 @@ change_sys_value(){
       sed -i "s|^$1=.*|$1='$2'|g" "$HESTIA/conf/hestia.conf"
   fi
 }
+
+# Checks the format of APIs that will be allowed for the key
+is_key_permissions_format_valid() {
+    local permissions="$1"
+    local user="$2"
+
+    if [[ "$user" != "admin" && -z "$permissions" ]]; then
+        check_result "$E_INVALID" "Non-admin users need a permission list"
+    fi
+
+    while IFS=',' read -ra permissions_arr; do
+        for permission in "${permissions_arr[@]}"; do
+            permission="$(basename "$permission" | sed -E "s/^\s*|\s*$//g")"
+
+#            if [[ -z "$(echo "$permission" | grep -E "^v-")" ]]; then
+            if [[ ! -e "$HESTIA/data/api/$permission" ]]; then
+                check_result "$E_NOTEXIST" "API $permission doesn't exist"
+            fi
+
+            source_conf "$HESTIA/data/api/$permission";
+            if [ "$ROLE" = "admin" ] && [ "$user" != "admin" ]; then
+              check_result "$E_INVALID" "Only the admin can run this API"
+            fi
+#            elif [[ ! -e "$BIN/$permission" ]]; then
+#                check_result "$E_NOTEXIST" "Command $permission doesn't exist"
+#            fi
+        done
+    done <<<"$permissions"
+}
+
+# Remove whitespaces, and bin path from commands
+cleanup_key_permissions() {
+    local permissions="$1"
+
+    local final quote
+    while IFS=',' read -ra permissions_arr; do
+        for permission in "${permissions_arr[@]}"; do
+            permission="$(basename "$permission" | sed -E "s/^\s*|\s*$//g")"
+
+            # Avoid duplicate items
+            if [[ -z "$(echo ",${final}," | grep ",${permission},")" ]]; then
+                final+="${quote}${permission}"
+                quote=','
+            fi
+        done
+    done <<<"$permissions"
+
+    echo "$final"
+}
+
+# Extract all allowed commands from a permission list
+get_apis_commands() {
+    local permissions="$1"
+
+    local allowed_commands quote commands_to_add
+    while IFS=',' read -ra permissions_arr; do
+        for permission in "${permissions_arr[@]}"; do
+            permission="$(basename "$permission" | sed -E "s/^\s*|\s*$//g")"
+
+            commands_to_add=""
+#            if [[ -n "$(echo "$permission" | grep -E "^v-")" ]]; then
+#                commands_to_add="$permission"
+#            el
+            if [[  -e "$HESTIA/data/api/$permission" ]]; then
+                source_conf "$HESTIA/data/api/$permission";
+                commands_to_add="$COMMANDS"
+            fi
+
+            if [[ -n "$commands_to_add" ]]; then
+                allowed_commands+="${quote}${commands_to_add}"
+                quote=','
+            fi
+        done
+    done <<<"$permissions"
+
+    cleanup_key_permissions "$allowed_commands"
+}
+
+# Get the position of an argument by name in a hestia command using the command's documentation comment.
+#
+# Return:
+# * 0:   It doesn't have the argument;
+# * 1-9: The position of the argument in the command.
+search_command_arg_position() {
+    local hst_command="$(basename "$1")"
+    local arg_name="$2"
+
+    local command_path="$BIN/$hst_command"
+    if [[ -z "$hst_command" || ! -e "$command_path" ]]; then
+        echo "-1"
+        return
+    fi
+
+    local position=0
+    local count=0
+    local command_options="$(sed -En 's/^# options: (.+)/\1/p' "$command_path")"
+    while IFS=' ' read -ra options_arr; do
+        for option in "${options_arr[@]}"; do
+            count=$((count+1))
+
+            option_name="$(echo "  $option   " | sed -E 's/^(\s|\[)*|(\s|\])*$//g')"
+            if [[ "${option_name^^}" == "$arg_name" ]]; then
+                position=$count
+            fi
+        done
+    done <<<"$command_options"
+
+    echo "$position"
+}

+ 38 - 32
func/syshealth.sh

@@ -9,7 +9,7 @@
 # Read known configuration keys from $HESTIA/conf/defaults/$system.conf
 function read_kv_config_file() {
     local system=$1
-    
+
     if [ ! -f "$HESTIA/conf/defaults/$system.conf" ]; then
         write_kv_config_file $system
     fi
@@ -150,7 +150,7 @@ function syshealth_repair_web_config() {
     prev="DOMAIN"
     for key in $known_keys; do
         if [ -z "${!key}" ]; then
-            add_object_key 'web' 'DOMAIN' "$domain" "$key" "$prev"   
+            add_object_key 'web' 'DOMAIN' "$domain" "$key" "$prev"
         fi
         prev=$key
     done
@@ -163,7 +163,7 @@ function syshealth_repair_mail_config() {
     prev="DOMAIN"
     for key in $known_keys; do
         if [ -z "${!key}" ]; then
-            add_object_key 'mail' 'DOMAIN' "$domain" "$key" "$prev"   
+            add_object_key 'mail' 'DOMAIN' "$domain" "$key" "$prev"
         fi
         prev=$key
     done
@@ -175,7 +175,7 @@ function syshealth_repair_mail_account_config() {
     get_object_values "mail/$domain" 'ACCOUNT' "$account"
     for key in $known_keys; do
         if [ -z "${!key}" ]; then
-            add_object_key "mail/$domain" 'ACCOUNT' "$account" "$key" "$prev"    
+            add_object_key "mail/$domain" 'ACCOUNT' "$account" "$key" "$prev"
         fi
         prev=$key
     done
@@ -206,7 +206,7 @@ function syshealth_restore_system_config() {
 }
 
 function check_key_exists() {
-    grep -e "^$1=" $HESTIA/conf/hestia.conf 
+    grep -e "^$1=" $HESTIA/conf/hestia.conf
 }
 
 # Repair System Configuration
@@ -228,13 +228,13 @@ function syshealth_repair_system_config() {
     # phpMyAdmin/phpPgAdmin alias
     if [ -n "$DB_SYSTEM" ]; then
         if [ "$DB_SYSTEM" = "mysql" ]; then
-            if [[ -z $(check_key_exists 'DB_PMA_ALIAS') ]]; then 
+            if [[ -z $(check_key_exists 'DB_PMA_ALIAS') ]]; then
                 echo "[ ! ] Adding missing variable to hestia.conf: DB_PMA_ALIAS ('phpmyadmin)"
                 $BIN/v-change-sys-config-value 'DB_PMA_ALIAS' 'phpmyadmin'
             fi
         fi
         if [ "$DB_SYSTEM" = "pgsql" ]; then
-            if [[ -z $(check_key_exists 'DB_PGA_ALIAS') ]]; then 
+            if [[ -z $(check_key_exists 'DB_PGA_ALIAS') ]]; then
                 echo "[ ! ] Adding missing variable to hestia.conf: DB_PGA_ALIAS ('phppgadmin')"
                 $BIN/v-change-sys-config-value 'DB_PGA_ALIAS' 'phppgadmin'
             fi
@@ -242,50 +242,50 @@ function syshealth_repair_system_config() {
     fi
 
     # Backup compression level
-    if [[ -z $(check_key_exists 'BACKUP_GZIP') ]]; then 
+    if [[ -z $(check_key_exists 'BACKUP_GZIP') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: BACKUP_GZIP ('4')"
         $BIN/v-change-sys-config-value 'BACKUP_GZIP' '4'
     fi
 
     # Theme
-    if [[ -z $(check_key_exists 'THEME') ]]; then 
+    if [[ -z $(check_key_exists 'THEME') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: THEME ('dark')"
         $BIN/v-change-sys-config-value 'THEME' 'dark'
     fi
 
     # Default language
-    if [[ -z  $(check_key_exists 'LANGUAGE') ]]; then 
+    if [[ -z  $(check_key_exists 'LANGUAGE') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: LANGUAGE ('en')"
         $BIN/v-change-sys-language 'LANGUAGE' 'en'
     fi
 
     # Disk Quota
-    if [[ -z $(check_key_exists 'DISK_QUOTA') ]]; then 
+    if [[ -z $(check_key_exists 'DISK_QUOTA') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: DISK_QUOTA ('no')"
         $BIN/v-change-sys-config-value 'DISK_QUOTA' 'no'
     fi
 
     # CRON daemon
-    if [[ -z $(check_key_exists 'CRON_SYSTEM') ]]; then 
+    if [[ -z $(check_key_exists 'CRON_SYSTEM') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: CRON_SYSTEM ('cron')"
         $BIN/v-change-sys-config-value 'CRON_SYSTEM' 'cron'
     fi
 
     # Backend port
-    if [[ -z $(check_key_exists 'BACKEND_PORT') ]]; then 
+    if [[ -z $(check_key_exists 'BACKEND_PORT') ]]; then
         ORIGINAL_PORT=$(cat $HESTIA/nginx/conf/nginx.conf | grep "listen" | sed 's/[^0-9]*//g')
         echo "[ ! ] Adding missing variable to hestia.conf: BACKEND_PORT ('$ORIGINAL_PORT')"
         $HESTIA/bin/v-change-sys-config-value 'BACKEND_PORT' $ORIGINAL_PORT
     fi
 
     # Upgrade: Send email notification
-    if [[ -z $(check_key_exists 'UPGRADE_SEND_EMAIL') ]]; then 
+    if [[ -z $(check_key_exists 'UPGRADE_SEND_EMAIL') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: UPGRADE_SEND_EMAIL ('true')"
         $BIN/v-change-sys-config-value 'UPGRADE_SEND_EMAIL' 'true'
     fi
 
     # Upgrade: Send email notification
-    if [[ -z $(check_key_exists 'UPGRADE_SEND_EMAIL_LOG') ]]; then 
+    if [[ -z $(check_key_exists 'UPGRADE_SEND_EMAIL_LOG') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: UPGRADE_SEND_EMAIL_LOG ('false')"
         $BIN/v-change-sys-config-value 'UPGRADE_SEND_EMAIL_LOG' 'false'
     fi
@@ -295,22 +295,22 @@ function syshealth_repair_system_config() {
         echo "[ ! ] Adding missing variable to hestia.conf: FILE_MANAGER ('true')"
         $BIN/v-add-sys-filemanager quiet
     fi
-    
+
     # Support for ZSTD / GZIP Change
     if [[ -z $(check_key_exists 'BACKUP_MODE') ]]; then
         echo "[ ! ] Setting zstd backup compression type as default..."
         $BIN/v-change-sys-config-value "BACKUP_MODE" "zstd"
     fi
-    
+
     # Login style switcher
     if [[ -z $(check_key_exists 'LOGIN_STYLE') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: LOGIN_STYLE ('default')"
         $BIN/v-change-sys-config-value "LOGIN_STYLE" "default"
     fi
-    
+
     # Webmail clients
     if [[ -z $(check_key_exists 'WEBMAIL_SYSTEM') ]]; then
-        if [ -d "/var/lib/roundcube" ]; then 
+        if [ -d "/var/lib/roundcube" ]; then
             echo "[ ! ] Adding missing variable to hestia.conf: WEBMAIL_SYSTEM ('roundcube')"
             $BIN/v-change-sys-config-value "WEBMAIL_SYSTEM" "roundcube"
         else
@@ -330,23 +330,29 @@ function syshealth_repair_system_config() {
         echo "[ ! ] Adding missing variable to hestia.conf: ENFORCE_SUBDOMAIN_OWNERSHIP ('no')"
         $BIN/v-change-sys-config-value "ENFORCE_SUBDOMAIN_OWNERSHIP" "no"
     fi
-    
-    if [[ -z $(check_key_exists 'API') ]]; then 
-        echo "[ ! ] Adding missing variable to hestia.conf: API ('no')"   
+
+    if [[ -z $(check_key_exists 'API') ]]; then
+        echo "[ ! ] Adding missing variable to hestia.conf: API ('no')"
         $BIN/v-change-sys-config-value "API" "no"
     fi
-    
+
+    # Enable API V2
+    if [[ -z $(check_key_exists 'API_SYSTEM') ]]; then
+        echo "[ ! ] Adding missing variable to hestia.conf: API_SYSTEM ('0')"
+        $BIN/v-change-sys-config-value "API_SYSTEM" "0"
+    fi
+
     # API access allowed IP's
     if [ "$API" = "yes" ]; then
         check_api_key=$(grep "API_ALLOWED_IP" $HESTIA/conf/hestia.conf)
         if [ -z "$check_api_key" ]; then
             if [[ -z $(check_key_exists 'API_ALLOWED_IP') ]]; then
-                echo "[ ! ] Adding missing variable to hestia.conf: API_ALLOWED_IP ('allow-all')"        
+                echo "[ ! ] Adding missing variable to hestia.conf: API_ALLOWED_IP ('allow-all')"
                 $BIN/v-change-sys-config-value "API_ALLOWED_IP" "allow-all"
             fi
         fi
     fi
-    
+
     # Enforce subdomain ownership
     if [[ -z $(check_key_exists 'ENFORCE_SUBDOMAIN_OWNERSHIP') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: ENFORCE_SUBDOMAIN_OWNERSHIP ('yes')"
@@ -382,12 +388,12 @@ function syshealth_repair_system_config() {
     if [[ -z $(check_key_exists 'POLICY_USER_CHANGE_THEME') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: POLICY_USER_CHANGE_THEME ('yes')"
         $BIN/v-change-sys-config-value "POLICY_USER_CHANGE_THEME" "true"
-    fi    
+    fi
     # Protect admin user
     if [[ -z $(check_key_exists 'POLICY_SYSTEM_PROTECTED_ADMIN') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: POLICY_SYSTEM_PROTECTED_ADMIN ('no')"
         $BIN/v-change-sys-config-value "POLICY_SYSTEM_PROTECTED_ADMIN" "no"
-    fi  
+    fi
     # Allow user delete logs
     if [[ -z $(check_key_exists 'POLICY_USER_DELETE_LOGS') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: POLICY_USER_DELETE_LOGS ('yes')"
@@ -423,7 +429,7 @@ function syshealth_repair_system_config() {
         echo "[ ! ] Adding missing variable to hestia.conf: PHPMYADMIN_KEY ('')"
         $BIN/v-change-sys-config-value "PHPMYADMIN_KEY" ""
     fi
-    # Use SMTP server for hestia internal mail 
+    # Use SMTP server for hestia internal mail
     if [[ -z $(check_key_exists 'USE_SERVER_SMTP') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: USE_SERVER_SMTP ('')"
         $BIN/v-change-sys-config-value "USE_SERVER_SMTP" "false"
@@ -448,20 +454,20 @@ function syshealth_repair_system_config() {
         echo "[ ! ] Adding missing variable to hestia.conf: SERVER_SMTP_USER ('')"
         $BIN/v-change-sys-config-value "SERVER_SMTP_USER" ""
     fi
-    
+
     if [[ -z $(check_key_exists 'SERVER_SMTP_PASSWD') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: SERVER_SMTP_PASSWD ('')"
         $BIN/v-change-sys-config-value "SERVER_SMTP_PASSWD" ""
     fi
-        
+
     if [[ -z $(check_key_exists 'SERVER_SMTP_ADDR') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: SERVER_SMTP_ADDR ('')"
         $BIN/v-change-sys-config-value "SERVER_SMTP_ADDR" ""
-    fi    
+    fi
     if [[ -z $(check_key_exists 'POLICY_CSRF_STRICTNESS') ]]; then
         echo "[ ! ] Adding missing variable to hestia.conf: POLICY_CSRF_STRICTNESS ('')"
         $BIN/v-change-sys-config-value "POLICY_CSRF_STRICTNESS" "1"
-    fi  
+    fi
 }
 
 # Repair System Cron Jobs

+ 2 - 0
install/deb/api/billing

@@ -0,0 +1,2 @@
+ROLE='admin'
+COMMANDS='v-add-user,v-delete-user,v-suspend-user,v-unsuspend-user,v-change-user-shell,v-list-user,v-list-users,v-make-tmp-file,v-add-domain,v-change-user-package'

+ 2 - 0
install/deb/api/mail-accounts

@@ -0,0 +1,2 @@
+ROLE='user'
+COMMANDS='v-list-mail-domains,v-list-mail-domain,v-list-mail-account,v-list-mail-accounts,v-list-mail-account-autoreply,v-delete-mail-account,v-delete-mail-account-alias,v-delete-mail-account-autoreply,v-delete-mail-account-forward,v-delete-mail-account-fwd-only,v-add-mail-account,v-add-mail-account-alias,v-add-mail-account-autoreply,v-add-mail-account-forward,v-add-mail-account-fwd-only,v-change-mail-account-password,v-change-mail-account-quota,v-suspend-mail-account,v-suspend-mail-accounts,v-unsuspend-mail-account,v-unsuspend-mail-accounts'

+ 2 - 0
install/deb/api/phpmyadmin-sso

@@ -0,0 +1,2 @@
+ROLE='admin'
+COMMANDS='v-add-database-temp-user,v-delete-database-temp-user'

+ 2 - 0
install/deb/api/purge-nginx-cache

@@ -0,0 +1,2 @@
+ROLE='user'
+COMMANDS='v-purge-nginx-cache,v-list-web-domains,v-list-web-domain'

+ 2 - 0
install/deb/api/sync-dns-cluster

@@ -0,0 +1,2 @@
+ROLE='admin'
+COMMANDS='v-delete-dns-domains-src,v-insert-dns-domain,v-insert-dns-records,v-rebuild-dns-domains,v-delete-dns-record'

+ 55 - 47
install/hst-install-debian.sh

@@ -47,10 +47,10 @@ software="nginx apache2 apache2-utils apache2-suexec-custom
   php$fpm_v-gd php$fpm_v-intl php$fpm_v-mbstring
   php$fpm_v-opcache php$fpm_v-pspell php$fpm_v-readline php$fpm_v-xml
   awstats vsftpd proftpd-basic bind9 exim4 exim4-daemon-heavy
-  clamav-daemon spamassassin dovecot-imapd dovecot-pop3d dovecot-sieve dovecot-managesieved 
+  clamav-daemon spamassassin dovecot-imapd dovecot-pop3d dovecot-sieve dovecot-managesieved
   net-tools mariadb-client mariadb-common mariadb-server postgresql
   postgresql-contrib phppgadmin mc flex whois git idn unzip zip sudo bc ftp lsof
-  rrdtool quota e2fslibs bsdutils e2fsprogs curl imagemagick fail2ban 
+  rrdtool quota e2fslibs bsdutils e2fsprogs curl imagemagick fail2ban
   dnsutils bsdmainutils cron hestia=${HESTIA_INSTALL_VER} hestia-nginx
   hestia-php expect libmail-dkim-perl unrar-free vim-common acl sysstat
   rsyslog openssh-server util-linux ipset libapache2-mpm-itk zstd
@@ -100,14 +100,14 @@ download_file() {
 
 # Defining password-gen function
 gen_pass() {
-    matrix=$1 
-    length=$2 
-    if [ -z "$matrix" ]; then 
-        matrix="A-Za-z0-9" 
-    fi 
-    if [ -z "$length" ]; then 
-        length=16 
-    fi 
+    matrix=$1
+    length=$2
+    if [ -z "$matrix" ]; then
+        matrix="A-Za-z0-9"
+    fi
+    if [ -z "$length" ]; then
+        length=16
+    fi
     head /dev/urandom | tr -dc $matrix | head -c$length
 }
 
@@ -307,7 +307,7 @@ if [ "$exim" = 'no' ]; then
     spamd='no'
     dovecot='no'
 fi
-if [ "$dovecot" = "no" ]; then 
+if [ "$dovecot" = "no" ]; then
   sieve='no'
 fi
 if [ "$iptables" = 'no' ]; then
@@ -344,9 +344,9 @@ fi
 
 # Welcome message
 echo "Welcome to the Hestia Control Panel installer!"
-echo 
+echo
 echo "Please wait, the installer is now checking for missing dependencies..."
-echo 
+echo
 
 # Update apt repository
 apt-get -qq update
@@ -453,7 +453,7 @@ if [ -z "$withdebs" ] || [ ! -d "$withdebs" ]; then
     fi
 fi
 
-case $architecture in 
+case $architecture in
 x86_64)
     ARCH="amd64"
     ;;
@@ -498,7 +498,7 @@ install_welcome_message() {
     echo "                            www.hestiacp.com                            "
     echo
     echo "========================================================================"
-    echo 
+    echo
     echo "Thank you for downloading Hestia Control Panel! In a few moments,"
     echo "we will begin installing the following components on your server:"
     echo
@@ -551,7 +551,7 @@ if [ "$exim" = 'yes' ]; then
     fi
 fi
 
-echo 
+echo
 # Database stack
 if [ "$mysql" = 'yes' ]; then
     echo '   - MariaDB Database Server'
@@ -588,7 +588,7 @@ if [ "$interactive" = 'yes' ]; then
     fi
 fi
 
-# Validate Email / Hostname even when interactive = no 
+# Validate Email / Hostname even when interactive = no
 # Asking for contact email
 if [ -z "$email" ]; then
     while validate_email; do
@@ -1225,6 +1225,9 @@ cp -rf $HESTIA_INSTALL_DIR/templates/web/skel/document_errors/* /var/www/documen
 # Installing firewall rules
 cp -rf $HESTIA_INSTALL_DIR/firewall $HESTIA/data/
 
+# Installing apis
+cp -rf $HESTIA_INSTALL_DIR/api $HESTIA/data/
+
 # Configuring server hostname
 $HESTIA/bin/v-change-sys-hostname $servername > /dev/null 2>&1
 
@@ -1379,7 +1382,7 @@ if [ "$phpfpm" = "yes" ]; then
         echo "[ * ] Install  PHP $fpm_v..."
         $HESTIA/bin/v-add-web-php "$fpm_v" > /dev/null 2>&1
   fi
-  
+
   echo "[ * ] Configuring PHP $fpm_v..."
   # Create www.conf for webmail and php(*)admin
   cp -f $HESTIA_INSTALL_DIR/php-fpm/www.conf /etc/php/$fpm_v/fpm/pool.d/www.conf
@@ -1441,15 +1444,15 @@ if [ "$proftpd" = 'yes' ]; then
     echo "127.0.0.1 $servername" >> /etc/hosts
     cp -f $HESTIA_INSTALL_DIR/proftpd/proftpd.conf /etc/proftpd/
     cp -f $HESTIA_INSTALL_DIR/proftpd/tls.conf /etc/proftpd/
-    
-    if [ "$release" -eq 11 ]; then 
+
+    if [ "$release" -eq 11 ]; then
       sed -i 's|IdentLookups                  off|#IdentLookups                  off|g' /etc/proftpd/proftpd.conf
     fi
-    
+
     update-rc.d proftpd defaults > /dev/null 2>&1
     systemctl start proftpd >> $LOG
     check_result $? "proftpd start failed"
-    
+
     if [ "$release" -eq 11 ]; then
      unit_files="$(systemctl list-unit-files |grep proftpd)"
        if [[ "$unit_files" =~ "disabled" ]]; then
@@ -1473,7 +1476,7 @@ if [ "$mysql" = 'yes' ]; then
         mycnf="my-large.cnf"
     fi
 
-    # Run mysql_install_db 
+    # Run mysql_install_db
     mysql_install_db >> $LOG
     # Remove symbolic link
     rm -f /etc/mysql/my.cnf
@@ -1488,7 +1491,7 @@ if [ "$mysql" = 'yes' ]; then
     mpass=$(gen_pass)
     echo -e "[client]\npassword='$mpass'\n" > /root/.my.cnf
     chmod 600 /root/.my.cnf
-    
+
     # Ater root password
     mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '$mpass'; FLUSH PRIVILEGES;"
     # Allow mysql access via socket for startup
@@ -1544,7 +1547,7 @@ if [ "$mysql" = 'yes' ]; then
     # Create temporary folder and change permission
     chmod 770 /usr/share/phpmyadmin/tmp
     chown root:www-data /usr/share/phpmyadmin/tmp
-    
+
     # Generate blow fish
     blowfish=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 32)
     sed -i "s|%blowfish_secret%|$blowfish|" /etc/phpmyadmin/config.inc.php
@@ -1560,8 +1563,8 @@ if [ "$mysql" = 'yes' ]; then
     # https://github.com/skurudo/phpmyadmin-fixer
     # shellcheck source=/usr/local/hestia/install/deb/phpmyadmin/pma.sh
     source $HESTIA_INSTALL_DIR/phpmyadmin/pma.sh > /dev/null 2>&1
-    
-    # limit access to /etc/phpmyadmin/ 
+
+    # limit access to /etc/phpmyadmin/
     chown -R root:www-data /etc/phpmyadmin/
     chmod -R 640  /etc/phpmyadmin/*
     chmod 750 /etc/phpmyadmin/conf.d/
@@ -1675,16 +1678,16 @@ if [ "$dovecot" = 'yes' ]; then
     cp -f $HESTIA_INSTALL_DIR/logrotate/dovecot /etc/logrotate.d/
     chown -R root:root /etc/dovecot*
     rm -f /etc/dovecot/conf.d/15-mailboxes.conf
-    
-    #Alter config for 2.2 
+
+    #Alter config for 2.2
     version=$(dovecot --version |  cut -f -2 -d .);
-    if [ "$version" = "2.2" ]; then 
-      echo "[ * ] Downgrade dovecot config to sync with 2.2 settings"	
+    if [ "$version" = "2.2" ]; then
+      echo "[ * ] Downgrade dovecot config to sync with 2.2 settings"
       sed -i 's|#ssl_dh_parameters_length = 4096|ssl_dh_parameters_length = 4096|g' /etc/dovecot/conf.d/10-ssl.conf
       sed -i 's|ssl_dh = </etc/ssl/dhparam.pem|#ssl_dh = </etc/ssl/dhparam.pem|g' /etc/dovecot/conf.d/10-ssl.conf
       sed -i 's|ssl_min_protocol = TLSv1.2|ssl_protocols = !SSLv3 !TLSv1 !TLSv1.1|g' /etc/dovecot/conf.d/10-ssl.conf
     fi
-    
+
     update-rc.d dovecot defaults
     systemctl start dovecot
     check_result $? "dovecot start failed"
@@ -1768,7 +1771,7 @@ if [ "$fail2ban" = 'yes' ]; then
         fline=$(cat /etc/fail2ban/jail.local |grep -n vsftpd-iptables -A 2)
         fline=$(echo "$fline" |grep enabled |tail -n1 |cut -f 1 -d -)
         sed -i "${fline}s/false/true/" /etc/fail2ban/jail.local
-    fi 
+    fi
     if [ ! -e /var/log/auth.log ]; then
         # Debian workaround: auth logging was moved to systemd
         touch /var/log/auth.log
@@ -1804,12 +1807,12 @@ if [ "$sieve" = 'yes' ]; then
     # Folder paths
     RC_INSTALL_DIR="/var/lib/roundcube"
     RC_CONFIG_DIR="/etc/roundcube"
-    
+
     echo "[ * ] Install Sieve..."
-     
+
     # dovecot.conf install
     sed -i "s/namespace/service stats \{\n  unix_listener stats-writer \{\n    group = mail\n    mode = 0660\n    user = dovecot\n  \}\n\}\n\nnamespace/g" /etc/dovecot/dovecot.conf
-    
+
     # dovecot conf files
     #  10-master.conf
     sed -i -E -z "s/  }\n  user = dovecot\n}/  \}\n  unix_listener auth-master \{\n    group = mail\n    mode = 0660\n    user = dovecot\n  \}\n  user = dovecot\n\}/g" /etc/dovecot/conf.d/10-master.conf
@@ -1817,24 +1820,24 @@ if [ "$sieve" = 'yes' ]; then
     sed -i "s/\#mail_plugins = \\\$mail_plugins/mail_plugins = \$mail_plugins quota sieve\n  auth_socket_path = \/var\/run\/dovecot\/auth-master/g" /etc/dovecot/conf.d/15-lda.conf
     #  20-imap.conf
     sed -i "s/mail_plugins = quota imap_quota/mail_plugins = quota imap_quota imap_sieve/g" /etc/dovecot/conf.d/20-imap.conf
-    
+
     # replace dovecot-sieve config files
     cp -f $HESTIA_INSTALL_DIR/dovecot/sieve/* /etc/dovecot/conf.d
-    
+
     echo -e "require [\"fileinto\"];\n# rule:[SPAM]\nif header :contains \"X-Spam-Flag\" \"YES\" {\n    fileinto \"INBOX.Spam\";\n}\n" > /etc/dovecot/sieve/default
-    
+
     # exim4 install
     sed -i "s/\stransport = local_delivery/ transport = dovecot_virtual_delivery/" /etc/exim4/exim4.conf.template
-    
+
    sed -i "s/address_pipe:/dovecot_virtual_delivery:\n  driver = pipe\n  command = \/usr\/lib\/dovecot\/dovecot-lda -e -d \$local_part@\$domain -f \$sender_address -a \$original_local_part@\$original_domain\n  delivery_date_add\n  envelope_to_add\n  return_path_add\n  log_output = true\n  log_defer_output = true\n  user = \${extract{2}{:}{\${lookup{\$local_part}lsearch{\/etc\/exim4\/domains\/\${lookup{\$domain}dsearch{\/etc\/exim4\/domains\/}}\/passwd}}}}\n group = mail\n  return_output\n\naddress_pipe:/g" /etc/exim4/exim4.conf.template
-    
-    
+
+
     # Modify Roundcube install
     mkdir -p $RC_CONFIG_DIR/plugins/managesieve
-    
+
     cp -f $HESTIA_INSTALL_DIR/roundcube/plugins/config_managesieve.inc.php $RC_CONFIG_DIR/plugins/managesieve/config.inc.php
         ln -s $RC_CONFIG_DIR/plugins/managesieve/config.inc.php $RC_INSTALL_DIR/plugins/managesieve/config.inc.php
-    
+
     # Permission changes
     chown -R dovecot:mail /var/log/dovecot.log
     chmod 660 /var/log/dovecot.log
@@ -1842,9 +1845,9 @@ if [ "$sieve" = 'yes' ]; then
     chmod 751 -R $RC_CONFIG_DIR
     chmod 644 $RC_CONFIG_DIR/*.php
     chmod 644 $RC_CONFIG_DIR/plugins/managesieve/config.inc.php
-    
+
     sed -i "s/'archive'/'archive', 'managesieve'/g" $RC_CONFIG_DIR/config.inc.php
-    
+
     # Restart Dovecot and exim4
     systemctl restart dovecot > /dev/null 2>&1
     systemctl restart exim4 > /dev/null 2>&1
@@ -1869,9 +1872,14 @@ $HESTIA/bin/v-add-sys-phpmailer quiet
 #----------------------------------------------------------#
 
 if [ "$api" = "yes" ]; then
+    # keep legacy api enabled until transition is complete
     write_config_value "API" "yes"
+    write_config_value "API_SYSTEM" "1"
     write_config_value "API_ALLOWED_IP" ""
 else
+    write_config_value "API" "no"
+    write_config_value "API_SYSTEM" "0"
+    write_config_value "API_ALLOWED_IP" ""
     $HESTIA/bin/v-change-sys-api disable
 fi
 
@@ -1997,7 +2005,7 @@ chown admin:admin $HESTIA/data/sessions
 mkdir -p /backup/
 chmod 755 /backup/
 
-# create cronjob to generate ssl 
+# create cronjob to generate ssl
 echo "@reboot root sleep 10 && rm /etc/cron.d/hestia-ssl && PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:' && /usr/local/hestia/bin/v-add-letsencrypt-host" > /etc/cron.d/hestia-ssl
 
 

+ 49 - 41
install/hst-install-ubuntu.sh

@@ -42,8 +42,8 @@ mariadb_v="10.6"
 # Defining software pack for all distros
 software="apache2 apache2.2-common apache2-suexec-custom apache2-utils
     apparmor-utils awstats bc bind9 bsdmainutils bsdutils clamav-daemon
-    cron curl dnsutils dovecot-imapd dovecot-pop3d dovecot-sieve dovecot-managesieved 
-    e2fslibs e2fsprogs exim4 exim4-daemon-heavy expect fail2ban flex ftp git idn 
+    cron curl dnsutils dovecot-imapd dovecot-pop3d dovecot-sieve dovecot-managesieved
+    e2fslibs e2fsprogs exim4 exim4-daemon-heavy expect fail2ban flex ftp git idn
     imagemagick libapache2-mod-fcgid libapache2-mod-php$fpm_v libapache2-mod-rpaf
     lsof mc mariadb-client mariadb-common mariadb-server nginx
     php$fpm_v php$fpm_v-cgi php$fpm_v-common php$fpm_v-curl
@@ -98,14 +98,14 @@ download_file() {
 
 # Defining password-gen function
 gen_pass() {
-    matrix=$1 
-    length=$2 
-    if [ -z "$matrix" ]; then 
-        matrix="A-Za-z0-9" 
-    fi 
-    if [ -z "$length" ]; then 
-        length=16 
-    fi 
+    matrix=$1
+    length=$2
+    if [ -z "$matrix" ]; then
+        matrix="A-Za-z0-9"
+    fi
+    if [ -z "$length" ]; then
+        length=16
+    fi
     head /dev/urandom | tr -dc $matrix | head -c$length
 }
 
@@ -342,9 +342,9 @@ fi
 
 # Welcome message
 echo "Welcome to the Hestia Control Panel installer!"
-echo 
+echo
 echo "Please wait, the installer is now checking for missing dependencies..."
-echo 
+echo
 
 # Update apt repository
 apt-get -qq update
@@ -444,7 +444,7 @@ if [ -z "$withdebs" ] || [ ! -d "$withdebs" ]; then
     fi
 fi
 
-case $architecture in 
+case $architecture in
     x86_64)
         ARCH="amd64"
         ;;
@@ -540,7 +540,7 @@ if [ "$exim" = 'yes' ]; then
     fi
 fi
 
-echo 
+echo
 # Database stack
 if [ "$mysql" = 'yes' ]; then
     echo '   - MariaDB Database Server'
@@ -577,7 +577,7 @@ if [ "$interactive" = 'yes' ]; then
     fi
 fi
 
-# Validate Email / Hostname even when interactive = no 
+# Validate Email / Hostname even when interactive = no
 # Asking for contact email
 if [ -z "$email" ]; then
     while validate_email; do
@@ -1282,6 +1282,9 @@ cp -rf $HESTIA_INSTALL_DIR/templates/web/skel/document_errors/* /var/www/documen
 # Installing firewall rules
 cp -rf $HESTIA_INSTALL_DIR/firewall $HESTIA/data/
 
+# Installing apis
+cp -rf $HESTIA_INSTALL_DIR/api $HESTIA/data/
+
 # Configuring server hostname
 $HESTIA/bin/v-change-sys-hostname $servername > /dev/null 2>&1
 
@@ -1451,7 +1454,7 @@ if [ "$phpfpm" = "yes" ]; then
         echo "[ * ] Install  PHP $fpm_v..."
         $HESTIA/bin/v-add-web-php "$fpm_v" > /dev/null 2>&1
   fi
-  
+
   echo "[ * ] Configuring PHP-FPM $fpm_v..."
   # Create www.conf for webmail and php(*)admin
   cp -f $HESTIA_INSTALL_DIR/php-fpm/www.conf /etc/php/$fpm_v/fpm/pool.d/www.conf
@@ -1540,8 +1543,8 @@ if [ "$mysql" = 'yes' ]; then
     if [ $memory -gt 3900000 ]; then
         mycnf="my-large.cnf"
     fi
-    
-    # Run mysql_install_db 
+
+    # Run mysql_install_db
     mysql_install_db >> $LOG
     # Remove symbolic link
     rm -f /etc/mysql/my.cnf
@@ -1556,7 +1559,7 @@ if [ "$mysql" = 'yes' ]; then
     mpass=$(gen_pass)
     echo -e "[client]\npassword='$mpass'\n" > /root/.my.cnf
     chmod 600 /root/.my.cnf
-    
+
     # Ater root password
     mysql -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '$mpass'; FLUSH PRIVILEGES;"
     # Allow mysql access via socket for startup
@@ -1604,7 +1607,7 @@ if [ "$mysql" = 'yes' ]; then
     mkdir -p /var/lib/phpmyadmin/tmp
     chmod 770 /var/lib/phpmyadmin/tmp
     chown root:www-data /usr/share/phpmyadmin/tmp
-    
+
     # Set config and log directory
     sed -i "s|define('CONFIG_DIR', ROOT_PATH);|define('CONFIG_DIR', '/etc/phpmyadmin/');|" /usr/share/phpmyadmin/libraries/vendor_config.php
     sed -i "s|define('TEMP_DIR', ROOT_PATH . 'tmp/');|define('TEMP_DIR', '/var/lib/phpmyadmin/tmp/');|" /usr/share/phpmyadmin/libraries/vendor_config.php
@@ -1627,8 +1630,8 @@ if [ "$mysql" = 'yes' ]; then
     # https://github.com/skurudo/phpmyadmin-fixer
     # shellcheck source=/usr/local/hestia/install/deb/phpmyadmin/pma.sh
     source $HESTIA_INSTALL_DIR/phpmyadmin/pma.sh > /dev/null 2>&1
-    
-    # limit access to /etc/phpmyadmin/ 
+
+    # limit access to /etc/phpmyadmin/
     chown -R root:www-data /etc/phpmyadmin/
     chmod -R 640  /etc/phpmyadmin/*
     chmod 750 /etc/phpmyadmin/conf.d/
@@ -1704,7 +1707,7 @@ if [ "$exim" = 'yes' ]; then
     if [ "$release" = "22.04" ]; then
       # Jammyy uses Exim 4.95 instead but config works with Exim4.94
       cp -f $HESTIA_INSTALL_DIR/exim/exim4.conf.4.94.template /etc/exim4/
-    else 
+    else
       cp -f $HESTIA_INSTALL_DIR/exim/exim4.conf.template /etc/exim4/
     fi
     cp -f $HESTIA_INSTALL_DIR/exim/dnsbl.conf /etc/exim4/
@@ -1747,18 +1750,18 @@ if [ "$dovecot" = 'yes' ]; then
     cp -rf $HESTIA_INSTALL_DIR/dovecot /etc/
     cp -f $HESTIA_INSTALL_DIR/logrotate/dovecot /etc/logrotate.d/
     rm -f /etc/dovecot/conf.d/15-mailboxes.conf
-    
+
     chown -R root:root /etc/dovecot*
-        
-    #Alter config for 2.2 
+
+    #Alter config for 2.2
     version=$(dovecot --version |  cut -f -2 -d .);
-    if [ "$version" = "2.2" ]; then 
-      echo "[ * ] Downgrade dovecot config to sync with 2.2 settings"	
+    if [ "$version" = "2.2" ]; then
+      echo "[ * ] Downgrade dovecot config to sync with 2.2 settings"
       sed -i 's|#ssl_dh_parameters_length = 4096|ssl_dh_parameters_length = 4096|g' /etc/dovecot/conf.d/10-ssl.conf
       sed -i 's|ssl_dh = </etc/ssl/dhparam.pem|#ssl_dh = </etc/ssl/dhparam.pem|g' /etc/dovecot/conf.d/10-ssl.conf
       sed -i 's|ssl_min_protocol = TLSv1.2|ssl_protocols = !SSLv3 !TLSv1 !TLSv1.1|g' /etc/dovecot/conf.d/10-ssl.conf
     fi
-    
+
     update-rc.d dovecot defaults
     systemctl start dovecot >> $LOG
     check_result $? "dovecot start failed"
@@ -1863,12 +1866,12 @@ if [ "$sieve" = 'yes' ]; then
     # Folder paths
     RC_INSTALL_DIR="/var/lib/roundcube"
     RC_CONFIG_DIR="/etc/roundcube"
-    
+
     echo "[ * ] Install Sieve..."
 
     # dovecot.conf install
     sed -i "s/namespace/service stats \{\n  unix_listener stats-writer \{\n    group = mail\n    mode = 0660\n    user = dovecot\n  \}\n\}\n\nnamespace/g" /etc/dovecot/dovecot.conf
-    
+
     # dovecot conf files
     #  10-master.conf
     sed -i -E -z "s/  }\n  user = dovecot\n}/  \}\n  unix_listener auth-master \{\n    group = mail\n    mode = 0660\n    user = dovecot\n  \}\n  user = dovecot\n\}/g" /etc/dovecot/conf.d/10-master.conf
@@ -1876,24 +1879,24 @@ if [ "$sieve" = 'yes' ]; then
     sed -i "s/\#mail_plugins = \\\$mail_plugins/mail_plugins = \$mail_plugins quota sieve\n  auth_socket_path = \/var\/run\/dovecot\/auth-master/g" /etc/dovecot/conf.d/15-lda.conf
     #  20-imap.conf
     sed -i "s/mail_plugins = quota imap_quota/mail_plugins = quota imap_quota imap_sieve/g" /etc/dovecot/conf.d/20-imap.conf
-    
+
     # replace dovecot-sieve config files
     cp -f $HESTIA_INSTALL_DIR/dovecot/sieve/* /etc/dovecot/conf.d
-    
+
     # Dovecot default file install
     echo -e "require [\"fileinto\"];\n# rule:[SPAM]\nif header :contains \"X-Spam-Flag\" \"YES\" {\n    fileinto \"INBOX.Spam\";\n}\n" > /etc/dovecot/sieve/default
-    
+
     # exim4 install
     sed -i "s/\stransport = local_delivery/ transport = dovecot_virtual_delivery/" /etc/exim4/exim4.conf.template
-    
+
     sed -i "s/address_pipe:/dovecot_virtual_delivery:\n  driver = pipe\n  command = \/usr\/lib\/dovecot\/dovecot-lda -e -d \$local_part@\$domain -f \$sender_address -a \$original_local_part@\$original_domain\n  delivery_date_add\n  envelope_to_add\n  return_path_add\n  log_output = true\n  log_defer_output = true\n  user = \${extract{2}{:}{\${lookup{\$local_part}lsearch{\/etc\/exim4\/domains\/\${lookup{\$domain}dsearch{\/etc\/exim4\/domains\/}}\/passwd}}}}\n  group = mail\n  return_output\n\naddress_pipe:/g" /etc/exim4/exim4.conf.template
-    
+
     # Modify Roundcube install
     mkdir -p $RC_CONFIG_DIR/plugins/managesieve
-    
+
     cp -f $HESTIA_INSTALL_DIR/roundcube/plugins/config_managesieve.inc.php $RC_CONFIG_DIR/plugins/managesieve/config.inc.php
         ln -s $RC_CONFIG_DIR/plugins/managesieve/config.inc.php $RC_INSTALL_DIR/plugins/managesieve/config.inc.php
-    
+
     # Permission changes
     chown -R dovecot:mail /var/log/dovecot.log
     chmod 660 /var/log/dovecot.log
@@ -1901,9 +1904,9 @@ if [ "$sieve" = 'yes' ]; then
     chmod 751 -R $RC_CONFIG_DIR
     chmod 644 $RC_CONFIG_DIR/*.php
     chmod 644 $RC_CONFIG_DIR/plugins/managesieve/config.inc.php
-        
+
     sed -i "s/'archive'/'archive', 'managesieve'/g" $RC_CONFIG_DIR/config.inc.php
-    
+
     # Restart Dovecot and exim4
     systemctl restart dovecot > /dev/null 2>&1
     systemctl restart exim4 > /dev/null 2>&1
@@ -1915,9 +1918,14 @@ fi
 #----------------------------------------------------------#
 
 if [ "$api" = "yes" ]; then
+    # keep legacy api enabled until transition is complete
     write_config_value "API" "yes"
+    write_config_value "API_SYSTEM" "1"
     write_config_value "API_ALLOWED_IP" ""
 else
+    write_config_value "API" "no"
+    write_config_value "API_SYSTEM" "0"
+    write_config_value "API_ALLOWED_IP" ""
     $HESTIA/bin/v-change-sys-api disable
 fi
 
@@ -2070,7 +2078,7 @@ chown admin:admin $HESTIA/data/sessions
 mkdir -p /backup/
 chmod 755 /backup/
 
-# create cronjob to generate ssl 
+# create cronjob to generate ssl
 echo "@reboot root sleep 10 && rm /etc/cron.d/hestia-ssl && PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:' && /usr/local/hestia/bin/v-add-letsencrypt-host" > /etc/cron.d/hestia-ssl
 
 #----------------------------------------------------------#

+ 5 - 0
install/upgrade/versions/1.6.0.sh

@@ -62,3 +62,8 @@ if [ -z "$(grep v-update-lets $HESTIA/data/users/admin/cron.conf)" ]; then
 	command="sudo $BIN/v-update-letsencrypt-ssl"
 	$BIN/v-add-cron-job 'admin' "$min" "$hour" '*' '*' '*' "$command"
 fi
+
+# Add apis if they don't exist
+if [[ ! -d $HESTIA/data/api ]]; then
+    cp -rf $HESTIA_INSTALL_DIR/api $HESTIA/data/
+fi

+ 87 - 0
web/add/access-key/index.php

@@ -0,0 +1,87 @@
+<?php
+ob_start();
+$TAB = 'Access Key';
+
+// Main include
+include($_SERVER['DOCUMENT_ROOT']."/inc/main.php");
+
+// Checks if API access is enabled
+$api_status = (!empty($_SESSION['API_SYSTEM']) && is_numeric($_SESSION['API_SYSTEM'])) ? $_SESSION['API_SYSTEM'] : 0;
+if (($user_plain == 'admin' && $api_status < 1) || ($user_plain != 'admin' && $api_status < 2)) {
+    header("Location: /edit/user/");
+    exit;
+}
+
+// APIs available
+exec(HESTIA_CMD."v-list-apis json", $output, $return_var);
+$apis = json_decode(implode('', $output), true);
+$apis = array_filter($apis, function ($api) use ($user_plain) {
+    return ($user_plain == 'admin' || $api['ROLE'] == 'user');
+});
+ksort($apis);
+unset($output);
+
+// Check POST request
+if (!empty($_POST['ok'])) {
+
+    // Check token
+    verify_csrf($_POST);
+
+    // Validate apis
+    $apis_selected = (!empty($_POST['v_apis']) && is_array($_POST['v_apis'])) ? $_POST['v_apis'] : [];
+    $check_invalid_apis = array_filter($apis_selected, function ($selected) use ($apis) {
+        return !array_key_exists($selected, $apis);
+    });
+
+    if (empty($apis_selected)) {
+        $errors[] = _('apis');
+    } else if (count($check_invalid_apis) > 0) {
+        //$errors[] = sprintf("%d apis not allowed", count($check_invalid_apis));
+        foreach ($check_invalid_apis as $api_name) {
+            $errors[] = sprintf("api %s not allowed", $api_name);
+        }
+    }
+
+    if (!empty($errors[0])) {
+        foreach ($errors as $i => $error) {
+            if ($i == 0) {
+                $error_msg = $error;
+            } else {
+                $error_msg = $error_msg.", ".$error;
+            }
+        }
+        $_SESSION['error_msg'] = sprintf(_('Field "%s" can not be blank.'), $error_msg);
+    }
+
+    // Protect input
+    $v_apis = escapeshellarg(implode(',', $apis_selected));
+    $v_comment = escapeshellarg(trim($_POST['v_comment'] ?? ''));
+
+    // Add access key
+    if (empty($_SESSION['error_msg'])) {
+        exec(HESTIA_CMD."v-add-access-key ".$user." ".$v_apis." ".$v_comment." json", $output, $return_var);
+        $key_data = json_decode(implode('', $output), true);
+        check_return_code($return_var, $output);
+        unset($output);
+    }
+
+    // Flush field values on success
+    if (empty($_SESSION['error_msg'])) {
+        $_SESSION['ok_msg'] = sprintf(_('Access key %s has been created'), htmlentities($key_data['ACCESS_KEY_ID']));
+        unset($apis_selected);
+        unset($check_invalid_apis);
+        unset($v_apis);
+        unset($v_comment);
+    }
+}
+
+// Render
+if (empty($key_data)) {
+    render_page($user, $TAB, 'add_access_key');
+} else {
+    render_page($user, $TAB, 'list_access_key');
+}
+
+// Flush session messages
+unset($_SESSION['error_msg']);
+unset($_SESSION['ok_msg']);

+ 230 - 100
web/api/index.php

@@ -1,71 +1,73 @@
 <?php
-define('HESTIA_CMD', '/usr/bin/sudo /usr/local/hestia/bin/');
 //die("Error: Disabled");
+define('HESTIA_CMD', '/usr/bin/sudo /usr/local/hestia/bin/');
 
-function check_local_ip($addr){
-    if(in_array($addr, array($_SERVER['SERVER_ADDR'], '127.0.0.1'))){
-        return true;
-    }else{
-        return false;
-    }
-}
+include($_SERVER['DOCUMENT_ROOT']."/inc/helpers.php");
 
-function get_real_user_ip(){
-    $ip = $_SERVER['REMOTE_ADDR'];
-    if(isset($_SERVER['HTTP_CLIENT_IP']) && !check_local_ip($_SERVER['HTTP_CLIENT_IP'])) {
-        if (filter_var($_SERVER['HTTP_CLIENT_IP'], FILTER_VALIDATE_IP)){
-        $ip = $_SERVER['HTTP_CLIENT_IP'];
-        }
-    }
-    
-    if(isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !check_local_ip($_SERVER['HTTP_X_FORWARDED_FOR'])) {
-        if (filter_var($_SERVER['HTTP_X_FORWARDED_FOR'], FILTER_VALIDATE_IP)){
-            $ip =  $_SERVER['HTTP_X_FORWARDED_FOR'];
-        }
-    }
-    
-    if(isset($_SERVER['HTTP_FORWARDED_FOR']) && !check_local_ip($_SERVER['HTTP_FORWARDED_FOR'])) {
-        if (filter_var($_SERVER['HTTP_FORWARDED_FOR'], FILTER_VALIDATE_IP)){
-            $ip =  $_SERVER['HTTP_FORWARDED_FOR'];
-        }
-    }
-    
-    if(isset($_SERVER['HTTP_X_FORWARDED']) && !check_local_ip($_SERVER['HTTP_X_FORWARDED'])) {
-        if (filter_var($_SERVER['HTTP_X_FORWARDED'], FILTER_VALIDATE_IP)){
-            $ip =  $_SERVER['HTTP_X_FORWARDED'];
-        }
-    }
-    
-    if(isset($_SERVER['HTTP_FORWARDED']) && !check_local_ip($_SERVER['HTTP_FORWARDED'])) {
-        if (filter_var($_SERVER['HTTP_FORWARDED'], FILTER_VALIDATE_IP)){
-            $ip =  $_SERVER['HTTP_FORWARDED'];
-        }
-    }
-    
-    if(isset($_SERVER['HTTP_CF_CONNECTING_IP']) && !check_local_ip($_SERVER['HTTP_CF_CONNECTING_IP'])) {
-        if (filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP)){
-            $ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
-        }
+/**
+ * Displays the error message, checks the proper code and saves a log if needed.
+ *
+ * @param int $exit_code
+ * @param string $message
+ * @param bool $add_log
+ * @param string $user
+ * @return void
+ */
+function api_error($exit_code, $message, bool $add_log = false, $user = 'system') {
+    $message = trim(is_array($message) ? implode("\n", $message) : $message);
+
+    // Add log
+    if ($add_log) {
+        $v_real_user_ip = get_real_user_ip();
+        hst_add_history_log("[$v_real_user_ip] $message", 'API', 'Error', $user);
     }
-    return $ip;
+
+    // Print the message with http_code and exit_code
+    $http_code = ($exit_code >= 100) ? $exit_code : exit_code_to_http_code($exit_code);
+    header("Hestia-Exit-Code: $exit_code");
+    http_response_code($http_code);
+    echo (!preg_match('/^Error:/', $message)) ? "Error: $message" : $message;
+    exit;
 }
 
-function api($hst_hash, $hst_user, $hst_password, $hst_returncode, $hst_cmd, $hst_arg1, $hst_arg2, $hst_arg3, $hst_arg4, $hst_arg5, $hst_arg6, $hst_arg7, $hst_arg8, $hst_arg9){
-    exec (HESTIA_CMD."v-list-sys-config json" , $output, $return_var);
+/**
+ * Legacy connection format using hash or user and password.
+ *
+ * @param string $hst_hash
+ * @param string $hst_user
+ * @param string $hst_password
+ * @param string $hst_returncode
+ * @param string $hst_cmd
+ * @param string $hst_arg1
+ * @param string $hst_arg2
+ * @param string $hst_arg3
+ * @param string $hst_arg4
+ * @param string $hst_arg5
+ * @param string $hst_arg6
+ * @param string $hst_arg7
+ * @param string $hst_arg8
+ * @param string $hst_arg9
+ * @return void
+ */
+function api_legacy($hst_hash, $hst_user, $hst_password, $hst_returncode, $hst_cmd, $hst_arg1, $hst_arg2, $hst_arg3, $hst_arg4, $hst_arg5, $hst_arg6, $hst_arg7, $hst_arg8, $hst_arg9) {
+    exec(HESTIA_CMD."v-list-sys-config json", $output, $return_var);
     $settings = json_decode(implode('', $output), true);
     unset($output);
-    if( $settings['config']['API'] != 'yes' ){
+
+    if ($settings['config']['API'] != 'yes') {
         echo 'Error: API has been disabled';
         exit;
     }
-    if ( $settings['config']['API_ALLOWED_IP'] != 'allow-all' ){
-        $ip_list = explode(',',$settings['config']['API_ALLOWED_IP']);
+
+    if ($settings['config']['API_ALLOWED_IP'] != 'allow-all') {
+        $ip_list = explode(',', $settings['config']['API_ALLOWED_IP']);
         $ip_list[] = '';
-        if ( !in_array(get_real_user_ip(), $ip_list)){
-           echo 'Error: IP is not allowed to connect with API';
-           exit; 
+        if (!in_array(get_real_user_ip(), $ip_list)) {
+            echo 'Error: IP is not allowed to connect with API';
+            exit;
         }
     }
+
     //This exists, so native JSON can be used without the repeating the code twice, so future code changes are easier and don't need to be replicated twice
     // Authentication
     if (empty($hst_hash)) {
@@ -75,25 +77,25 @@ function api($hst_hash, $hst_user, $hst_password, $hst_returncode, $hst_cmd, $hs
         }
 
         $password = $hst_password;
-        if (!isset($password)){
+        if (!isset($password)) {
             echo 'Error: missing authentication';
             exit;
         }
         $v_ip = escapeshellarg(get_real_user_ip());
         $output = '';
-        exec (HESTIA_CMD."v-get-user-salt admin ".$v_ip." json" , $output, $return_var);
+        exec(HESTIA_CMD."v-get-user-salt admin ".$v_ip." json", $output, $return_var);
         $pam = json_decode(implode('', $output), true);
         $salt = $pam['admin']['SALT'];
         $method = $pam['admin']['METHOD'];
 
-        if ($method == 'md5' ) {
+        if ($method == 'md5') {
             $hash = crypt($password, '$1$'.$salt.'$');
         }
-        if ($method == 'sha-512' ) {
+        if ($method == 'sha-512') {
             $hash = crypt($password, '$6$rounds=5000$'.$salt.'$');
-            $hash = str_replace('$rounds=5000','',$hash);
+            $hash = str_replace('$rounds=5000', '', $hash);
         }
-        if ($method == 'des' ) {
+        if ($method == 'des') {
             $hash = crypt($password, $salt);
         }
 
@@ -104,24 +106,24 @@ function api($hst_hash, $hst_user, $hst_password, $hst_returncode, $hst_cmd, $hs
         fclose($fp);
 
         // Check user hash
-        exec(HESTIA_CMD ."v-check-user-hash admin ".$v_hash." ".$v_ip,  $output, $return_var);
+        exec(HESTIA_CMD."v-check-user-hash admin ".$v_hash." ".$v_ip, $output, $return_var);
         unset($output);
 
         // Remove tmp file
         unlink($v_hash);
 
         // Check API answer
-        if ( $return_var > 0 ) {
+        if ($return_var > 0) {
             echo 'Error: authentication failed';
             exit;
         }
     } else {
-        $key = '/usr/local/hestia/data/keys/' . basename($hst_hash);
+        $key = '/usr/local/hestia/data/keys/'.basename($hst_hash);
         $v_ip = escapeshellarg(get_real_user_ip());
-        exec(HESTIA_CMD ."v-check-api-key ".escapeshellarg($key)." ".$v_ip,  $output, $return_var);
+        exec(HESTIA_CMD."v-check-api-key ".escapeshellarg($key)." ".$v_ip, $output, $return_var);
         unset($output);
         // Check API answer
-        if ( $return_var > 0 ) {
+        if ($return_var > 0) {
             echo 'Error: authentication failed';
             exit;
         }
@@ -138,26 +140,36 @@ function api($hst_hash, $hst_user, $hst_password, $hst_returncode, $hst_cmd, $hs
     if (isset($hst_arg7)) $arg7 = escapeshellarg($hst_arg7);
     if (isset($hst_arg8)) $arg8 = escapeshellarg($hst_arg8);
     if (isset($hst_arg9)) $arg9 = escapeshellarg($hst_arg9);
+
     // Build query
     $cmdquery = HESTIA_CMD.$cmd." ";
-    if(!empty($arg1)){
-         $cmdquery = $cmdquery.$arg1." "; }
-    if(!empty($arg2)){
-         $cmdquery = $cmdquery.$arg2." "; }
-    if(!empty($arg3)){
-         $cmdquery = $cmdquery.$arg3." "; }
-    if(!empty($arg4)){
-         $cmdquery = $cmdquery.$arg4." "; }
-    if(!empty($arg5)){
-         $cmdquery = $cmdquery.$arg5." "; }
-    if(!empty($arg6)){
-         $cmdquery = $cmdquery.$arg6." "; }
-    if(!empty($arg7)){
-         $cmdquery = $cmdquery.$arg7." "; }
-    if(!empty($arg8)){
-         $cmdquery = $cmdquery.$arg8." "; }
-    if(!empty($arg9)){
-         $cmdquery = $cmdquery.$arg9; }
+    if (!empty($arg1)) {
+        $cmdquery = $cmdquery.$arg1." ";
+    }
+    if (!empty($arg2)) {
+        $cmdquery = $cmdquery.$arg2." ";
+    }
+    if (!empty($arg3)) {
+        $cmdquery = $cmdquery.$arg3." ";
+    }
+    if (!empty($arg4)) {
+        $cmdquery = $cmdquery.$arg4." ";
+    }
+    if (!empty($arg5)) {
+        $cmdquery = $cmdquery.$arg5." ";
+    }
+    if (!empty($arg6)) {
+        $cmdquery = $cmdquery.$arg6." ";
+    }
+    if (!empty($arg7)) {
+        $cmdquery = $cmdquery.$arg7." ";
+    }
+    if (!empty($arg8)) {
+        $cmdquery = $cmdquery.$arg8." ";
+    }
+    if (!empty($arg9)) {
+        $cmdquery = $cmdquery.$arg9;
+    }
 
     // Check command
     if ($cmd == "'v-make-tmp-file'") {
@@ -168,7 +180,7 @@ function api($hst_hash, $hst_user, $hst_password, $hst_returncode, $hst_cmd, $hs
         $return_var = 0;
     } else {
         // Run normal cmd query
-        exec ($cmdquery, $output, $return_var);
+        exec($cmdquery, $output, $return_var);
     }
 
     if ((!empty($hst_returncode)) && ($hst_returncode == 'yes')) {
@@ -177,33 +189,151 @@ function api($hst_hash, $hst_user, $hst_password, $hst_returncode, $hst_cmd, $hs
         if (($return_var == 0) && (empty($output))) {
             echo "OK";
         } else {
-            echo implode("\n",$output)."\n";
+            echo implode("\n", $output)."\n";
         }
     }
+
+    exit;
 }
 
-$array = array('user','password','hash','returncode','cmd','arg1','arg2','arg3','arg4','arg5','arg6','arg7','arg8','arg9');
-    
-if (isset($_POST['user']) || isset($_POST['hash'])) {
-    foreach($array as $key){
-        if(empty($_POST[$key])){
-            $_POST[$key] = '';
+/**
+ * Connection using access key.
+ *
+ * @param array{access_key: string, secret_key: string, cmd: string, arg1?: string, arg2?: string, arg3?: string, arg4?: string, arg5?: string, arg6?: string, arg7?: string, arg8?: string, arg9?: string, returncode?: string} $request_data
+ * @return void
+ */
+function api_connection(array $request_data) {
+    $v_real_user_ip = get_real_user_ip();
+
+    exec(HESTIA_CMD."v-list-sys-config json", $output, $return_var);
+    $settings = json_decode(implode('', $output), true);
+    unset($output, $return_var);
+
+    // Get the status of api
+    $api_status = (!empty($settings['config']['API_SYSTEM']) && is_numeric($settings['config']['API_SYSTEM'])) ? $settings['config']['API_SYSTEM'] : 0;
+    if ($api_status == 0) {
+        // Check if API is disabled for all users
+        api_error(E_DISABLED, "API has been disabled");
+    }
+
+    // Check if API access is enabled for the user
+    if ($settings['config']['API_ALLOWED_IP'] != 'allow-all') {
+        $ip_list = explode(',', $settings['config']['API_ALLOWED_IP']);
+        $ip_list[] = '';
+        if (!in_array($v_real_user_ip, $ip_list) && !in_array('0.0.0.0', $ip_list)) {
+            api_error(E_FORBIDDEN, "IP is not allowed to connect with API");
         }
     }
-    api($_POST['hash'], $_POST['user'], $_POST['password'], $_POST['returncode'], $_POST['cmd'], $_POST['arg1'], $_POST['arg2'], $_POST['arg3'], $_POST['arg4'], $_POST['arg5'], $_POST['arg6'], $_POST['arg7'], $_POST['arg8'], $_POST['arg9']);
 
-} else if (json_decode(file_get_contents("php://input"), true) != NULL){ //JSON POST support
-    $json_data = json_decode(file_get_contents("php://input"), true);
-    foreach($array as $key){
-        if(empty($json_data[$key])){
-            $json_data[$key] = '';
+    // Get POST Params
+    $hst_access_key_id = trim($request_data['access_key'] ?? '');
+    $hst_secret_access_key = trim($request_data['secret_key'] ?? '');
+    $hst_return = (($request_data['returncode'] ?? 'no') === 'yes') ? 'code' : 'data';
+    $hst_cmd = trim($request_data['cmd'] ?? '');
+    $hst_cmd_args = [];
+    for ($i = 1; $i <= 9; $i++) {
+        if (isset($request_data["arg{$i}"])) {
+            $hst_cmd_args["arg{$i}"] = trim($request_data["arg{$i}"]);
         }
     }
-    api($json_data['hash'], $json_data['user'], $json_data['password'], $json_data['returncode'], $json_data['cmd'], $json_data['arg1'], $json_data['arg2'], $json_data['arg3'], $json_data['arg4'], $json_data['arg5'], $json_data['arg6'], $json_data['arg7'], $json_data['arg8'], $json_data['arg9']);
 
-} else {
-    echo "Error: data received is null or invalid, check https://docs.hestiacp.com/admin_docs/api.html";
+    if (empty($hst_cmd)) {
+        api_error(E_INVALID, "Command not provided");
+    } else if (!preg_match('/^[a-zA-Z0-9_-]+$/', $hst_cmd)) {
+        api_error(E_INVALID, "$hst_cmd command invalid");
+    }
+
+    if (empty($hst_access_key_id) || empty($hst_secret_access_key)) {
+        api_error(E_PASSWORD, "Authentication failed");
+    }
+
+    // Authenticates the key and checks permission to run the script
+    exec(HESTIA_CMD."v-check-access-key ".escapeshellarg($hst_access_key_id)." ".escapeshellarg($hst_secret_access_key)." ".escapeshellarg($hst_cmd)." ".escapeshellarg($v_real_user_ip)." json", $output, $return_var);
+    if ($return_var > 0) {
+        //api_error($return_var, "Key $hst_access_key_id - authentication failed");
+        api_error($return_var, $output);
+    }
+    $key_data = json_decode(implode('', $output), true) ?? [];
+    unset($output, $return_var);
+
+    $key_user = $key_data['USER'];
+    $user_arg_position = (isset($key_data['USER_ARG_POSITION']) && is_numeric($key_data['USER_ARG_POSITION'])) ? $key_data['USER_ARG_POSITION'] : -1;
+
+    # Check if API access is enabled for nonadmin users
+    if ($key_user != 'admin' && $api_status < 2) {
+        api_error(E_DISABLED, "API has been disabled");
+    }
+
+    // Checks if the value entered in the "user" argument matches the user of the key
+    if ($key_user != 'admin' && $user_arg_position > 0 && $hst_cmd_args["arg{$user_arg_position}"] != $key_user) {
+        api_error(E_FORBIDDEN, "Key $hst_access_key_id - the \"user\" argument doesn\'t match the key\'s user");
+    }
+
+    // Prepare command
+    $cmdquery = HESTIA_CMD.escapeshellcmd($hst_cmd);
+
+    // Prepare arguments
+    foreach ($hst_cmd_args as $cmd_arg) {
+        $cmdquery .= " ".escapeshellarg($cmd_arg);
+    }
+
+    // Run cmd query
+    exec($cmdquery, $output, $cmd_exit_code);
+    $cmd_output = trim(implode("\n", $output));
+    unset($output);
+
+    header("Hestia-Exit-Code: $cmd_exit_code");
+
+    if ($hst_return == 'code') {
+        echo $cmd_exit_code;
+    } else {
+        if ($cmd_exit_code > 0) {
+            http_response_code(exit_code_to_http_code($cmd_exit_code));
+        } else {
+            http_response_code(!empty($cmd_output) ? 200 : 204);
+
+            if (!empty($cmd_output) && json_decode($cmd_output, true)) {
+                header('Content-Type: application/json; charset=utf-8');
+            }
+        }
+
+        echo $cmd_output;
+    }
+
     exit;
 }
 
-?>
+// Get request data
+if (isset($_POST['access_key']) || isset($_POST['user']) || isset($_POST['hash'])) {
+    $request_data = $_POST;
+} else if (($json_data = json_decode(file_get_contents("php://input"), true)) != null) {
+    $request_data = $json_data;
+} else {
+    api_error(405, "Error: data received is null or invalid, check https://docs.hestiacp.com/admin_docs/api.html");
+}
+
+// Try to get access key in the hash
+if (!isset($request_data['access_key']) && isset($request_data['hash']) && substr_count($request_data['hash'], ':') == 1) {
+    $hash_parts = explode(':', $request_data['hash']);
+    if (strlen($hash_parts[0]) == 20 && strlen($hash_parts[1]) == 40) {
+        $request_data['access_key'] = $hash_parts[0];
+        $request_data['secret_key'] = $hash_parts[1];
+        unset($request_data['hash']);
+    }
+}
+
+// Check data format
+if (isset($request_data['access_key']) && isset($request_data['secret_key'])) {
+    api_connection($request_data);
+} else if (isset($request_data['user']) || isset($request_data['hash'])) {
+    $array = array('user', 'password', 'hash', 'returncode', 'cmd', 'arg1', 'arg2', 'arg3', 'arg4', 'arg5', 'arg6', 'arg7', 'arg8', 'arg9');
+    foreach ($array as $key) {
+        if (empty($request_data[$key])) {
+            $request_data[$key] = '';
+        }
+    }
+
+    api_legacy($request_data['hash'], $request_data['user'], $request_data['password'], $request_data['returncode'], $request_data['cmd'], $request_data['arg1'], $request_data['arg2'], $request_data['arg3'], $request_data['arg4'], $request_data['arg5'], $request_data['arg6'], $request_data['arg7'], $request_data['arg8'], $request_data['arg9']);
+} else {
+    api_error(405, "Error: data received is null or invalid, check https://docs.hestiacp.com/admin_docs/api.html");
+}

+ 45 - 0
web/bulk/access-key/index.php

@@ -0,0 +1,45 @@
+<?php
+
+ob_start();
+
+include($_SERVER['DOCUMENT_ROOT']."/inc/main.php");
+
+// Check token
+verify_csrf($_POST);
+
+if (($_SESSION['userContext'] === 'admin') && (!empty($_GET['user']))) {
+    $user = escapeshellarg($_GET['user']);
+    $user_plain = $_GET['user'];
+}
+
+// Checks if API access is enabled
+$api_status = (!empty($_SESSION['API_SYSTEM']) && is_numeric($_SESSION['API_SYSTEM'])) ? $_SESSION['API_SYSTEM'] : 0;
+if (($user_plain == 'admin' && $api_status < 1) || ($user_plain != 'admin' && $api_status < 2)) {
+    header("Location: /edit/user/");
+    exit;
+}
+
+$key = $_POST['key'];
+$action = $_POST['action'];
+
+switch ($action) {
+    case 'delete': $cmd='v-delete-access-key';
+        break;
+    default: header("Location: /list/access-key/"); exit;
+}
+
+foreach ($key as $value) {
+    $v_key = escapeshellarg(trim($value));
+
+    // Key data
+    exec(HESTIA_CMD."v-list-access-key ".$v_key." json", $output, $return_var);
+    $key_data = json_decode(implode('', $output), true);
+    unset($output);
+
+    if (!empty($key_data) && $key_data['USER'] == $user_plain) {
+        exec(HESTIA_CMD.$cmd." ".$v_key, $output, $return_var);
+        unset($output);
+    }
+}
+
+header("Location: /list/access-key/");

+ 46 - 0
web/delete/access-key/index.php

@@ -0,0 +1,46 @@
+<?php
+
+ob_start();
+
+include($_SERVER['DOCUMENT_ROOT']."/inc/main.php");
+
+// Check token
+verify_csrf($_GET);
+
+if (($_SESSION['userContext'] === 'admin') && (!empty($_GET['user']))) {
+    $user = escapeshellarg($_GET['user']);
+    $user_plain = $_GET['user'];
+}
+
+// Checks if API access is enabled
+$api_status = (!empty($_SESSION['API_SYSTEM']) && is_numeric($_SESSION['API_SYSTEM'])) ? $_SESSION['API_SYSTEM'] : 0;
+if (($user_plain == 'admin' && $api_status < 1) || ($user_plain != 'admin' && $api_status < 2)) {
+    header("Location: /edit/user/");
+    exit;
+}
+
+if (!empty($_GET['key'])) {
+    $v_key = escapeshellarg(trim($_GET['key']));
+
+    // Key data
+    exec(HESTIA_CMD."v-list-access-key ".$v_key." json", $output, $return_var);
+    $key_data = json_decode(implode('', $output), true);
+    unset($output);
+
+    if (empty($key_data) || $key_data['USER'] != $user_plain) {
+        header("Location: /list/access-key/");
+        exit;
+    }
+
+    exec(HESTIA_CMD."v-delete-access-key ".$v_key, $output, $return_var);
+    check_return_code($return_var, $output);
+    unset($output);
+}
+
+$back = $_SESSION['back'];
+if (!empty($back)) {
+    header("Location: ".$back);
+    exit;
+}
+header("Location: /list/key/");
+exit;

+ 69 - 42
web/edit/server/index.php

@@ -1039,6 +1039,75 @@ if (!empty($_POST['save'])) {
             $v_security_adv = 'yes';
         }
     }
+    
+    if ($_POST['v_api_system'] != $_SESSION['API_SYSTEM'] || $_POST['v_api'] != $_SESSION['API'] || $_POST['v_api_allowed_ip'] != $_SESSION['API_ALLOWED_IP']){
+        if (empty($_SESSION['error_msg'])) {
+            if($_POST['v_api'] == "no" && $_POST['v_api_system'] === 0 ){
+                exec(HESTIA_CMD."v-change-sys-api 'disable'", $output, $return_var);
+                check_return_code($return_var, $output);
+                unset($output);
+            }
+            if($_POST['v_api'] == "yes" || $_POST['v_api_system'] !== 0  && $_POST['v_api_system'] != $_SESSION['API_SYSTEM'] || $_POST['v_api'] != $_SESSION['API']){
+                exec(HESTIA_CMD."v-change-sys-api 'enable'", $output, $return_var);
+                check_return_code($return_var, $output);
+                unset($output);
+            }
+        }
+        if (empty($_SESSION['error_msg'])) {
+            if ($_POST['v_api_system'] != $_SESSION['API_SYSTEM']) {
+                exec(HESTIA_CMD."v-change-sys-config-value API_SYSTEM ".escapeshellarg($_POST['v_api_system']), $output, $return_var);
+                check_return_code($return_var, $output);
+                unset($output);
+                if (empty($_SESSION['error_msg'])) {
+                    $v_policy_user_edit_details = $_POST['v_api_system'];
+                }
+                $v_security_adv = 'yes';
+            }
+        }
+        
+        // Change API access
+        if (empty($_SESSION['error_msg'])) {
+            if ($_POST['v_api'] != $_SESSION['API']) {
+                $api_status = 'no';
+                if ($_POST['v_api'] == 'yes') {
+                    $api_status = 'yes';
+                }
+                exec(HESTIA_CMD."v-change-sys-config-value API ".escapeshellarg($api_status), $output, $return_var);
+                check_return_code($return_var, $output);
+                unset($output);
+                if (empty($_SESSION['error_msg'])) {
+                    $v_api = $_POST['v_api'];
+                }
+                $v_security_adv = 'yes';
+            }
+        }
+            
+        // Change API allowed IPs
+        if (empty($_SESSION['error_msg'])) {
+            if ($_POST['v_api_allowed_ip'] != $_SESSION['API_ALLOWED_IP']) {
+                $ips = array();
+                foreach (explode("\n", $_POST['v_api_allowed_ip']) as $ip) {
+                    if (trim($ip) != "allow-all") {
+                        if (filter_var(trim($ip), FILTER_VALIDATE_IP)) {
+                            $ips[] = trim($ip);
+                        }
+                    } else {
+                        $ips[] = trim($ip);
+                    }
+                }
+                if (implode(',', $ips) != $_SESSION['API_ALLOWED_IP']) {
+                    exec(HESTIA_CMD."v-change-sys-config-value API_ALLOWED_IP ".escapeshellarg(implode(',', $ips)), $output, $return_var);
+                    check_return_code($return_var, $output);
+                    unset($output);
+                    if (empty($_SESSION['error_msg'])) {
+                        $v_api_allowed_ip = $_POST['v_api_allowed_ip'];
+                    }
+                    $v_security_adv = 'yes';
+                }
+            }
+        }   
+
+    }
 
     // Change POLICY_USER_VIEW_LOGS
     if (empty($_SESSION['error_msg'])) {
@@ -1171,48 +1240,6 @@ if (!empty($_POST['save'])) {
         }
     }
 
-    // Change API access
-    if (empty($_SESSION['error_msg'])) {
-        if ($_POST['v_api'] != $_SESSION['API']) {
-            $api_status = 'disable';
-            if ($_POST['v_api'] == 'yes') {
-                $api_status = 'enable';
-            }
-            exec(HESTIA_CMD."v-change-sys-api ".escapeshellarg($api_status), $output, $return_var);
-            check_return_code($return_var, $output);
-            unset($output);
-            if (empty($_SESSION['error_msg'])) {
-                $v_api = $_POST['v_api'];
-            }
-            $v_security_adv = 'yes';
-        }
-    }
-
-    // Change API allowed IPs
-    if (empty($_SESSION['error_msg'])) {
-        if ($_POST['v_api_allowed_ip'] != $_SESSION['API_ALLOWED_IP']) {
-            $ips = array();
-            foreach (explode("\n", $_POST['v_api_allowed_ip']) as $ip) {
-                if (trim($ip) != "allow-all") {
-                    if (filter_var(trim($ip), FILTER_VALIDATE_IP)) {
-                        $ips[] = trim($ip);
-                    }
-                } else {
-                    $ips[] = trim($ip);
-                }
-            }
-            if (implode(',', $ips) != $_SESSION['API_ALLOWED_IP']) {
-                exec(HESTIA_CMD."v-change-sys-config-value API_ALLOWED_IP ".escapeshellarg(implode(',', $ips)), $output, $return_var);
-                check_return_code($return_var, $output);
-                unset($output);
-                if (empty($_SESSION['error_msg'])) {
-                    $v_api_allowed_ip = $_POST['v_api_allowed_ip'];
-                }
-                $v_security_adv = 'yes';
-            }
-        }
-    }
-
     // Update SSL certificate
     if ((!empty($_POST['v_ssl_crt'])) && (empty($_SESSION['error_msg']))) {
         if (($v_ssl_crt != str_replace("\r\n", "\n", $_POST['v_ssl_crt'])) || ($v_ssl_key != str_replace("\r\n", "\n", $_POST['v_ssl_key']))) {

+ 135 - 0
web/inc/helpers.php

@@ -0,0 +1,135 @@
+<?php
+
+# Return codes
+const E_ARGS = 1;
+const E_INVALID = 2;
+const E_NOTEXIST = 3;
+const E_EXISTS = 4;
+const E_SUSPENDED = 5;
+const E_UNSUSPENDED = 6;
+const E_INUSE = 7;
+const E_LIMIT = 8;
+const E_PASSWORD = 9;
+const E_FORBIDEN = 10;
+const E_FORBIDDEN = 10;
+const E_DISABLED = 11;
+const E_PARSING = 12;
+const E_DISK = 13;
+const E_LA = 14;
+const E_CONNECT = 15;
+const E_FTP = 16;
+const E_DB = 17;
+const E_RRD = 18;
+const E_UPDATE = 19;
+const E_RESTART = 20;
+
+/**
+ * Looks for a code equivalent to "exit_code" to use in http_code.
+ *
+ * @param int $exit_code
+ * @param int $default
+ * @return int
+ */
+function exit_code_to_http_code(
+    int $exit_code,
+    int $default = 400
+//    int $default = 500
+): int {
+    switch ($exit_code) {
+        case 0:
+            return 200;
+        case E_ARGS:
+//            return 500;
+            return 400;
+        case E_INVALID:
+            return 422;
+//        case E_NOTEXIST:
+//            return 404;
+//        case E_EXISTS:
+//            return 302;
+        case E_PASSWORD:
+            return 401;
+        case E_SUSPENDED:
+        case E_UNSUSPENDED:
+        case E_FORBIDEN:
+        case E_FORBIDDEN:
+            return 401;
+//            return 403;
+        case E_DISABLED:
+            return 400;
+//            return 503;
+    }
+
+    return $default;
+}
+
+function check_local_ip($addr) {
+    if (in_array($addr, array($_SERVER['SERVER_ADDR'], '127.0.0.1'))) {
+        return true;
+    } else {
+        return false;
+    }
+}
+
+function get_real_user_ip() {
+    $ip = $_SERVER['REMOTE_ADDR'];
+    if (isset($_SERVER['HTTP_CLIENT_IP']) && !check_local_ip($_SERVER['HTTP_CLIENT_IP'])) {
+        if (filter_var($_SERVER['HTTP_CLIENT_IP'], FILTER_VALIDATE_IP)) {
+            $ip = $_SERVER['HTTP_CLIENT_IP'];
+        }
+    }
+
+    if (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !check_local_ip($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+        if (filter_var($_SERVER['HTTP_X_FORWARDED_FOR'], FILTER_VALIDATE_IP)) {
+            $ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
+        }
+    }
+
+    if (isset($_SERVER['HTTP_FORWARDED_FOR']) && !check_local_ip($_SERVER['HTTP_FORWARDED_FOR'])) {
+        if (filter_var($_SERVER['HTTP_FORWARDED_FOR'], FILTER_VALIDATE_IP)) {
+            $ip = $_SERVER['HTTP_FORWARDED_FOR'];
+        }
+    }
+
+    if (isset($_SERVER['HTTP_X_FORWARDED']) && !check_local_ip($_SERVER['HTTP_X_FORWARDED'])) {
+        if (filter_var($_SERVER['HTTP_X_FORWARDED'], FILTER_VALIDATE_IP)) {
+            $ip = $_SERVER['HTTP_X_FORWARDED'];
+        }
+    }
+
+    if (isset($_SERVER['HTTP_FORWARDED']) && !check_local_ip($_SERVER['HTTP_FORWARDED'])) {
+        if (filter_var($_SERVER['HTTP_FORWARDED'], FILTER_VALIDATE_IP)) {
+            $ip = $_SERVER['HTTP_FORWARDED'];
+        }
+    }
+
+    if (isset($_SERVER['HTTP_CF_CONNECTING_IP']) && !check_local_ip($_SERVER['HTTP_CF_CONNECTING_IP'])) {
+        if (filter_var($_SERVER['HTTP_CF_CONNECTING_IP'], FILTER_VALIDATE_IP)) {
+            $ip = $_SERVER['HTTP_CF_CONNECTING_IP'];
+        }
+    }
+    return $ip;
+}
+
+/**
+ * Create a history log using 'v-log-action' script.
+ *
+ * @param string $message The message for log.
+ * @param string $category A category for log. Ex: Auth, Firewall, API...
+ * @param string $level Info|Warning|Error.
+ * @param string $user A username for save in the user history ou 'system' to save in Hestia history.
+ * @return int The script result code.
+ */
+function hst_add_history_log($message, $category = 'System', $level = 'Info', $user = 'system') {
+    //$message = ucfirst($message);
+    //$message = str_replace("'", "`", $message);
+    $category = ucfirst(strtolower($category));
+    $level = ucfirst(strtolower($level));
+
+    $command_args = escapeshellarg($user).' '.escapeshellarg($level).' '.escapeshellarg($category).' '.escapeshellarg($message);
+    exec(HESTIA_CMD."v-log-action ".$command_args, $output, $return_var);
+    unset($output);
+
+    return $return_var;
+}
+

+ 1 - 0
web/inc/main.php

@@ -21,6 +21,7 @@ define('DEFAULT_PHP_VERSION', 'php-' . exec('php -r "echo substr(phpversion(),0,
 // Load Hestia Config directly
 load_hestia_config();
 require_once(dirname(__FILE__) . '/prevent_csrf.php');
+require_once(dirname(__FILE__) . '/helpers.php');
 
 function destroy_sessions()
 {

+ 11 - 7
web/js/pages/edit_server.js

@@ -7,10 +7,14 @@ $('#backup_type').change(function (){
        $('#backup_sftp').show();
    }
 });
-$('#api').change(function (){
-       if(this.value == 'yes'){
-           $('#security_ip').show();
-       }else{
-           $('#security_ip').hide();
-       }
-    });
+
+$('#api, #api-system').change(function () {
+    var api = $('#api').val();
+    var apiSystem = $('#api-system').val();
+
+    if (api === 'yes' || apiSystem > 0) {
+        $('#security_ip').show();
+    } else {
+        $('#security_ip').hide();
+    }
+});

+ 57 - 0
web/list/access-key/index.php

@@ -0,0 +1,57 @@
+<?php
+
+$TAB = 'Access Key';
+
+// Main include
+include($_SERVER['DOCUMENT_ROOT']."/inc/main.php");
+
+if (($_SESSION['userContext'] === 'admin') && (!empty($_GET['user']))) {
+    $user = escapeshellarg($_GET['user']);
+    $user_plain = $_GET['user'];
+}
+
+// Checks if API access is enabled
+$api_status = (!empty($_SESSION['API_SYSTEM']) && is_numeric($_SESSION['API_SYSTEM'])) ? $_SESSION['API_SYSTEM'] : 0;
+if (($user_plain == 'admin' && $api_status < 1) || ($user_plain != 'admin' && $api_status < 2)) {
+    header("Location: /edit/user/");
+    exit;
+}
+
+if (!empty($_GET['key'])) {
+    $v_key = escapeshellarg(trim($_GET['key']));
+
+    // Key data
+    exec(HESTIA_CMD."v-list-access-key ".$v_key." json", $output, $return_var);
+    $key_data = json_decode(implode('', $output), true);
+    unset($output);
+
+    if (empty($key_data) || $key_data['USER'] != $user_plain) {
+        header("Location: /list/access-key/");
+        exit;
+    }
+
+    // APIs
+    exec(HESTIA_CMD."v-list-apis json", $output, $return_var);
+    $apis = json_decode(implode('', $output), true);
+    $apis = array_filter($apis, function ($api) use ($user_plain) {
+        return ($user_plain == 'admin' || $api['ROLE'] == 'user');
+    });
+    ksort($apis);
+    unset($output);
+
+    render_page($user, $TAB, 'list_access_key');
+} else {
+    exec(HESTIA_CMD."v-list-access-keys $user json", $output, $return_var);
+    $data = json_decode(implode('', $output), true);
+
+    uasort($data, function ($a, $b) {
+        return $a['DATE'] <=> $b['DATE'] ?: $a['TIME'] <=> $b['TIME'];
+    });
+    unset($output);
+
+    // Render page
+    render_page($user, $TAB, 'list_access_keys');
+}
+
+// Back uri
+$_SESSION['back'] = $_SERVER['REQUEST_URI'];

BIN
web/locale/en/LC_MESSAGES/hestiacp.mo


BIN
web/locale/pt-br/LC_MESSAGES/hestiacp.mo


+ 80 - 0
web/templates/pages/add_access_key.html

@@ -0,0 +1,80 @@
+<!-- Begin toolbar -->
+<div class="l-center edit">
+    <div class="l-sort clearfix">
+        <div class="l-unit-toolbar__buttonstrip">
+            <a class="ui-button cancel" dir="ltr" id="btn-back" href="/list/access-key/"><i
+                        class="fas fa-arrow-left status-icon blue"></i><?= _('Back'); ?></a>
+        </div>
+        <div class="l-unit-toolbar__buttonstrip float-right">
+            <a href="#" class="ui-button" data-action="submit" data-id="vstobjects"><i
+                        class="fas fa-save status-icon purple"></i><?= _('Save'); ?></a>
+        </div>
+    </div>
+</div>
+<!-- End toolbar -->
+
+<div class="l-separator"></div>
+
+<div class="l-center animated fadeIn">
+
+    <form id="vstobjects" name="v_add_access_key" method="post">
+        <input type="hidden" name="token" value="<?= $_SESSION['token'] ?>"/>
+        <input type="hidden" name="ok" value="Add"/>
+
+        <table class="data mode-add">
+            <tr class="data-add">
+                <td class="data-dotted">
+                    <table class="data-col1">
+                        <tr>
+                            <td></td>
+                        </tr>
+                    </table>
+                </td>
+                <td class="data-dotted">
+                    <table class="data-col2">
+                        <tr>
+                            <td class="step-top">
+                                <span class="page-title"><?= _('Adding Access Key'); ?></span>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>
+                                <?php show_error_panel($_SESSION); ?>
+                            </td>
+                        </tr>
+
+                        <tr>
+                            <td class="vst-text input-label">
+                                <?= _('Permissions'); ?>
+                            </td>
+                        </tr>
+                        <?php
+                        foreach ($apis as $api_name => $api_data) {
+                            ?>
+                            <tr>
+                                <td class="vst-text input-label">
+                                    <label><input type="checkbox" size="20" class="vst-checkbox"
+                                                  name="v_apis[]" value="<?= $api_name ?>"
+                                                  tabindex="5"/><?= _($api_name); ?></label>
+                                </td>
+                            </tr>
+                            <?php
+                        } ?>
+
+                        <tr>
+                            <td class="vst-text step-top">
+                                <?= _('Comment'); ?> <span class="optional">(<?= _('optional'); ?>)</span>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>
+                                <input type="text" size="20" class="vst-input" name="v_comment" maxlength="255" />
+                            </td>
+                        </tr>
+                    </table>
+                    <table class="data-col2"></table>
+                </td>
+            </tr>
+        </table>
+    </form>
+</div>

+ 21 - 6
web/templates/pages/edit_server.html

@@ -293,7 +293,7 @@
 												if($php_version -> installed){
 												?>
 												<option value="<?=$php_version->version; ?>" <?php if($php_version->name == DEFAULT_PHP_VERSION){ echo "selected" ;}?> ><?=$php_version->name; ?></option>
-												<?php	
+												<?php
 												}
 												}
 												?>
@@ -957,7 +957,7 @@
 												</td>
 												<tr>
 													<td class="vst-text input-label">
-														<?=_('Enable API access');?>
+														<?=_('Enable legacy API access');?>
 													</td>
 												</tr>
 												<tr>
@@ -969,9 +969,24 @@
 														<br><br>
 													</td>
 												</tr>
+                                                <tr>
+                                                    <td class="vst-text input-label">
+                                                        <?=_('Enable API access');?>
+                                                    </td>
+                                                </tr>
+                                                <tr>
+                                                    <td>
+                                                        <select class="vst-list" name="v_api_system" id="api-system">
+                                                            <option value='0' <?php if(empty($_SESSION['API_SYSTEM']) || $_SESSION['API_SYSTEM'] == '0') echo 'selected' ?>><?=_('Disabled'); ?></option>
+                                                            <option value='1' <?php if($_SESSION['API_SYSTEM'] == '1') echo 'selected' ?>><?=_('Enabled for admin'); ?></option>
+                                                            <option value='2' <?php if($_SESSION['API_SYSTEM'] == '2') echo 'selected' ?>><?=_('Enabled for all users'); ?></option>
+                                                        </select>
+                                                        <br><br>
+                                                    </td>
+                                                </tr>
 												<tr>
 													<td>
-														<table id="security_ip" style="<?php if ($_SESSION['API'] == "no") echo 'display:none;';?>">
+														<table id="security_ip" style="<?php if ($_SESSION['API'] == "no" && $_SESSION['API_SYSTEM'] == '0') echo 'display:none;';?>">
 															<tr>
 																<td class="vst-text input-label">
 																	<?=_('Allowed IP addresses for API');?> <span class="optional" style="float:right">1 IP address per line</span>
@@ -979,8 +994,8 @@
 															</tr>
 															<tr>
 																<td>
-																	<textarea size="20" class="vst-textinput short" name="v_api_allowed_ip"><?php 
-																		foreach(explode(',',$_SESSION['API_ALLOWED_IP']) as $ip ){ 
+																	<textarea size="20" class="vst-textinput short" name="v_api_allowed_ip"><?php
+																		foreach(explode(',',$_SESSION['API_ALLOWED_IP']) as $ip ){
                                                                             echo trim($ip)."\n";
 																			}
 																		?></textarea>
@@ -1043,7 +1058,7 @@
 															<option value="0"  <?php if($_SESSION['POLICY_CSRF_STRICTNESS'] == '0') echo 'selected' ?>><?=_('Disabled');?></option>
 															<option value="1"  <?php if($_SESSION['POLICY_CSRF_STRICTNESS'] == '1') echo 'selected' ?>><?=_('Enabled');?></option>
 															<option value="2"  <?php if($_SESSION['POLICY_CSRF_STRICTNESS'] == '2') echo 'selected' ?>><?=_('Strict');?></option>
-															
+
 														</select>
 														<br><br>
 													</td>

+ 10 - 3
web/templates/pages/edit_user.html

@@ -3,19 +3,26 @@
 	<div class="l-sort clearfix">
 		<div class="l-unit-toolbar__buttonstrip">
 			<a class="ui-button cancel" dir="ltr" id="btn-back" href="/list/user/"><i class="fas fa-arrow-left status-icon blue"></i><?=_('Back');?></a>
-			<?php 
+			<?php
 				if (($_SESSION['userContext'] === 'admin') && (!isset($_SESSION['look'])) && ($_SESSION['user'] !== $v_username)) {
 					$ssh_key_url = "/list/key/?user=".htmlentities($user_plain)."&token=".$_SESSION['token']."";
 					$log_url = "/list/log/?user=".htmlentities($user_plain)."&token=".$_SESSION['token']."";
+					$keys_url = "/list/access-key/?user=".htmlentities($user_plain)."&token=".$_SESSION['token']."";
 				} else {
 					$ssh_key_url = "/list/key/";
 					$log_url = "/list/log/";
-				} 
+					$keys_url = "/list/access-key/";
+				}
 			?>
 			<a href="<?=$ssh_key_url; ?>" id="btn-create" class="ui-button cancel" dir="ltr" title="<?=_('Manage SSH keys');?>"><i class="fas fa-key status-icon orange"></i><?=_('Manage SSH keys');?></a>
 			<?php if (($_SESSION['userContext'] == 'admin') || ($_SESSION['userContext'] !== 'admin') && ($_SESSION['POLICY_USER_VIEW_LOGS'] !== 'no')) {?>
 				<a href="<?=$log_url; ?>" id="btn-create" class="ui-button cancel" dir="ltr" title="<?=_('Logs');?>"><i class="fas fa-history status-icon maroon"></i><?=_('Logs');?></a>
 			<?php } ?>
+			<?php
+            $api_status = (!empty($_SESSION['API_SYSTEM']) && is_numeric($_SESSION['API_SYSTEM'])) ? $_SESSION['API_SYSTEM'] : 0;
+            if (($user_plain == 'admin' && $api_status > 0) || ($user_plain != 'admin' && $api_status > 1)) { ?>
+				<a href="<?=$keys_url; ?>" id="btn-create" class="ui-button cancel" dir="ltr" title="<?=_('Access Keys');?>"><i class="fas fa-key status-icon purple"></i><?=_('Access Keys');?></a>
+			<?php } ?>
 		</div>
 		<div class="l-unit-toolbar__buttonstrip float-right">
 			<div class="actions-panel clearfix">
@@ -415,4 +422,4 @@
 			</tr>
 		</table>
 	</form>
-</div>
+</div>

+ 118 - 0
web/templates/pages/list_access_key.html

@@ -0,0 +1,118 @@
+<?php
+// Prevent resubmit form on page refresh
+if (!empty($_POST['ok'])) {
+    ?>
+    <script>
+        if (window.history.replaceState) {
+            window.history.replaceState(null, null, window.location.href);
+        }
+    </script>
+<?php } ?>
+
+<!-- Begin toolbar -->
+<div class="l-center edit">
+    <div class="l-sort clearfix">
+        <div class="l-unit-toolbar__buttonstrip">
+            <a class="ui-button cancel" dir="ltr" id="btn-back" href="/list/access-key/"><i
+                        class="fas fa-arrow-left status-icon blue"></i><?= _('Back'); ?></a>
+        </div>
+    </div>
+</div>
+<!-- End toolbar -->
+
+<div class="l-separator"></div>
+
+<div class="l-center animated fadeIn">
+
+    <form id="vstobjects">
+        <table class="data mode-add">
+            <tr class="data-add">
+                <td class="data-dotted">
+                    <table class="data-col1">
+                        <tr>
+                            <td></td>
+                        </tr>
+                    </table>
+                </td>
+                <td class="data-dotted">
+                    <table class="data-col2">
+                        <tr>
+                            <td class="step-top">
+                                <span class="page-title"><?= _("Access Key") ?></span>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>
+                                <?php show_error_panel($_SESSION); ?>
+                            </td>
+                        </tr>
+
+                        <?php if (!empty($key_data['ACCESS_KEY_ID'])) { ?>
+                            <tr>
+                                <td class="vst-text step-top">
+                                    <?= _('Access Key Id'); ?>
+                                </td>
+                            </tr>
+                            <tr>
+                                <td>
+                                    <input type="text" size="20" class="vst-input" maxlength="255" readonly
+                                           value="<?= htmlentities(trim($key_data['ACCESS_KEY_ID'], "'")) ?>">
+                                </td>
+                            </tr>
+                        <?php } ?>
+
+                        <?php
+                        if (!empty($_SESSION['ok_msg'])) {
+                            ?>
+                            <?php if (!empty($key_data['ACCESS_KEY_ID']) && !empty($key_data['SECRET_ACCESS_KEY'])) { ?>
+                                <tr>
+                                    <td class="vst-text input-label">
+                                        <?= _('Secret Key'); ?><br>
+                                        <span style="font-size: 10pt; color:#ffd500;"><?= _('Warning! Last chance to save secrect access key!'); ?></span>
+                                    </td>
+                                </tr>
+                                <tr>
+                                    <td>
+                                        <input type="text" size="20" class="vst-input" maxlength="255" readonly
+                                               value="<?= htmlentities(trim($key_data['SECRET_ACCESS_KEY'], "'")) ?>">
+                                    </td>
+                                </tr>
+                            <?php } ?>
+                        <?php }
+                        ?>
+
+                        <tr>
+                            <td class="vst-text step-top">
+                                <?= _('Permissions'); ?>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td class="vst-text">
+                                <ul>
+                                    <?php
+                                    foreach ($key_data['PERMISSIONS'] as $api_name) {
+                                        ?>
+                                        <li><?= _($api_name); ?></li>
+                                    <?php } ?>
+                                </ul>
+                            </td>
+                        </tr>
+
+                        <tr>
+                            <td class="vst-text input-label">
+                                <?= _('Comment'); ?>
+                            </td>
+                        </tr>
+                        <tr>
+                            <td>
+                                <input type="text" size="20" class="vst-input" maxlength="255" readonly
+                                       value="<?= htmlentities(trim($key_data['COMMENT'], "'")) ?>">
+                            </td>
+                        </tr>
+                    </table>
+                    <table class="data-col2"></table>
+                </td>
+            </tr>
+        </table>
+    </form>
+</div>

+ 116 - 0
web/templates/pages/list_access_keys.html

@@ -0,0 +1,116 @@
+<!-- Begin toolbar -->
+<div class="l-center">
+	<div class="l-sort clearfix noselect">
+		<div class="l-unit-toolbar__buttonstrip">
+			<a class="ui-button cancel" dir="ltr" id="btn-back" href="/edit/user/"><i class="fas fa-arrow-left status-icon blue"></i><?=_('Back');?></a>
+			<a href="/add/access-key/" id="btn-create" class="ui-button cancel" dir="ltr"><i class="fas fa-plus-circle status-icon green"></i><?=_('Add Access Key');?></a>
+		</div>
+
+		<ul class="context-menu sort-order animated fadeIn" style="display:none;">
+			<li entity="sort-date" sort_as_int="1"><span class="name <?php if ($_SESSION['userSortOrder'] === 'date') { echo 'active'; } ?>"><?=_('Date');?> <i class="fas fa-sort-alpha-down"></i></span><span class="up"><i class="fas fa-sort-alpha-up"></i></span></li>
+			<li entity="sort-key"><span class="name"><?=_('Access Key');?> <i class="fas fa-sort-alpha-down"></i></span><span class="up"><i class="fas fa-sort-alpha-up"></i></span></li>
+			<li entity="sort-comment"><span class="name"><?=_('Comment');?> <i class="fas fa-sort-alpha-down"></i></span><span class="up"><i class="fas fa-sort-alpha-up"></i></span></li>
+		</ul>
+		<div class="l-sort-toolbar clearfix">
+			<table>
+				<tr>
+					<td class="sort-by" title="<?=_('Sort items');?>">
+						<?=_('sort by');?>: <span><b><?=_('Date');?> <i class="fas fa-sort-alpha-down"></i></b></span>
+					</td>
+					<td>
+						<form action="/bulk/access-key/" method="post" id="objects">
+							<input type="hidden" name="token" value="<?=$_SESSION['token']?>" />
+							<div class="l-select">
+								<select name="action" id="">
+									<option value=""><?=_('apply to selected');?></option>
+									<option value="delete"><?=_('delete');?></option>
+								</select>
+							</div>
+							<button type="submit" class="l-sort-toolbar__filter-apply" value="" title="<?=_('apply to selected');?>"><i class="fas fa-arrow-right"></i></button>
+						</form>
+					</td>
+				</tr>
+			</table>
+		</div>
+	</div>
+</div>
+<!-- End toolbar -->
+
+<div class="l-separator"></div>
+
+<div class="l-center units">
+	<div class="header table-header">
+		<div class="l-unit__col l-unit__col--right">
+			<div>
+				<div class="clearfix l-unit__stat-col--left super-compact">
+					<input id="toggle-all" type="checkbox" name="toggle-all" value="toggle-all" title="<?=_('Select all');?>" onChange="checkedAll('objects');">
+				</div>
+				<div class="clearfix l-unit__stat-col--left wide-6"><b><?=_('Access Key');?></b></div>
+				<div class="clearfix l-unit__stat-col--left compact text-right"><b>&nbsp;</b></div>
+				<div class="clearfix l-unit__stat-col--left text-center wide-2"><b><?=_('Comment');?></b></div>
+				<div class="clearfix l-unit__stat-col--left text-center"><b><?=_('Date');?></b></div>
+				<div class="clearfix l-unit__stat-col--left text-center"><b><?=_('Time');?></b></div>
+			</div>
+		</div>
+	</div>
+
+	<!-- Begin Access Keys list item loop -->
+	<?php
+		foreach ($data as $key => $value) {
+			++$i;
+            $key_user = !empty($value['USER']) ? $value['USER'] : 'admin';
+            $key_comment = !empty($value['COMMENT']) ? $value['COMMENT'] : '-';
+            //$key_permissions = !empty($value['PERMISSIONS']) ? $value['PERMISSIONS'] : '-';
+            //$key_permissions = implode(' ', $key_permissions);
+            $key_date = !empty($value['DATE']) ? $value['DATE'] : '-';
+            $key_time = !empty($value['TIME']) ? $value['TIME'] : '-';
+		?>
+		<div class="l-unit animated fadeIn" v_unit_id="<?=$key?>"
+			v_section="key" sort-key="<?=strtolower($key)?>"
+            sort-comment="<?=strtolower($key_comment)?>"
+            sort-date="<?=strtotime($data[$key]['DATE'] .' '. $data[$key]['TIME'] )?>">
+
+			<div class="l-unit__col l-unit__col--right">
+				<div class="clearfix l-unit__stat-col--left super-compact">
+					<input id="check<?=$i ?>" class="ch-toggle" type="checkbox" title="<?=_('Select');?>" name="key[]" value="<?=$key?>">
+				</div>
+                <div class="clearfix l-unit__stat-col--left wide-6">
+                    <b><a href="/list/access-key/?key=<?=htmlentities($key);?>&token=<?=$_SESSION['token']?>" title="<?=_('Access Key');?>: <?=$key;?>"><?=$key;?></a></b>
+                </div>
+
+				<!-- START QUICK ACTION TOOLBAR AREA -->
+				<div class="clearfix l-unit__stat-col--left compact text-right">
+					<div class="l-unit-toolbar__col l-unit-toolbar__col--right noselect">
+						<div class="actions-panel clearfix">
+							<div class="actions-panel__col actions-panel__delete shortcut-delete" key-action="js">
+								<a id="delete_link_<?=$i?>" class="data-controls do_delete" title="<?=_('delete');?>">
+									<i class="fas fa-trash status-icon red status-icon dim do_delete"></i>
+									<input type="hidden" name="delete_url" value="/delete/access-key/?key=<?=$key?>&token=<?=$_SESSION['token']?>" />
+									<div id="delete_dialog_<?=$i?>" class="confirmation-text-delete hidden" title="<?=_('Confirmation');?>">
+										<p class="confirmation"><?=sprintf(_('DELETE_ACCESS_KEY_CONFIRMATION'),$key)?></p>
+									</div>
+								</a>
+							</div>
+						</div>
+					</div>
+				</div>
+				<!-- END QUICK ACTION TOOLBAR AREA -->
+				<div class="clearfix l-unit__stat-col--left text-center wide-2"><b><?=_($key_comment)?></b></div>
+                <div class="clearfix l-unit__stat-col--left text-center"><b><?=$key_date?></b></div>
+                <div class="clearfix l-unit__stat-col--left text-center"><b><?=$key_time?></b></div>
+			</div>
+		</div>
+	<?php } ?>
+</div>
+
+<div id="vstobjects">
+	<div class="l-separator"></div>
+	<div class="l-center">
+		<div class="l-unit-ft">
+			<table class='data'></table>
+			<div class="data-count l-unit__col l-unit__col--right clearfix">
+                <?php printf(ngettext('%d Access Key', '%d Access Keys', $i),$i); ?>
+			</div>
+		</div>
+	</div>
+</div>