diff --git a/.drone.yml b/.drone.yml deleted file mode 100644 index 8587219f..00000000 --- a/.drone.yml +++ /dev/null @@ -1,120 +0,0 @@ ---- -kind: pipeline -name: integration-testing - -platform: - os: linux - arch: amd64 - -clone: - disable: true - -steps: -- name: prepare-tests - pull: default - image: timovibritannia/ansible - commands: - - git clone https://github.com/mailcow/mailcow-integration-tests.git --branch $(curl -sL https://api.github.com/repos/mailcow/mailcow-integration-tests/releases/latest | jq -r '.tag_name') --single-branch . - - chmod +x ci.sh - - chmod +x ci-ssh.sh - - chmod +x ci-piprequierments.sh - - ./ci.sh - - wget -O group_vars/all/secrets.yml $SECRETS_DOWNLOAD_URL --quiet - environment: - SECRETS_DOWNLOAD_URL: - from_secret: SECRETS_DOWNLOAD_URL - VAULT_PW: - from_secret: VAULT_PW - when: - branch: - - master - - staging - event: - - push - -- name: lint - pull: default - image: timovibritannia/ansible - commands: - - ansible-lint ./ - when: - branch: - - master - - staging - event: - - push - -- name: create-server - pull: default - image: timovibritannia/ansible - commands: - - ./ci-piprequierments.sh - - ansible-playbook mailcow-start-server.yml --diff - - ./ci-ssh.sh - environment: - ANSIBLE_HOST_KEY_CHECKING: false - ANSIBLE_FORCE_COLOR: true - when: - branch: - - master - - staging - event: - - push - -- name: setup-server - pull: default - image: timovibritannia/ansible - commands: - - sleep 120 - - ./ci-piprequierments.sh - - ansible-playbook mailcow-setup-server.yml --private-key /drone/src/id_ssh_rsa --diff - environment: - ANSIBLE_HOST_KEY_CHECKING: false - ANSIBLE_FORCE_COLOR: true - when: - branch: - - master - - staging - event: - - push - -- name: run-tests - pull: default - image: timovibritannia/ansible - commands: - - ./ci-piprequierments.sh - - ansible-playbook mailcow-integration-tests.yml --private-key /drone/src/id_ssh_rsa --diff - environment: - ANSIBLE_HOST_KEY_CHECKING: false - ANSIBLE_FORCE_COLOR: true - when: - branch: - - master - - staging - event: - - push - -- name: delete-server - pull: default - image: timovibritannia/ansible - commands: - - ./ci-piprequierments.sh - - ansible-playbook mailcow-delete-server.yml --diff - environment: - ANSIBLE_HOST_KEY_CHECKING: false - ANSIBLE_FORCE_COLOR: true - when: - branch: - - master - - staging - event: - - push - status: - - failure - - success - ---- -kind: signature -hmac: f6619243fe2a27563291c9f2a46d93ffbc3b6dced9a05f23e64b555ce03a31e5 - -... diff --git a/.github/workflows/close_old_issues_and_prs.yml b/.github/workflows/close_old_issues_and_prs.yml index e746bc6f..58cfdccd 100644 --- a/.github/workflows/close_old_issues_and_prs.yml +++ b/.github/workflows/close_old_issues_and_prs.yml @@ -14,7 +14,7 @@ jobs: pull-requests: write steps: - name: Mark/Close Stale Issues and Pull Requests 🗑️ - uses: actions/stale@v5.0.0 + uses: actions/stale@v5.1.1 with: repo-token: ${{ secrets.STALE_ACTION_PAT }} days-before-stale: 60 @@ -30,6 +30,7 @@ jobs: stale-issue-label: "stale" stale-pr-label: "stale" exempt-draft-pr: "true" + close-issue-reason: "not_planned" operations-per-run: "250" ascending: "true" #DRY-RUN diff --git a/.github/workflows/image_builds.yml b/.github/workflows/image_builds.yml new file mode 100644 index 00000000..007b1014 --- /dev/null +++ b/.github/workflows/image_builds.yml @@ -0,0 +1,42 @@ +name: Build mailcow Docker Images + +on: + push: + branches: [ "master", "staging" ] + workflow_dispatch: + +jobs: + docker_image_builds: + strategy: + matrix: + images: + - "acme-mailcow" + - "clamd-mailcow" + - "dockerapi-mailcow" + - "dovecot-mailcow" + - "netfilter-mailcow" + - "olefy-mailcow" + - "php-fpm-mailcow" + - "postfix-mailcow" + - "rspamd-mailcow" + - "sogo-mailcow" + - "solr-mailcow" + - "unbound-mailcow" + - "watchdog-mailcow" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup Docker + run: | + curl -sSL https://get.docker.com/ | CHANNEL=stable sudo sh + sudo service docker start + sudo curl -L https://github.com/docker/compose/releases/download/v$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose + sudo chmod +x /usr/local/bin/docker-compose + - name: Prepair Image Builds + run: | + cp helper-scripts/docker-compose.override.yml.d/BUILD_FLAGS/docker-compose.override.yml docker-compose.override.yml + - name: Build Docker Images + run: | + docker-compose build ${image} + env: + image: ${{ matrix.images }} diff --git a/.github/workflows/integration_tests.yml b/.github/workflows/integration_tests.yml new file mode 100644 index 00000000..7d6c4ac2 --- /dev/null +++ b/.github/workflows/integration_tests.yml @@ -0,0 +1,60 @@ +name: mailcow Integration Tests + +on: + push: + branches: [ "master", "staging" ] + workflow_dispatch: + +jobs: + integration_tests: + runs-on: ubuntu-latest + steps: + - name: Setup Ansible + run: | + export DEBIAN_FRONTEND=noninteractive + sudo apt-get update + sudo apt-get install python3 python3-pip git + sudo pip3 install ansible + - name: Prepair Test Environment + run: | + git clone https://github.com/mailcow/mailcow-integration-tests.git --branch $(curl -sL https://api.github.com/repos/mailcow/mailcow-integration-tests/releases/latest | jq -r '.tag_name') --single-branch . + ./fork_check.sh + ./ci.sh + ./ci-pip-requirements.sh + env: + VAULT_PW: ${{ secrets.MAILCOW_TESTS_VAULT_PW }} + VAULT_FILE: ${{ secrets.MAILCOW_TESTS_VAULT_FILE }} + - name: Start Integration Test Server + run: | + ./fork_check.sh + ansible-playbook mailcow-start-server.yml --diff + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + ANSIBLE_HOST_KEY_CHECKING: 'false' + - name: Setup Integration Test Server + run: | + ./fork_check.sh + sleep 30 + ansible-playbook mailcow-setup-server.yml --private-key id_ssh_rsa --diff + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + ANSIBLE_HOST_KEY_CHECKING: 'false' + - name: Run Integration Tests + run: | + ./fork_check.sh + ansible-playbook mailcow-integration-tests.yml --private-key id_ssh_rsa --diff + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + ANSIBLE_HOST_KEY_CHECKING: 'false' + - name: Delete Integration Test Server + if: always() + run: | + ./fork_check.sh + ansible-playbook mailcow-delete-server.yml --diff + env: + PY_COLORS: '1' + ANSIBLE_FORCE_COLOR: '1' + ANSIBLE_HOST_KEY_CHECKING: 'false' diff --git a/.github/workflows/tweet-trigger-publish-release.yml b/.github/workflows/tweet-trigger-publish-release.yml new file mode 100644 index 00000000..82f1dc3a --- /dev/null +++ b/.github/workflows/tweet-trigger-publish-release.yml @@ -0,0 +1,17 @@ +name: "Tweet trigger release" +on: + release: + types: [published] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Tweet-trigger-publish-release + uses: mugi111/tweet-trigger-release@v1.1 + with: + consumer_key: ${{ secrets.CONSUMER_KEY }} + consumer_secret: ${{ secrets.CONSUMER_SECRET }} + access_token_key: ${{ secrets.ACCESS_TOKEN_KEY }} + access_token_secret: ${{ secrets.ACCESS_TOKEN_SECRET }} + tweet_body: 'A new mailcow-dockerized Release has been Released on GitHub! Checkout our GitHub Page for the latest Release: github.com/mailcow/mailcow-dockerized/releases/latest' diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 84357090..00000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -sudo: required -services: -- docker -script: -- echo 'Europe/Berlin' | MAILCOW_HOSTNAME=build.mailcow ./generate_config.sh -- docker-compose pull --ignore-pull-failures --parallel -- docker-compose build -- docker login --username=$DOCKER_HUB_USERNAME --password=$DOCKER_HUB_PASSWORD -- docker-compose push -branches: - only: - - master_disabled -env: - global: - - secure: MpxpTwD7f0CNEVLitSpVmocK7O9r+BwFE1deEHK4AlQo/oc9cOlhGe1EL3mx9zbglPmjlDg/8kMUGv6vSirIabfBo9Szjps76bHckFr9lr2Ykkg0e29oC8pgPpSXD1eY/1ZIN/FvIkxpUFLETo1okS/j9q/A0DCGFmti0n3EoMORsgRz9CpNAiEh0zpSd6+euPAGHuczuCrDuO84my9bIOCjA/+aPunHNeXiuM8yIM2SxCSyGtIKT0+jvquIvLF58VxivysXBlRfhDn8fhB09nXA2Ru/derYQACfcmNSn9Pd4bDpebPJW5B9H/XA8xjb58uKinUlncbAMB/QnxoT75j9YRWJZRSQ+34XNYP6ZgK9soZ2TC6djQyEKTUu45Kp/1s+poSn42m9jytJJTmmK0KxsZTRcC8JD5nrjIMZWPUNNTwC5L4+I7ZRWg2WooK3LNyq1Ng8Hn6W77wSgsvAJw2HD3Lx58AprGUhHuBeaIZRuSN9aKwZrl9vKQJLqPnOp/nF2EC6kot5HYYtcotGtETXPUDih21gWD5ZM2BqVqYfQQnJnNMgeYmMdj6QQuTFqhuNJf7hXRIRkTnD3j1gDOLKQZazW0+N2JE8XWDFwi6fKScDsxT85lJti9HmzHa7+k4RVHmUYuDgRoPuzUgjWHvPsiz3/Z8WQ9JYpH84S8w= - - secure: fWzZisT6nGDNL4lf6tXB07eFG2drgBakHxzdF/NFVvzuP861RFR6omuL+ED0PgXrEHDJBxaBLv52je8irmUXrAH1CNr7T8DWiZo/h5h609Uzr+38T1NnIu4krL0Wo6/CDwlLKnzqTq9yBIZLQSHVJmo8AOpo1JPIi2ajodqj9ZfmAxDQTQl+G6zvQjtqIkYHsHY7A44Rto0f14ykn7w2S82Jn6Ry89VNI5V1WEO3sMpM/XekNP/HokNcRIuntL/0+kuLvTJ5akGoTjBQxSnSW95opzPeGky74HRU2obExJYqKvF0VfVJRNAqejwjIiFIbbjqV0Sk5391kFuhuBErQQDM1bOHGdxZ41HsJH29qNWIl7C33Yl10qERoqecgsJ1N/bS2ZEmWqm/zQh5GClCXPvYmzEqMYsMGM3vjbKdjDlc1Wh2w/eFclsXN9LSXh1mc35rtj46frcT6e5Kof87AIfC9hTgDvk9kAsyjaHMkSHSZthbZXCIcsD8qriNm5UqfFBYD79mPIP1S2YMQ2jscCsjHOZgYVrcm0kzDF21J1w6H0Lo7d1jw37LYlegBdtLQ9gYgqY2D5m+nxWuVoD5FZmpR+5JGtK+ootyLFF8aiFoHXd4op1JCxRLjgkmnZKXzw3kTQSpE7oa7CgzchtQmK2nqcqla1b5Qk7ilVcjooo= diff --git a/README.md b/README.md index 7abd1024..313fa13b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ ## We stand with 🇺🇦 -[![master build status](https://img.shields.io/drone/build/mailcow/mailcow-dockerized/master?label=master%20build&server=https%3A%2F%2Fdrone.mailcow.email)](https://drone.mailcow.email/mailcow/mailcow-dockerized) [![staging build status](https://img.shields.io/drone/build/mailcow/mailcow-dockerized/staging?label=staging%20build&server=https%3A%2F%2Fdrone.mailcow.email)](https://drone.mailcow.email/mailcow/mailcow-dockerized) [![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/) +[![Mailcow Integration Tests](https://github.com/mailcow/mailcow-dockerized/actions/workflows/integration_tests.yml/badge.svg?branch=master)](https://github.com/mailcow/mailcow-dockerized/actions/workflows/integration_tests.yml) +[![Translation status](https://translate.mailcow.email/widgets/mailcow-dockerized/-/translation/svg-badge.svg)](https://translate.mailcow.email/engage/mailcow-dockerized/) [![Twitter URL](https://img.shields.io/twitter/url/https/twitter.com/mailcow_email.svg?style=social&label=Follow%20%40mailcow_email)](https://twitter.com/mailcow_email) ## Want to support mailcow? diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..de63ca3e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,42 @@ +# Security Policies and Procedures + +This document outlines security procedures and general policies for the _mailcow: dockerized_ project as found on [mailcow-dockerized](https://github.com/mailcow/mailcow-dockerized). + + * [Reporting a Vulnerability](#reporting-a-vulnerability) + * [Disclosure Policy](#disclosure-policy) + * [Comments on this Policy](#comments-on-this-policy) + +## Reporting a Vulnerability + +The mailcow team and community take all security vulnerabilities +seriously. Thank you for improving the security of our open source +software. We appreciate your efforts and responsible disclosure and will +make every effort to acknowledge your contributions. + +Report security vulnerabilities by emailing the mailcow team at: + + info at servercow.de + +mailcow team will acknowledge your email as soon as possible, and will +send a more detailed response afterwards indicating the next steps in +handling your report. After the initial reply to your report, the mailcow +team will endeavor to keep you informed of the progress towards a fix and +full announcement, and may ask for additional information or guidance. + +Report security vulnerabilities in third-party modules to the person or +team maintaining the module. + +## Disclosure Policy + +When the mailcow team receives a security bug report, they will assign it +to a primary handler. This person will coordinate the fix and release +process, involving the following steps: + + * Confirm the problem and determine the affected versions. + * Audit code to find any potential similar problems. + * Prepare fixes for all releases still under maintenance. + +## Comments on this Policy + +If you have suggestions on how this process could be improved please submit a +pull request. diff --git a/create_cold_standby.sh b/create_cold_standby.sh old mode 100644 new mode 100755 diff --git a/data/Dockerfiles/clamd/Dockerfile b/data/Dockerfiles/clamd/Dockerfile index a4971133..efbc6a4d 100644 --- a/data/Dockerfiles/clamd/Dockerfile +++ b/data/Dockerfiles/clamd/Dockerfile @@ -1,4 +1,4 @@ -FROM clamav/clamav:0.105.0_base +FROM clamav/clamav:0.105.1_base LABEL maintainer "André Peters " @@ -8,8 +8,14 @@ RUN apk upgrade --no-cache \ bind-tools \ bash -COPY clamd.sh ./ +# init +COPY clamd.sh /clamd.sh RUN chmod +x /sbin/tini +# healthcheck +COPY healthcheck.sh /healthcheck.sh +RUN chmod +x /healthcheck.sh +HEALTHCHECK --start-period=6m CMD "/healthcheck.sh" + ENTRYPOINT [] CMD ["/sbin/tini", "-g", "--", "/clamd.sh"] \ No newline at end of file diff --git a/data/Dockerfiles/clamd/healthcheck.sh b/data/Dockerfiles/clamd/healthcheck.sh new file mode 100755 index 00000000..6c18ac06 --- /dev/null +++ b/data/Dockerfiles/clamd/healthcheck.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +if [[ "${SKIP_CLAMD}" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "SKIP_CLAMD=y, skipping ClamAV..." + exit 0 +fi + +# run clamd healthcheck +/usr/local/bin/clamdcheck.sh diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index 4a914904..4e90052b 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -2,7 +2,7 @@ FROM debian:bullseye-slim LABEL maintainer "Andre Peters " ARG DEBIAN_FRONTEND=noninteractive -ARG DOVECOT=2.3.18 +ARG DOVECOT=2.3.19.1 ENV LC_ALL C ENV GOSU_VERSION 1.14 diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 9ac2dc64..ac7aeb1d 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -349,6 +349,14 @@ sievec /var/vmail/sieve/global_sieve_after.sieve sievec /usr/lib/dovecot/sieve/report-spam.sieve sievec /usr/lib/dovecot/sieve/report-ham.sieve +for file in /var/vmail/*/*/sieve/*.sieve ; do + if [[ "$file" == "/var/vmail/*/*/sieve/*.sieve" ]]; then + continue + fi + sievec "$file" "$(dirname "$file")/../.dovecot.svbin" + chown vmail:vmail "$(dirname "$file")/../.dovecot.svbin" +done + # Fix permissions chown root:root /etc/dovecot/sql/*.conf chown root:dovecot /etc/dovecot/sql/dovecot-dict-sql-sieve* /etc/dovecot/sql/dovecot-dict-sql-quota* /etc/dovecot/lua/passwd-verify.lua diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index b7aca757..33a6e409 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -194,7 +194,6 @@ plugin { fts_solr = url=http://solr:8983/solr/dovecot-fts/ quota = dict:Userquota::proxy::sqlquota quota_rule2 = Trash:storage=+100%% - sieve = /var/vmail/sieve/%u.sieve sieve_plugins = sieve_imapsieve sieve_extprograms sieve_vacation_send_from_recipient = yes sieve_redirect_envelope_from = recipient diff --git a/data/conf/rspamd/local.d/groups.conf b/data/conf/rspamd/local.d/groups.conf index 9ca3409d..f77d8a46 100644 --- a/data/conf/rspamd/local.d/groups.conf +++ b/data/conf/rspamd/local.d/groups.conf @@ -18,6 +18,9 @@ symbols { "ENCRYPTED_CHAT" { score = -20.0; } + "SOGO_CONTACT" { + score = -99.0; + } } group "MX" { diff --git a/data/web/api/openapi.yaml b/data/web/api/openapi.yaml index e9655751..8fb6245c 100644 --- a/data/web/api/openapi.yaml +++ b/data/web/api/openapi.yaml @@ -518,21 +518,23 @@ paths: - domain.tld type: success schema: - properties: - log: - description: contains request object - items: {} - type: array - msg: - items: {} - type: array - type: - enum: - - success - - danger - - error - type: string - type: object + type: array + items: + type: object + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string description: OK headers: {} tags: @@ -579,6 +581,11 @@ paths: domain: description: Fully qualified domain name type: string + gal: + description: >- + is domain global address list active or not, it enables + shared contacts accross domain in SOGo webmail + type: boolean mailboxes: description: limit count of mailboxes associated with this domain type: number @@ -596,6 +603,9 @@ paths: if not, them you have to create "dummy" mailbox for each address to relay type: boolean + relay_unknown_only: + description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally. + type: boolean rl_frame: enum: - s @@ -606,6 +616,11 @@ paths: rl_value: description: rate limit value type: number + tags: + description: tags for this Domain + type: array + items: + type: string type: object summary: Create domain /api/v1/add/domain-admin: @@ -1952,21 +1967,23 @@ paths: - domain2.tld type: success schema: - properties: - log: - description: contains request object - items: {} - type: array - msg: - items: {} - type: array - type: - enum: - - success - - danger - - error - type: string - type: object + type: array + items: + type: object + properties: + log: + description: contains request object + items: {} + type: array + msg: + items: {} + type: array + type: + enum: + - success + - danger + - error + type: string description: OK headers: {} tags: @@ -1977,14 +1994,15 @@ paths: content: application/json: schema: + type: object example: - domain.tld - domain2.tld properties: - items: - description: contains list of domains you want to delete - type: object - type: object + items: + type: array + items: + type: string summary: Delete domain /api/v1/delete/domain-admin: post: @@ -2972,23 +2990,25 @@ paths: $ref: "#/components/responses/Unauthorized" "200": content: - "*/*": + application/json: schema: - properties: - log: - description: contains request object - items: {} - type: array - msg: - items: {} - type: array - type: - enum: - - success - - danger - - error - type: string - type: object + type: array + items: + type: object + properties: + log: + type: array + description: contains request object + items: {} + msg: + type: array + items: {} + type: + enum: + - success + - danger + - error + type: string description: OK headers: {} tags: @@ -3056,13 +3076,33 @@ paths: if not, them you have to create "dummy" mailbox for each address to relay type: boolean + relay_unknown_only: + description: Relay non-existing mailboxes only. Existing mailboxes will be delivered locally. + type: boolean relayhost: description: id of relayhost type: number + rl_frame: + enum: + - s + - m + - h + - d + type: string + rl_value: + description: rate limit value + type: number + tags: + description: tags for this Domain + type: array + items: + type: string type: object items: description: contains list of domain names you want update - type: object + type: array + items: + type: string type: object summary: Update domain /api/v1/edit/fail2ban: @@ -3953,6 +3993,8 @@ paths: in: query name: tags required: false + schema: + type: string - description: e.g. api-key-string example: api-key-string in: header @@ -4512,6 +4554,8 @@ paths: in: query name: tags required: false + schema: + type: string - description: e.g. api-key-string example: api-key-string in: header diff --git a/data/web/css/build/013-mailcow.css b/data/web/css/build/013-mailcow.css index 49a47693..88d82e0c 100644 --- a/data/web/css/build/013-mailcow.css +++ b/data/web/css/build/013-mailcow.css @@ -270,27 +270,6 @@ code { .flag-icon { margin-right: 5px; } -.dropdown-header { - font-weight: 600; -} - -.dataTables_info { - margin: 15px 0 !important; - padding: 0px !important; -} -.dataTables_paginate, .dataTables_length, .dataTables_filter { - margin: 15px 0 !important; -} -.dtr-details { - width: 100%; -} -.dtr-title { - width: 20%; -} -table.dataTable>tbody>tr.child ul.dtr-details>li { - border-bottom: 1px solid rgba(239, 239, 239, 0.129); - padding: 0.5em 0; -} .tag-box { display: flex; @@ -328,6 +307,7 @@ table.dataTable>tbody>tr.child ul.dtr-details>li { align-items: center; display: inline-flex; } + #dnstable { overflow-x: auto!important; } @@ -335,61 +315,4 @@ table.dataTable>tbody>tr.child ul.dtr-details>li { border: 1px solid #dfdfdf; background-color: #f9f9f9; padding: 10px; -} - - -table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before:hover, -table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before:hover { - background-color: #5e5e5e; -} -table.dataTable.dtr-inline.collapsed>tbody>tr>td.dtr-control:before, -table.dataTable.dtr-inline.collapsed>tbody>tr>th.dtr-control:before, -table.dataTable td.dt-control:before { - background-color: #979797 !important; - border: 1.5px solid #616161 !important; - border-radius: 2px !important; - color: #fff; - height: 1em; - width: 1em; - line-height: 1.25em; - border-radius: 0px; - box-shadow: none; - font-size: 14px; - transition: 0.5s all; -} -table.dataTable.dtr-inline.collapsed>tbody>tr.parent>td.dtr-control:before, -table.dataTable.dtr-inline.collapsed>tbody>tr.parent>th.dtr-control:before, -table.dataTable td.dt-control:before { - background-color: #979797 !important; -} -table.dataTable.dtr-inline.collapsed>tbody>tr>td.child, -table.dataTable.dtr-inline.collapsed>tbody>tr>th.child, -table.dataTable.dtr-inline.collapsed>tbody>tr>td.dataTables_empty { - background-color: #fbfbfb; -} -table.dataTable.table-striped>tbody>tr>td { - vertical-align: middle; -} -table.dataTable.table-striped>tbody>tr>td>input[type="checkbox"] { - margin-top: 7px; -} - - -.btn-check-label { - color: #555; -} - -.caret { - transform: rotate(0deg); -} -a[aria-expanded='true'] > .caret, -button[aria-expanded='true'] > .caret { - transform: rotate(-180deg); -} - -.list-group-details { - background: #fff; -} -.list-group-header { - background: #f7f7f7; } \ No newline at end of file diff --git a/data/web/inc/ajax/destroy_tfa_auth.php b/data/web/inc/ajax/destroy_tfa_auth.php index 72c7f1e3..07873b55 100644 --- a/data/web/inc/ajax/destroy_tfa_auth.php +++ b/data/web/inc/ajax/destroy_tfa_auth.php @@ -2,5 +2,5 @@ session_start(); unset($_SESSION['pending_mailcow_cc_username']); unset($_SESSION['pending_mailcow_cc_role']); -unset($_SESSION['pending_tfa_method']); +unset($_SESSION['pending_tfa_methods']); ?> diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index ccfdbdda..61d81dff 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -23,18 +23,43 @@ if (is_array($alertbox_log_parser)) { unset($_SESSION['return']); } +// map tfa details for twig +$pending_tfa_authmechs = []; +foreach($_SESSION['pending_tfa_methods'] as $authdata){ + $pending_tfa_authmechs[$authdata['authmech']] = false; +} +if (isset($pending_tfa_authmechs['webauthn'])) { + $pending_tfa_authmechs['webauthn'] = true; +} +if (!isset($pending_tfa_authmechs['webauthn']) + && isset($pending_tfa_authmechs['yubi_otp'])) { + $pending_tfa_authmechs['yubi_otp'] = true; +} +if (!isset($pending_tfa_authmechs['webauthn']) + && !isset($pending_tfa_authmechs['yubi_otp']) + && isset($pending_tfa_authmechs['totp'])) { + $pending_tfa_authmechs['totp'] = true; +} +if (isset($pending_tfa_authmechs['u2f'])) { + $pending_tfa_authmechs['u2f'] = true; +} + // globals $globalVariables = [ 'mailcow_info' => array( 'version_tag' => $GLOBALS['MAILCOW_GIT_VERSION'], 'last_version_tag' => $GLOBALS['MAILCOW_LAST_GIT_VERSION'], - 'project_url' => $GLOBALS['MAILCOW_GIT_URL'], - 'project_owner' => $GLOBALS['MAILCOW_GIT_OWNER'], - 'project_repo' => $GLOBALS['MAILCOW_GIT_REPO'], - 'updatedAt' => $GLOBALS['MAILCOW_UPDATEDAT'] + 'git_owner' => $GLOBALS['MAILCOW_GIT_OWNER'], + 'git_repo' => $GLOBALS['MAILCOW_GIT_REPO'], + 'git_project_url' => $GLOBALS['MAILCOW_GIT_URL'], + 'git_commit' => $GLOBALS['MAILCOW_GIT_COMMIT'], + 'git_commit_date' => $GLOBALS['MAILCOW_GIT_COMMIT_DATE'], + 'mailcow_branch' => $GLOBALS['MAILCOW_BRANCH'], + 'updated_at' => $GLOBALS['MAILCOW_UPDATEDAT'] ), 'js_path' => '/cache/'.basename($JSPath), - 'pending_tfa_method' => @$_SESSION['pending_tfa_method'], + 'pending_tfa_methods' => @$_SESSION['pending_tfa_methods'], + 'pending_tfa_authmechs' => $pending_tfa_authmechs, 'pending_mailcow_cc_username' => @$_SESSION['pending_mailcow_cc_username'], 'lang_footer' => json_encode($lang['footer']), 'lang_acl' => json_encode($lang['acl']), diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index b1f7764d..3c767fad 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -833,11 +833,15 @@ function check_login($user, $pass, $app_passwd_data = false) { $stmt->execute(array(':user' => $user)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { + // verify password if (verify_hash($row['password'], $pass)) { - if (get_tfa($user)['name'] != "none") { + // check for tfa authenticators + $authenticators = get_tfa($user); + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { + // active tfa authenticators found, set pending user login $_SESSION['pending_mailcow_cc_username'] = $user; $_SESSION['pending_mailcow_cc_role'] = "admin"; - $_SESSION['pending_tfa_method'] = get_tfa($user)['name']; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; unset($_SESSION['ldelay']); $_SESSION['return'][] = array( 'type' => 'info', @@ -845,8 +849,7 @@ function check_login($user, $pass, $app_passwd_data = false) { 'msg' => 'awaiting_tfa_confirmation' ); return "pending"; - } - else { + } else { unset($_SESSION['ldelay']); // Reactivate TFA if it was set to "deactivate TFA for next login" $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); @@ -869,11 +872,14 @@ function check_login($user, $pass, $app_passwd_data = false) { $stmt->execute(array(':user' => $user)); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { + // verify password if (verify_hash($row['password'], $pass) !== false) { - if (get_tfa($user)['name'] != "none") { + // check for tfa authenticators + $authenticators = get_tfa($user); + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { $_SESSION['pending_mailcow_cc_username'] = $user; $_SESSION['pending_mailcow_cc_role'] = "domainadmin"; - $_SESSION['pending_tfa_method'] = get_tfa($user)['name']; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; unset($_SESSION['ldelay']); $_SESSION['return'][] = array( 'type' => 'info', @@ -933,24 +939,47 @@ function check_login($user, $pass, $app_passwd_data = false) { $rows = array_merge($rows, $stmt->fetchAll(PDO::FETCH_ASSOC)); } foreach ($rows as $row) { - if (verify_hash($row['password'], $pass) !== false) { - unset($_SESSION['ldelay']); - $_SESSION['return'][] = array( - 'type' => 'success', - 'log' => array(__FUNCTION__, $user, '*'), - 'msg' => array('logged_in_as', $user) - ); - if ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { - $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; - $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)"); - $stmt->execute(array( - ':service' => $service, - ':app_id' => $row['app_passwd_id'], - ':username' => $user, - ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) - )); + // verify password + if ($app_passwd_data['eas'] !== true && $app_passwd_data['dav'] !== true){ + if (verify_hash($row['password'], $pass) !== false) { + // check for tfa authenticators + $authenticators = get_tfa($user); + if (isset($authenticators['additional']) && is_array($authenticators['additional']) && count($authenticators['additional']) > 0) { + $_SESSION['pending_mailcow_cc_username'] = $user; + $_SESSION['pending_mailcow_cc_role'] = "user"; + $_SESSION['pending_tfa_methods'] = $authenticators['additional']; + unset($_SESSION['ldelay']); + $_SESSION['return'][] = array( + 'type' => 'success', + 'log' => array(__FUNCTION__, $user, '*'), + 'msg' => array('logged_in_as', $user) + ); + return "pending"; + } else { + // Reactivate TFA if it was set to "deactivate TFA for next login" + $stmt = $pdo->prepare("UPDATE `tfa` SET `active`='1' WHERE `username` = :user"); + $stmt->execute(array(':user' => $user)); + + unset($_SESSION['ldelay']); + return "user"; + } + } + } elseif ($app_passwd_data['eas'] === true || $app_passwd_data['dav'] === true) { + if (array_key_exists("app_passwd_id", $row)){ + if (verify_hash($row['password'], $pass) !== false) { + $service = ($app_passwd_data['eas'] === true) ? 'EAS' : 'DAV'; + $stmt = $pdo->prepare("REPLACE INTO sasl_log (`service`, `app_password`, `username`, `real_rip`) VALUES (:service, :app_id, :username, :remote_addr)"); + $stmt->execute(array( + ':service' => $service, + ':app_id' => $row['app_passwd_id'], + ':username' => $user, + ':remote_addr' => ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']) + )); + + unset($_SESSION['ldelay']); + return "user"; + } } - return "user"; } } @@ -1145,47 +1174,46 @@ function set_tfa($_data) { global $yubi; global $tfa; $_data_log = $_data; + $access_denied = null; !isset($_data_log['confirm_password']) ?: $_data_log['confirm_password'] = '*'; $username = $_SESSION['mailcow_cc_username']; - if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - $stmt = $pdo->prepare("SELECT `password` FROM `admin` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if (!empty($num_results)) { - if (!verify_hash($row['password'], $_data["confirm_password"])) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } - } - $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` - WHERE `username` = :username"); - $stmt->execute(array(':username' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if (!empty($num_results)) { - if (!verify_hash($row['password'], $_data["confirm_password"])) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; + + // check for empty user and role + if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true; + + // check admin confirm password + if ($access_denied === null) { + $stmt = $pdo->prepare("SELECT `password` FROM `admin` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($row) { + if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true; + else $access_denied = false; } } + // check mailbox confirm password + if ($access_denied === null) { + $stmt = $pdo->prepare("SELECT `password` FROM `mailbox` + WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($row) { + if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true; + else $access_denied = false; + } + } + + // set access_denied error + if ($access_denied){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_data_log), + 'msg' => 'access_denied' + ); + return false; + } switch ($_data["tfa_method"]) { case "yubi_otp": @@ -1223,8 +1251,7 @@ function set_tfa($_data) { $yubico_modhex_id = substr($_data["otp_token"], 0, 12); $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username - AND (`authmech` != 'yubi_otp') - OR (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)"); + AND (`authmech` = 'yubi_otp' AND `secret` LIKE :modhex)"); $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id)); $stmt = $pdo->prepare("INSERT INTO `tfa` (`key_id`, `username`, `authmech`, `active`, `secret`) VALUES (:key_id, :username, 'yubi_otp', '1', :secret)"); @@ -1268,9 +1295,6 @@ function set_tfa($_data) { case "webauthn": $key_id = (!isset($_data["key_id"])) ? 'unidentified' : $_data["key_id"]; - $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `authmech` != 'webauthn'"); - $stmt->execute(array(':username' => $username)); - $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `keyHandle`, `publicKey`, `certificate`, `counter`, `active`) VALUES (?, ?, 'webauthn', ?, ?, ?, ?, '1')"); $stmt->execute(array( @@ -1442,25 +1466,27 @@ function unset_tfa_key($_data) { global $pdo; global $lang; $_data_log = $_data; + $access_denied = null; $id = intval($_data['unset_tfa_key']); $username = $_SESSION['mailcow_cc_username']; - if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) { - $_SESSION['return'][] = array( - 'type' => 'danger', - 'log' => array(__FUNCTION__, $_data_log), - 'msg' => 'access_denied' - ); - return false; - } + + // check for empty user and role + if (!isset($_SESSION['mailcow_cc_role']) || empty($username)) $access_denied = true; + try { - if (!is_numeric($id)) { - $_SESSION['return'][] = array( + if (!is_numeric($id)) $access_denied = true; + + // set access_denied error + if ($access_denied){ + $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $_data_log), 'msg' => 'access_denied' ); return false; - } + } + + // check if it's last key $stmt = $pdo->prepare("SELECT COUNT(*) AS `keys` FROM `tfa` WHERE `username` = :username AND `active` = '1'"); $stmt->execute(array(':username' => $username)); @@ -1473,6 +1499,8 @@ function unset_tfa_key($_data) { ); return false; } + + // delete key $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username AND `id` = :id"); $stmt->execute(array(':username' => $username, ':id' => $id)); $_SESSION['return'][] = array( @@ -1490,7 +1518,7 @@ function unset_tfa_key($_data) { return false; } } -function get_tfa($username = null) { +function get_tfa($username = null, $id = null) { global $pdo; if (isset($_SESSION['mailcow_cc_username'])) { $username = $_SESSION['mailcow_cc_username']; @@ -1498,95 +1526,119 @@ function get_tfa($username = null) { elseif (empty($username)) { return false; } - $stmt = $pdo->prepare("SELECT * FROM `tfa` - WHERE `username` = :username AND `active` = '1'"); - $stmt->execute(array(':username' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if (isset($row["authmech"])) { - switch ($row["authmech"]) { - case "yubi_otp": - $data['name'] = "yubi_otp"; - $data['pretty'] = "Yubico OTP"; - $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $data['additional'][] = $row; + if (!isset($id)){ + // fetch all tfa methods - just get information about possible authenticators + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `authmech` FROM `tfa` + WHERE `username` = :username AND `active` = '1'"); + $stmt->execute(array(':username' => $username)); + $results = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // no tfa methods found + if (count($results) == 0) { + $data['name'] = 'none'; + $data['pretty'] = "-"; + $data['additional'] = array(); + return $data; + } + + $data['additional'] = $results; + return $data; + } else { + // fetch specific authenticator details by id + $stmt = $pdo->prepare("SELECT * FROM `tfa` + WHERE `username` = :username AND `id` = :id AND `active` = '1'"); + $stmt->execute(array(':username' => $username, ':id' => $id)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + + if (isset($row["authmech"])) { + switch ($row["authmech"]) { + case "yubi_otp": + $data['name'] = "yubi_otp"; + $data['pretty'] = "Yubico OTP"; + $stmt = $pdo->prepare("SELECT `id`, `key_id`, RIGHT(`secret`, 12) AS 'modhex' FROM `tfa` WHERE `authmech` = 'yubi_otp' AND `username` = :username AND `id` = :id"); + $stmt->execute(array( + ':username' => $username, + ':id' => $id + )); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $data['additional'][] = $row; + } + return $data; + break; + // u2f - deprecated, should be removed + case "u2f": + $data['name'] = "u2f"; + $data['pretty'] = "Fido U2F"; + $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username AND `id` = :id"); + $stmt->execute(array( + ':username' => $username, + ':id' => $id + )); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $data['additional'][] = $row; + } + return $data; + break; + case "hotp": + $data['name'] = "hotp"; + $data['pretty'] = "HMAC-based OTP"; + return $data; + break; + case "totp": + $data['name'] = "totp"; + $data['pretty'] = "Time-based OTP"; + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username AND `id` = :id"); + $stmt->execute(array( + ':username' => $username, + ':id' => $id + )); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $data['additional'][] = $row; + } + return $data; + break; + case "webauthn": + $data['name'] = "webauthn"; + $data['pretty'] = "WebAuthn"; + $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username AND `id` = :id"); + $stmt->execute(array( + ':username' => $username, + ':id' => $id + )); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $data['additional'][] = $row; + } + return $data; + break; + default: + $data['name'] = 'none'; + $data['pretty'] = "-"; + return $data; + break; } - return $data; - break; - // u2f - deprecated, should be removed - case "u2f": - $data['name'] = "u2f"; - $data['pretty'] = "Fido U2F"; - $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $data['additional'][] = $row; - } - return $data; - break; - case "hotp": - $data['name'] = "hotp"; - $data['pretty'] = "HMAC-based OTP"; - return $data; - break; - case "totp": - $data['name'] = "totp"; - $data['pretty'] = "Time-based OTP"; - $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $data['additional'][] = $row; - } - return $data; - break; - case "webauthn": - $data['name'] = "webauthn"; - $data['pretty'] = "WebAuthn"; - $stmt = $pdo->prepare("SELECT `id`, `key_id` FROM `tfa` WHERE `authmech` = 'webauthn' AND `username` = :username"); - $stmt->execute(array( - ':username' => $username, - )); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $data['additional'][] = $row; - } - return $data; - break; - default: + } + else { $data['name'] = 'none'; $data['pretty'] = "-"; return $data; - break; + } } - } - else { - $data['name'] = 'none'; - $data['pretty'] = "-"; - return $data; - } } -function verify_tfa_login($username, $_data, $WebAuthn) { - global $pdo; - global $yubi; - global $u2f; - global $tfa; - $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` - WHERE `username` = :username AND `active` = '1'"); - $stmt->execute(array(':username' => $username)); - $row = $stmt->fetch(PDO::FETCH_ASSOC); +function verify_tfa_login($username, $_data) { + global $pdo; + global $yubi; + global $u2f; + global $tfa; + global $WebAuthn; - switch ($row["authmech"]) { + if ($_data['tfa_method'] != 'u2f'){ + + switch ($_data["tfa_method"]) { case "yubi_otp": if (!ctype_alnum($_data['token']) || strlen($_data['token']) != 44) { $_SESSION['return'][] = array( @@ -1600,7 +1652,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'yubi_otp' - AND `active`='1' + AND `active` = '1' AND `secret` LIKE :modhex"); $stmt->execute(array(':username' => $username, ':modhex' => '%' . $yubico_modhex_id)); $row = $stmt->fetch(PDO::FETCH_ASSOC); @@ -1635,15 +1687,16 @@ function verify_tfa_login($username, $_data, $WebAuthn) { return false; break; case "totp": - try { + try { $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` WHERE `username` = :username AND `authmech` = 'totp' + AND `id` = :id AND `active`='1'"); - $stmt->execute(array(':username' => $username)); + $stmt->execute(array(':username' => $username, ':id' => $_data['id'])); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as $row) { - if ($tfa->verifyCode($row['secret'], $_data['token']) === true) { + if ($tfa->verifyCode($row['secret'], $_data['token']) === true) { $_SESSION['tfa_id'] = $row['id']; $_SESSION['return'][] = array( 'type' => 'success', @@ -1651,7 +1704,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) { 'msg' => 'verified_totp_login' ); return true; - } + } } $_SESSION['return'][] = array( 'type' => 'danger', @@ -1659,23 +1712,16 @@ function verify_tfa_login($username, $_data, $WebAuthn) { 'msg' => 'totp_verification_failed' ); return false; - } - catch (PDOException $e) { + } + catch (PDOException $e) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), 'msg' => array('mysql_error', $e) ); return false; - } + } break; - // u2f - deprecated, should be removed - case "u2f": - // delete old keys that used u2f - $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = :authmech AND `username` = :username"); - $stmt->execute(array(':authmech' => 'u2f', ':username' => $username)); - - return true; case "webauthn": $tokenData = json_decode($_data['token']); $clientDataJSON = base64_decode($tokenData->clientDataJSON); @@ -1684,13 +1730,20 @@ function verify_tfa_login($username, $_data, $WebAuthn) { $id = base64_decode($tokenData->id); $challenge = $_SESSION['challenge']; - $stmt = $pdo->prepare("SELECT `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `keyHandle` = :tokenId"); - $stmt->execute(array(':tokenId' => $tokenData->id)); + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `keyHandle`, `username`, `publicKey` FROM `tfa` WHERE `id` = :id AND `active`='1'"); + $stmt->execute(array(':id' => $_data['id'])); $process_webauthn = $stmt->fetch(PDO::FETCH_ASSOC); - if (empty($process_webauthn) || empty($process_webauthn['publicKey']) || empty($process_webauthn['username'])) return false; + if (empty($process_webauthn)){ + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $username, '*'), + 'msg' => array('webauthn_verification_failed', 'authenticator not found') + ); + return false; + } - if ($process_webauthn['publicKey'] === false) { + if (empty($process_webauthn['publicKey']) || $process_webauthn['publicKey'] === false) { $_SESSION['return'][] = array( 'type' => 'danger', 'log' => array(__FUNCTION__, $username, '*'), @@ -1698,6 +1751,7 @@ function verify_tfa_login($username, $_data, $WebAuthn) { ); return false; } + try { $WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $process_webauthn['publicKey'], $challenge, null, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN'], $GLOBALS['WEBAUTHN_USER_PRESENT_FLAG']); } @@ -1710,26 +1764,31 @@ function verify_tfa_login($username, $_data, $WebAuthn) { return false; } - $stmt = $pdo->prepare("SELECT `superadmin` FROM `admin` WHERE `username` = :username"); $stmt->execute(array(':username' => $process_webauthn['username'])); $obj_props = $stmt->fetch(PDO::FETCH_ASSOC); if ($obj_props['superadmin'] === 1) { - $_SESSION["mailcow_cc_role"] = "admin"; + $_SESSION["mailcow_cc_role"] = "admin"; } elseif ($obj_props['superadmin'] === 0) { - $_SESSION["mailcow_cc_role"] = "domainadmin"; + $_SESSION["mailcow_cc_role"] = "domainadmin"; } else { - $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username"); - $stmt->execute(array(':username' => $process_webauthn['username'])); - $row = $stmt->fetch(PDO::FETCH_ASSOC); - if ($row['username'] == $process_webauthn['username']) { + $stmt = $pdo->prepare("SELECT `username` FROM `mailbox` WHERE `username` = :username"); + $stmt->execute(array(':username' => $process_webauthn['username'])); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (!empty($row['username'])) { $_SESSION["mailcow_cc_role"] = "user"; - } + } else { + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $username, '*'), + 'msg' => array('webauthn_verification_failed', 'could not determine user role') + ); + return false; + } } - if ($process_webauthn['username'] != $_SESSION['pending_mailcow_cc_username']){ $_SESSION['return'][] = array( 'type' => 'danger', @@ -1739,9 +1798,8 @@ function verify_tfa_login($username, $_data, $WebAuthn) { return false; } - $_SESSION["mailcow_cc_username"] = $process_webauthn['username']; - $_SESSION['tfa_id'] = $process_webauthn['key_id']; + $_SESSION['tfa_id'] = $process_webauthn['id']; $_SESSION['authReq'] = null; unset($_SESSION["challenge"]); $_SESSION['return'][] = array( @@ -1762,6 +1820,17 @@ function verify_tfa_login($username, $_data, $WebAuthn) { } return false; + } else { + // delete old keys that used u2f + $stmt = $pdo->prepare("SELECT * FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username"); + $stmt->execute(array(':username' => $username)); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + if (count($rows) == 0) return false; + + $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `authmech` = 'u2f' AND `username` = :username"); + $stmt->execute(array(':username' => $username)); + return true; + } } function admin_api($access, $action, $data = null) { global $pdo; diff --git a/data/web/inc/functions.mailbox.inc.php b/data/web/inc/functions.mailbox.inc.php index 7f8ff3ac..2cf9f6c6 100644 --- a/data/web/inc/functions.mailbox.inc.php +++ b/data/web/inc/functions.mailbox.inc.php @@ -336,9 +336,37 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { $mins_interval = $_data['mins_interval']; $enc1 = $_data['enc1']; $custom_params = (empty(trim($_data['custom_params']))) ? '' : trim($_data['custom_params']); - // Workaround, fixme - if (stripos($custom_params, 'pipemess') || stripos($custom_params, 'pipemes')) { - $custom_params = ''; + + // validate custom params + foreach (explode('-', $custom_params) as $param){ + if(empty($param)) continue; + + // extract option + if (str_contains($param, '=')) $param = explode('=', $param)[0]; + else $param = rtrim($param, ' '); + // remove first char if first char is - + if ($param[0] == '-') $param = ltrim($param, $param[0]); + + if (str_contains($param, ' ')) { + // bad char + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'bad character SPACE' + ); + return false; + } + + // check if param is whitelisted + if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){ + // bad option + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'bad option '. $param + ); + return false; + } } if (empty($subfolder2)) { $subfolder2 = ""; @@ -1764,8 +1792,37 @@ function mailbox($_action, $_type, $_data = null, $_extra = null) { ); continue; } - if (stripos($custom_params, 'pipemess') || stripos($custom_params, 'pipemes')) { - $custom_params = ''; + + // validate custom params + foreach (explode('-', $custom_params) as $param){ + if(empty($param)) continue; + + // extract option + if (str_contains($param, '=')) $param = explode('=', $param)[0]; + else $param = rtrim($param, ' '); + // remove first char if first char is - + if ($param[0] == '-') $param = ltrim($param, $param[0]); + + if (str_contains($param, ' ')) { + // bad char + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'bad character SPACE' + ); + return false; + } + + // check if param is whitelisted + if (!in_array(strtolower($param), $GLOBALS["IMAPSYNC_OPTIONS"]["whitelist"])){ + // bad option + $_SESSION['return'][] = array( + 'type' => 'danger', + 'log' => array(__FUNCTION__, $_action, $_type, $_data_log, $_attr), + 'msg' => 'bad option '. $param + ); + return false; + } } if (empty($subfolder2)) { $subfolder2 = ""; diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php index 88be5bca..b47bd5c2 100644 --- a/data/web/inc/init_db.inc.php +++ b/data/web/inc/init_db.inc.php @@ -3,7 +3,7 @@ function init_db_schema() { try { global $pdo; - $db_version = "20052022_0938"; + $db_version = "25072022_2300"; $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); @@ -440,7 +440,7 @@ function init_db_schema() { "spam_score" => "TINYINT(1) NOT NULL DEFAULT '1'", "spam_policy" => "TINYINT(1) NOT NULL DEFAULT '1'", "delimiter_action" => "TINYINT(1) NOT NULL DEFAULT '1'", - "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '1'", + "syncjobs" => "TINYINT(1) NOT NULL DEFAULT '0'", "eas_reset" => "TINYINT(1) NOT NULL DEFAULT '1'", "sogo_profile_reset" => "TINYINT(1) NOT NULL DEFAULT '0'", "pushover" => "TINYINT(1) NOT NULL DEFAULT '1'", @@ -738,8 +738,8 @@ function init_db_schema() { "username" => "VARCHAR(255) NOT NULL", "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')", "secret" => "VARCHAR(255) DEFAULT NULL", - "keyHandle" => "VARCHAR(255) DEFAULT NULL", - "publicKey" => "VARCHAR(255) DEFAULT NULL", + "keyHandle" => "VARCHAR(1023) DEFAULT NULL", + "publicKey" => "VARCHAR(4096) DEFAULT NULL", "counter" => "INT NOT NULL DEFAULT '0'", "certificate" => "TEXT", "active" => "TINYINT(1) NOT NULL DEFAULT '0'" @@ -1227,8 +1227,16 @@ function init_db_schema() { $pdo->query($create); } - // Mitigate imapsync pipemess issue - $pdo->query("UPDATE `imapsync` SET `custom_params` = '' WHERE `custom_params` LIKE '%pipemess%' OR `custom_params` LIKE '%pipemes%';"); + // Mitigate imapsync argument injection issue + $pdo->query("UPDATE `imapsync` SET `custom_params` = '' + WHERE `custom_params` LIKE '%pipemess%' + OR custom_params LIKE '%skipmess%' + OR custom_params LIKE '%delete2foldersonly%' + OR custom_params LIKE '%delete2foldersbutnot%' + OR custom_params LIKE '%regexflag%' + OR custom_params LIKE '%pipemess%' + OR custom_params LIKE '%regextrans2%' + OR custom_params LIKE '%maxlinelengthcmd%';"); // Migrate webauthn tfa $stmt = $pdo->query("ALTER TABLE `tfa` MODIFY COLUMN `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp', 'webauthn')"); diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 3d13c5f7..36f108ad 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -51,8 +51,9 @@ $qrprovider = new RobThree\Auth\Providers\Qr\QRServerProvider(); $tfa = new RobThree\Auth\TwoFactorAuth($OTP_LABEL, 6, 30, 'sha1', $qrprovider); // FIDO2 +$server_name = parse_url('https://' . $_SERVER['HTTP_HOST'], PHP_URL_HOST); $formats = $GLOBALS['FIDO2_FORMATS']; -$WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $_SERVER['HTTP_HOST'], $formats); +$WebAuthn = new lbuchs\WebAuthn\WebAuthn('WebAuthn Library', $server_name, $formats); // only include root ca's when needed if (getenv('WEBAUTHN_ONLY_TRUSTED_VENDORS') == 'y') $WebAuthn->addRootCertificates($_SERVER['DOCUMENT_ROOT'] . '/inc/lib/WebAuthn/rootCertificates'); diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index cb3a3771..aec043e9 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -1,24 +1,24 @@ 'monitoring_nolog.map' ) ); + + +$IMAPSYNC_OPTIONS = array( + 'whitelist' => array( + 'authmech1', + 'authmech2', + 'authuser1', + 'authuser2', + 'debugcontent', + 'disarmreadreceipts', + 'logdir', + 'debugcrossduplicates', + 'maxsize', + 'minsize', + 'minage', + 'search', + 'noabletosearch', + 'pidfile', + 'pidfilelocking', + 'search1', + 'search2', + 'sslargs1', + 'sslargs2', + 'syncduplicates', + 'usecache', + 'synclabels', + 'truncmess', + 'domino2', + 'expunge1', + 'filterbuggyflags', + 'justconnect', + 'justfolders', + 'maxlinelength', + 'useheader', + 'noabletosearch1', + 'nolog', + 'prefix1', + 'prefix2', + 'sep1', + 'sep2', + 'nofoldersizesatend', + 'justfoldersizes', + 'proxyauth1', + 'skipemptyfolders', + 'include', + 'subfolder1', + 'subscribed', + 'subscribe', + 'debug', + 'debugimap2', + 'domino1', + 'exchange1', + 'exchange2', + 'justlogin', + 'keepalive1', + 'keepalive2', + 'noabletosearch2', + 'noexpunge2', + 'noresyncflags', + 'nossl1', + 'nouidexpunge2', + 'syncinternaldates', + 'idatefromheader', + 'useuid', + 'debugflags', + 'debugimap', + 'delete1emptyfolders', + 'delete2folders', + 'gmail2', + 'office1', + 'testslive6', + 'debugimap1', + 'errorsmax', + 'tests', + 'gmail1', + 'maxmessagespersecond', + 'maxbytesafter', + 'maxsleep', + 'abort', + 'resyncflags', + 'resynclabels', + 'syncacls', + 'nosyncacls', + 'nousecache', + 'office2', + 'testslive', + 'debugmemory', + 'exitwhenover', + 'noid', + 'noexpunge1', + 'authmd51', + 'logfile', + 'proxyauth2', + 'domain1', + 'domain2', + 'oauthaccesstoken1', + 'oauthaccesstoken2', + 'oauthdirect1', + 'oauthdirect2', + 'folder', + 'folderrec', + 'folderfirst', + 'folderlast', + 'nomixfolders', + 'authmd52', + 'debugfolders', + 'nossl2', + 'ssl2', + 'tls2', + 'notls2', + 'debugssl', + 'notls1', + 'inet4', + 'inet6', + 'log', + 'showpasswords' + ), + 'blacklist' => array( + 'skipmess', + 'delete2foldersonly', + 'delete2foldersbutnot', + 'regexflag', + 'regexmess', + 'pipemess', + 'regextrans2', + 'maxlinelengthcmd' + ) +); diff --git a/data/web/json_api.php b/data/web/json_api.php index f8c88d33..15bc8b5e 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -178,15 +178,22 @@ if (isset($_GET['query'])) { // parse post data $post = trim(file_get_contents('php://input')); if ($post) $post = json_decode($post); - - // decode base64 strings - $clientDataJSON = base64_decode($post->clientDataJSON); - $attestationObject = base64_decode($post->attestationObject); // process registration data from authenticator try { + // decode base64 strings + $clientDataJSON = base64_decode($post->clientDataJSON); + $attestationObject = base64_decode($post->attestationObject); + // processCreate($clientDataJSON, $attestationObject, $challenge, $requireUserVerification=false, $requireUserPresent=true, $failIfRootMismatch=true) $data = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $_SESSION['challenge'], false, true); + + // safe authenticator in mysql `tfa` table + $_data['tfa_method'] = $post->tfa_method; + $_data['key_id'] = $post->key_id; + $_data['confirm_password'] = $post->confirm_password; + $_data['registration'] = $data; + set_tfa($_data); } catch (Throwable $ex) { // err @@ -197,11 +204,6 @@ if (isset($_GET['query'])) { exit; } - // safe authenticator in mysql `tfa` table - $_data['tfa_method'] = $post->tfa_method; - $_data['key_id'] = $post->key_id; - $_data['registration'] = $data; - set_tfa($_data); // send response $return = new stdClass(); @@ -419,7 +421,7 @@ if (isset($_GET['query'])) { // } $ids = NULL; - $getArgs = $WebAuthn->getGetArgs($ids, 30, true, true, true, true, $GLOBALS['FIDO2_UV_FLAG_LOGIN']); + $getArgs = $WebAuthn->getGetArgs($ids, 30, false, false, false, false, $GLOBALS['FIDO2_UV_FLAG_LOGIN']); print(json_encode($getArgs)); $_SESSION['challenge'] = $WebAuthn->getChallenge(); return; @@ -428,8 +430,11 @@ if (isset($_GET['query'])) { case "webauthn-tfa-registration": if (isset($_SESSION["mailcow_cc_role"])) { // Exclude existing CredentialIds, if any - $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username"); - $stmt->execute(array(':username' => $_SESSION['mailcow_cc_username'])); + $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech"); + $stmt->execute(array( + ':username' => $_SESSION['mailcow_cc_username'], + ':authmech' => 'webauthn' + )); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); while($row = array_shift($rows)) { $excludeCredentialIds[] = base64_decode($row['keyHandle']); @@ -450,20 +455,24 @@ if (isset($_GET['query'])) { } break; case "webauthn-tfa-get-args": - $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username"); - $stmt->execute(array(':username' => $_SESSION['pending_mailcow_cc_username'])); + $stmt = $pdo->prepare("SELECT `keyHandle` FROM `tfa` WHERE username = :username AND authmech = :authmech"); + $stmt->execute(array( + ':username' => $_SESSION['pending_mailcow_cc_username'], + ':authmech' => 'webauthn' + )); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - while($row = array_shift($rows)) { - $cids[] = base64_decode($row['keyHandle']); - } - if (count($cids) == 0) { + if (count($rows) == 0) { print(json_encode(array( 'type' => 'error', 'msg' => 'Cannot find matching credentialIds' ))); + exit; + } + while($row = array_shift($rows)) { + $cids[] = base64_decode($row['keyHandle']); } - $getArgs = $WebAuthn->getGetArgs($cids, 30, true, true, true, true, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']); + $getArgs = $WebAuthn->getGetArgs($cids, 30, false, false, false, false, $GLOBALS['WEBAUTHN_UV_FLAG_LOGIN']); $getArgs->publicKey->extensions = array('appid' => "https://".$getArgs->publicKey->rpId); print(json_encode($getArgs)); $_SESSION['challenge'] = $WebAuthn->getChallenge(); diff --git a/data/web/lang/lang.es.json b/data/web/lang/lang.es.json index 94345220..b2cfd783 100644 --- a/data/web/lang/lang.es.json +++ b/data/web/lang/lang.es.json @@ -19,7 +19,8 @@ "syncjobs": "Trabajos de sincronización", "tls_policy": "Póliza de TLS", "unlimited_quota": "Cuota ilimitada para buzones", - "app_passwds": "Gestionar las contraseñas de aplicaciones" + "app_passwds": "Gestionar las contraseñas de aplicaciones", + "domain_desc": "Cambiar descripción del dominio" }, "add": { "activate_filter_warn": "Todos los demás filtros se desactivarán cuando este filtro se active.", diff --git a/data/web/lang/lang.ru.json b/data/web/lang/lang.ru.json index e6cfacba..f4668bbe 100644 --- a/data/web/lang/lang.ru.json +++ b/data/web/lang/lang.ru.json @@ -988,7 +988,7 @@ "enter_qr_code": "Ваш код TOTP, если устройство не может отсканировать QR-код", "error_code": "Код ошибки", "init_webauthn": "Инициализация, пожалуйста, подождите...", - "key_id": "Идентификатор YubiKey ключа", + "key_id": "Идентификатор вашего устройства", "key_id_totp": "Идентификатор TOTP ключа", "none": "Отключить", "reload_retry": "- (перезагрузить страницу браузера или почистите кеш/cookies, если ошибка повторяется)", @@ -1002,7 +1002,8 @@ "webauthn": "WebAuthn аутентификация", "waiting_usb_auth": "Ожидание устройства USB...

Пожалуйста, нажмите кнопку на USB устройстве сейчас.", "waiting_usb_register": "Ожидание устройства USB...

Пожалуйста, введите пароль выше и подтвердите регистрацию, нажав кнопку на USB устройстве.", - "yubi_otp": "Yubico OTP аутентификация" + "yubi_otp": "Yubico OTP аутентификация", + "u2f_deprecated": "Похоже, что ваш ключ был зарегистрирован с использованием устаревшего метода U2F. Мы деактивируем для вас двухфакторную аутентификацию и удалим ваш ключ." }, "user": { "action": "Действия", diff --git a/data/web/lang/lang.uk.json b/data/web/lang/lang.uk.json index 1004a7f9..a327ef46 100644 --- a/data/web/lang/lang.uk.json +++ b/data/web/lang/lang.uk.json @@ -980,7 +980,8 @@ "resource_modified": "Зміни поштового акаунту %s збережено", "settings_map_added": "Правило додано", "tls_policy_map_entry_deleted": "Політику TLS ID %s видалено", - "verified_totp_login": "Авторизацію TOTP пройдено" + "verified_totp_login": "Авторизацію TOTP пройдено", + "domain_add_dkim_available": "Ключ DKIM вже існує" }, "tfa": { "confirm": "Підтвердьте", diff --git a/data/web/templates/base.twig b/data/web/templates/base.twig index c39ceda4..3a34f6ba 100644 --- a/data/web/templates/base.twig +++ b/data/web/templates/base.twig @@ -208,9 +208,69 @@ function recursiveBase64StrToArrayBuffer(obj) { keyboard: false }).show(); + + // validate Time based OTP tfa + $("#pending_tfa_tab_totp").click(function(){ + $(".webauthn-authenticator-selection").removeClass("active"); + $("#collapseWebAuthnTFA").collapse('hide'); + + // select default if only one authenticator exists + if ($('.totp-authenticator-selection').length == 1){ + $('.totp-authenticator-selection').addClass("active"); + var id = $('.totp-authenticator-selection').children('input').first().val(); + $("#totp_selected_id").val(id); + $("#collapseTotpTFA").collapse('show'); + } + }); + $(".totp-authenticator-selection").click(function(){ + $(".totp-authenticator-selection").removeClass("active"); + $(this).addClass("active"); + + var id = $(this).children('input').first().val(); + $("#totp_selected_id").val(id); + + $("#collapseTotpTFA").collapse('show'); + }); + if ($('.totp-authenticator-selection').length == 1 && + $('#pending_tfa_tab_yubi_otp').length == 0 && + $('.webauthn-authenticator-selection').length == 0){ + + // select default if only one authenticator exists + $('.totp-authenticator-selection').addClass("active"); + + var id = $('.totp-authenticator-selection').children('input').first().val(); + $("#totp_selected_id").val(id); + + $("#collapseTotpTFA").collapse('show'); + setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 1000); + } + $('#pending_tfa_tab_totp').on('shown.bs.tab', function() { + // autofocus + setTimeout(function() { $("#collapseTotpTFA").find('input[name="token"]').focus(); }, 200); + }); + // validate Yubi OTP tfa + if ($('.webauthn-authenticator-selection').length == 0){ + // autofocus + setTimeout(function() { $("#collapseYubiTFA").find('input[name="token"]').focus(); }, 1000); + } + $('#pending_tfa_tab_yubi_otp').on('shown.bs.tab', function() { + // autofocus + $("#collapseYubiTFA").find('input[name="token"]').focus(); + }); // validate WebAuthn tfa - $('#start_webauthn_confirmation').click(function(){ - $('#webauthn_status_auth').html('

' + lang_tfa.init_webauthn + '

'); + $("#pending_tfa_tab_webauthn").click(function(){ + $(".totp-authenticator-selection").removeClass("active"); + + $("#collapseTotpTFA").collapse('hide'); + }); + $(".webauthn-authenticator-selection").click(function(){ + $(".webauthn-authenticator-selection").removeClass("active"); + $(this).addClass("active"); + + var id = $(this).children('input').first().val(); + $("#webauthn_selected_id").val(id); + + $("#collapseWebAuthnTFA").collapse('show'); $(this).find('input[name=token]').focus(); if(document.getElementById("webauthn_auth_data") !== null) { @@ -224,30 +284,32 @@ function recursiveBase64StrToArrayBuffer(obj) { window.fetch("/api/v1/get/webauthn-tfa-get-args", {method:'GET',cache:'no-cache'}).then(response => { return response.json(); }).then(json => { - if (json.success === false) throw new Error(); + console.log(json); + if (json.success === false) throw new Error(); + if (json.type === "error") throw new Error(json.msg); - recursiveBase64StrToArrayBuffer(json); - return json; + recursiveBase64StrToArrayBuffer(json); + return json; }).then(getCredentialArgs => { - // get credentials - return navigator.credentials.get(getCredentialArgs); + // get credentials + return navigator.credentials.get(getCredentialArgs); }).then(cred => { - return { - id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, - clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, - authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, - signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null - }; + return { + id: cred.rawId ? arrayBufferToBase64(cred.rawId) : null, + clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, + authenticatorData: cred.response.authenticatorData ? arrayBufferToBase64(cred.response.authenticatorData) : null, + signature : cred.response.signature ? arrayBufferToBase64(cred.response.signature) : null + }; }).then(JSON.stringify).then(function(AuthenticatorAttestationResponse) { - // send request by submit - var form = document.getElementById('webauthn_auth_form'); - var auth = document.getElementById('webauthn_auth_data'); - auth.value = AuthenticatorAttestationResponse; - form.submit(); + // send request by submit + var form = document.getElementById('webauthn_auth_form'); + var auth = document.getElementById('webauthn_auth_data'); + auth.value = AuthenticatorAttestationResponse; + form.submit(); }).catch(function(err) { - var webauthn_return_code = document.getElementById('webauthn_return_code'); - webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null; - webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; + var webauthn_return_code = document.getElementById('webauthn_return_code'); + webauthn_return_code.style.display = webauthn_return_code.style.display === 'none' ? '' : null; + webauthn_return_code.innerHTML = lang_tfa.error_code + ': ' + err + ' ' + lang_tfa.reload_retry; }); } }); @@ -263,7 +325,9 @@ function recursiveBase64StrToArrayBuffer(obj) { } }); }); - {% endif %} + {% endif %} + + // Validate FIDO2 $("#fido2-login").click(function(){ $('#fido2-alerts').html(); @@ -384,11 +448,13 @@ function recursiveBase64StrToArrayBuffer(obj) { $("#start_webauthn_register").click(() => { var key_id = document.getElementsByName('key_id')[1].value; + var confirm_password = document.getElementsByName('confirm_password')[1].value; // fetch WebAuthn create args window.fetch("/api/v1/get/webauthn-tfa-registration/{{ mailcow_cc_username|url_encode(true)|default('null') }}", {method:'GET',cache:'no-cache'}).then(response => { return response.json(); }).then(json => { + console.log(json); if (json.success === false) throw new Error(json.msg); recursiveBase64StrToArrayBuffer(json); @@ -401,7 +467,8 @@ function recursiveBase64StrToArrayBuffer(obj) { clientDataJSON: cred.response.clientDataJSON ? arrayBufferToBase64(cred.response.clientDataJSON) : null, attestationObject: cred.response.attestationObject ? arrayBufferToBase64(cred.response.attestationObject) : null, key_id: key_id, - tfa_method: "webauthn" + tfa_method: "webauthn", + confirm_password: confirm_password }; }).then(JSON.stringify).then(AuthenticatorAttestationResponse => { // send request @@ -449,13 +516,20 @@ function recursiveBase64StrToArrayBuffer(obj) { {% if ui_texts.ui_footer %}
{{ ui_texts.ui_footer|rot13|raw }} {% endif %} - {% if mailcow_cc_username and mailcow_info.version_tag|default %} + {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "master" and mailcow_info.version_tag|default %} 🐮 + 🐋 = 💕 - - Version: {{ mailcow_info.version_tag }} + Version: {{ mailcow_info.version_tag }} + {% endif %} + {% if mailcow_cc_username and mailcow_info.mailcow_branch|lower == "nightly" and mailcow_info.version_tag|default %} + + 🛠️🐮 + 🐋 = 💕 + Nightly: {{ mailcow_info.version_tag }} +
+ Build: {{ mailcow_info.git_commit_date }} +
{% endif %} diff --git a/data/web/templates/domainadmin.twig b/data/web/templates/domainadmin.twig index 5ce28410..56f5e75f 100644 --- a/data/web/templates/domainadmin.twig +++ b/data/web/templates/domainadmin.twig @@ -28,7 +28,7 @@
diff --git a/data/web/templates/modals/footer.twig b/data/web/templates/modals/footer.twig index 8bb0889e..87e36514 100644 --- a/data/web/templates/modals/footer.twig +++ b/data/web/templates/modals/footer.twig @@ -131,37 +131,37 @@
{% endif %} -{% if pending_tfa_method %} +{% if pending_tfa_methods %}