diff --git a/README.md b/README.md --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ * Make sure you have docker and docker-compose available. * Change to the base directory of this repository. * Run 'HOST=kolab.local ADMIN_PASSWORD="simple123" bin/configure.sh config.prod' to configure this deployment. -* Run 'bin/deploy.sh' to start the deployment. -* Run 'docker exec -w /src/kolabsrc/ kolab-webapp ./artisan user:password admin@kolab.local simple123' to set your admin password +* Run 'env ADMIN_PASSWORD="simple123" bin/deploy.sh' to start the deployment. * Add an /etc/hosts entry "127.0.0.1 kolab.local" * navigate to https://kolab.local * login as "admin@kolab.local" with password "simple123" (or whatever you have set), and create your users. diff --git a/ansible/setup.yml b/ansible/setup.yml --- a/ansible/setup.yml +++ b/ansible/setup.yml @@ -58,6 +58,7 @@ FIREBASE_API_KEY: "{{ firebase_api_key }}" PUBLIC_IP: "{{ public_ip }}" ADMIN_PASSWORD: "{{ admin_password }}" + KOLAB_GIT_REF: "{{ git_branch }}" - name: Permit receiving mail @@ -122,15 +123,10 @@ ansible.builtin.command: bin/deploy.sh args: chdir: /home/kolab/kolab + environment: + ADMIN_PASSWORD: "{{ admin_password }}" register: result always: - name: Print output from previous task with newlines ansible.builtin.debug: msg="{{result.stdout_lines}}" - - - name: Set admin password - become: true - become_user: kolab - ansible.builtin.command: docker exec -w /src/kolabsrc/ kolab-webapp ./artisan user:password admin@{{ hostname }} {{ admin_password|quote }} - args: - chdir: /home/kolab/kolab diff --git a/bin/configure.sh b/bin/configure.sh --- a/bin/configure.sh +++ b/bin/configure.sh @@ -78,6 +78,10 @@ echo "PASSPORT_PUBLIC_KEY=\"${PASSPORT_PUBLIC_KEY}\"" >> src/.env fi +if ! grep -q "KOLAB_GIT_REF=" .env; then + echo "KOLAB_GIT_REF=${KOLAB_GIT_REF:-master}" >> src/.env +fi + # Customize configuration sed -i \ -e "s/{{ host }}/${HOST:-kolab.local}/g" \ diff --git a/bin/deploy.sh b/bin/deploy.sh --- a/bin/deploy.sh +++ b/bin/deploy.sh @@ -1,2 +1,6 @@ #!/bin/bash bin/quickstart.sh --nodev +if [[ -n $ADMIN_PASSWORD ]]; then + DOMAIN=$(grep APP_DOMAIN .env | tail -n1 | sed "s/APP_DOMAIN=//") + docker exec -w /src/kolabsrc/ kolab-webapp ./artisan user:password "admin@$DOMAIN" "$ADMIN_PASSWORD" +fi diff --git a/bin/quickstart.sh b/bin/quickstart.sh --- a/bin/quickstart.sh +++ b/bin/quickstart.sh @@ -20,7 +20,7 @@ export DOCKER_BUILDKIT=0 -docker-compose down --remove-orphans +docker-compose down -t 1 --remove-orphans docker volume rm kolab_mariadb || : docker volume rm kolab_imap || : docker volume rm kolab_ldap || : @@ -33,8 +33,8 @@ pkill -9 -f swoole || : bin/regen-certs -docker-compose build coturn kolab mariadb meet pdns proxy redis haproxy roundcube -docker-compose up -d coturn kolab mariadb meet pdns redis roundcube +docker-compose build coturn ldap kolab mariadb meet pdns proxy redis haproxy roundcube +docker-compose up -d coturn ldap kolab mariadb meet pdns redis roundcube # Workaround until we have docker-compose --wait (https://github.com/docker/compose/pull/8777) function wait_for_container { diff --git a/bin/regen-certs b/bin/regen-certs --- a/bin/regen-certs +++ b/bin/regen-certs @@ -30,7 +30,7 @@ exit 1 fi -export $(cat ${base_dir}/src/.env | xargs) >/dev/null 2>&1 +APP_DOMAIN=$(grep APP_DOMAIN .env | tail -n1 | sed "s/APP_DOMAIN=//") for name in kolab.mgmt.com kolab.hosted.com imap.hosted.com {{admin,meet}.,}${APP_DOMAIN}; do openssl genrsa -out ${cert_dir}/${name}.key 4096 diff --git a/ci/Makefile b/ci/Makefile --- a/ci/Makefile +++ b/ci/Makefile @@ -13,7 +13,7 @@ lint: docker kill kolab-tests || : ; \ - kolab rm kolab-tests || : ; \ + docker rm kolab-tests || : ; \ docker run -v ${PWD}/../:/src/kolab.orig -t kolab-tests /lint.sh test: diff --git a/config.demo/src/.env b/config.demo/src/.env --- a/config.demo/src/.env +++ b/config.demo/src/.env @@ -18,7 +18,7 @@ APP_WITH_FILES=1 APP_LDAP=1 -APP_IMAP=0 +APP_IMAP=1 APP_HEADER_CSP="connect-src 'self'; child-src 'self'; font-src 'self'; form-action 'self' data:; frame-ancestors 'self'; img-src blob: data: 'self' *; media-src 'self'; object-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; default-src 'self';" APP_HEADER_XFO=sameorigin @@ -69,7 +69,7 @@ LDAP_BASE_DN="dc=mgmt,dc=com" LDAP_DOMAIN_BASE_DN="ou=Domains,dc=mgmt,dc=com" -LDAP_HOSTS=kolab +LDAP_HOSTS=ldap LDAP_PORT=389 LDAP_SERVICE_BIND_DN="uid=kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_SERVICE_BIND_PW="Welcome2KolabSystems" diff --git a/config.dev/docker-compose.override.yml b/config.dev/docker-compose.override.yml --- a/config.dev/docker-compose.override.yml +++ b/config.dev/docker-compose.override.yml @@ -2,7 +2,6 @@ services: kolab: ports: - - "389:389" - "8880:8880" - "8443:8443" - "10143:10143" @@ -13,6 +12,9 @@ mariadb: ports: - "3306:3306" + ldap: + ports: + - "389:389" redis: ports: - "6379:6379" diff --git a/config.prod/src/.env b/config.prod/src/.env --- a/config.prod/src/.env +++ b/config.prod/src/.env @@ -68,7 +68,7 @@ LDAP_BASE_DN="dc=mgmt,dc=com" LDAP_DOMAIN_BASE_DN="ou=Domains,dc=mgmt,dc=com" -LDAP_HOSTS=kolab +LDAP_HOSTS=ldap LDAP_PORT=389 LDAP_SERVICE_BIND_DN="uid=kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_SERVICE_BIND_PW="{{ admin_password }}" diff --git a/docker-compose.build.yml b/docker-compose.build.yml --- a/docker-compose.build.yml +++ b/docker-compose.build.yml @@ -4,7 +4,7 @@ build: context: ./docker/swoole/ container_name: kolab-swoole - image: apheleia/swoole:4.8.x + image: apheleia/swoole tests: build: context: ./docker/tests/ diff --git a/docker-compose.yml b/docker-compose.yml --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,24 +24,32 @@ DB_KOLAB_DATABASE: kolab DB_KOLAB_USERNAME: kolab DB_KOLAB_PASSWORD: ${DB_PASSWORD:?"DB_PASSWORD is missing"} + LDAP_HOST: ldap + LDAP_ADMIN_BIND_DN: ${LDAP_ADMIN_BIND_DN} + LDAP_ADMIN_BIND_PW: ${LDAP_ADMIN_BIND_PW} + LDAP_SERVICE_BIND_PW: ${LDAP_SERVICE_BIND_PW} + IMAP_ADMIN_LOGIN: ${IMAP_ADMIN_LOGIN} + IMAP_ADMIN_PASSWORD: ${IMAP_ADMIN_PASSWORD} container_name: kolab privileged: true restart: on-failure + tty: true depends_on: mariadb: condition: service_healthy pdns: condition: service_healthy + ldap: + condition: service_healthy extra_hosts: - "kolab.mgmt.com:127.0.0.1" - "services.${APP_DOMAIN}:172.18.0.4" environment: - APP_DOMAIN=${APP_DOMAIN} - - LDAP_HOST=127.0.0.1 + - LDAP_HOST=ldap - LDAP_ADMIN_BIND_DN=${LDAP_ADMIN_BIND_DN} - LDAP_ADMIN_BIND_PW=${LDAP_ADMIN_BIND_PW} - LDAP_SERVICE_BIND_PW=${LDAP_SERVICE_BIND_PW} - - LDAP_HOSTED_BIND_PW=${LDAP_HOSTED_BIND_PW} - DB_HOST=mariadb - DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} - DB_HKCCP_DATABASE=${DB_DATABASE} @@ -91,11 +99,45 @@ - ./docker/kolab/utils:/root/utils:ro - /sys/fs/cgroup:/sys/fs/cgroup:ro - imap:/imapdata + + ldap: + build: + context: ./docker/ldap/ + container_name: kolab-ldap + restart: on-failure + tty: true + hostname: ldap + privileged: true + environment: + - APP_DOMAIN=${APP_DOMAIN} + - LDAP_ADMIN_ROOT_DN=${LDAP_ADMIN_ROOT_DN} + - LDAP_ADMIN_BIND_DN=${LDAP_ADMIN_BIND_DN} + - LDAP_ADMIN_BIND_PW=${LDAP_ADMIN_BIND_PW} + - LDAP_SERVICE_BIND_PW=${LDAP_SERVICE_BIND_PW} + - LDAP_HOSTED_BIND_PW=${LDAP_HOSTED_BIND_PW} + - IMAP_ADMIN_PASSWORD=${IMAP_ADMIN_PASSWORD} + healthcheck: + interval: 10s + test: "systemctl status dirsrv@kolab || exit 1" + timeout: 5s + retries: 30 + start_period: 5m + image: kolab-ldap + networks: + kolab: + ipv4_address: 172.18.0.12 + tmpfs: + - /run + - /tmp + - /var/run + - /var/tmp + volumes: + - /sys/fs/cgroup:/sys/fs/cgroup:ro - ldap:/ldapdata roundcube: build: context: ./docker/roundcube/ - container_name: roundcube + container_name: kolab-roundcube hostname: roundcube.hosted.com restart: on-failure depends_on: @@ -107,7 +149,7 @@ condition: service_healthy environment: - APP_DOMAIN=${APP_DOMAIN} - - LDAP_HOST=kolab + - LDAP_HOST=ldap - LDAP_ADMIN_BIND_DN=${LDAP_ADMIN_BIND_DN} - LDAP_ADMIN_BIND_PW=${LDAP_ADMIN_BIND_PW} - LDAP_SERVICE_BIND_PW=${LDAP_SERVICE_BIND_PW} @@ -198,6 +240,7 @@ DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD} container_name: kolab-pdns restart: on-failure + tty: true hostname: pdns depends_on: mariadb: @@ -237,6 +280,8 @@ webapp: build: context: ./docker/webapp/ + args: + GIT_REF: ${KOLAB_GIT_REF:-master} container_name: kolab-webapp restart: on-failure image: kolab-webapp @@ -263,6 +308,8 @@ meet: build: context: ./docker/meet/ + args: + GIT_REF: ${KOLAB_GIT_REF:-master} container_name: kolab-meet restart: on-failure healthcheck: diff --git a/docker/kolab/Dockerfile b/docker/kolab/Dockerfile --- a/docker/kolab/Dockerfile +++ b/docker/kolab/Dockerfile @@ -54,10 +54,22 @@ ARG DB_KOLAB_DATABASE ARG DB_KOLAB_USERNAME ARG DB_KOLAB_PASSWORD +ARG LDAP_HOST +ARG LDAP_ADMIN_BIND_DN +ARG LDAP_ADMIN_BIND_PW +ARG LDAP_SERVICE_BIND_PW +ARG IMAP_ADMIN_LOGIN +ARG IMAP_ADMIN_PASSWORD RUN sed -i -r \ -e "s|DB_KOLAB_DATABASE|$DB_KOLAB_DATABASE|g" \ -e "s|DB_KOLAB_USERNAME|$DB_KOLAB_USERNAME|g" \ -e "s|DB_KOLAB_PASSWORD|$DB_KOLAB_PASSWORD|g" \ + -e "s|LDAP_HOST|$LDAP_HOST|g" \ + -e "s|LDAP_ADMIN_BIND_DN|$LDAP_ADMIN_BIND_DN|g" \ + -e "s|LDAP_ADMIN_BIND_PW|$LDAP_ADMIN_BIND_PW|g" \ + -e "s|LDAP_SERVICE_BIND_PW|$LDAP_SERVICE_BIND_PW|g" \ + -e "s|IMAP_ADMIN_LOGIN|$IMAP_ADMIN_LOGIN|g" \ + -e "s|IMAP_ADMIN_PASSWORD|$IMAP_ADMIN_PASSWORD|g" \ /etc/kolab/kolab.conf RUN mkdir -p /imapdata/{spool,lib} && \ @@ -66,15 +78,8 @@ chmod -R 777 /imapdata && \ chown cyrus:mail /var/spool/imap /var/lib/imap -RUN mkdir -p /ldapdata/{config,ssca,run} /var/run/dirsrv && \ - ln -s /ldapdata/config /etc/dirsrv/slapd-kolab && \ - ln -s /ldapdata/ssca /etc/dirsrv/ssca && \ - ln -s /ldapdata/run /var/run/dirsrv && \ - chmod -R 777 /ldapdata /etc/dirsrv - VOLUME [ "/sys/fs/cgroup" ] VOLUME [ "/imapdata" ] -VOLUME [ "/ldapdata" ] WORKDIR /root/ diff --git a/docker/kolab/imapd.conf b/docker/kolab/imapd.conf --- a/docker/kolab/imapd.conf +++ b/docker/kolab/imapd.conf @@ -32,6 +32,7 @@ delete_mode: delayed expunge_mode: delayed postuser: shared +sharedprefix: shared # on systems with cyrus 3+ specify search engine # search_engine: squat chatty: 1 diff --git a/docker/kolab/kolab-init.sh b/docker/kolab/kolab-init.sh --- a/docker/kolab/kolab-init.sh +++ b/docker/kolab/kolab-init.sh @@ -4,7 +4,6 @@ ./01-reverse-etc-hosts.sh && echo "01 done" ./02-write-my.cnf.sh && echo "02 done" -./03-setup-ldap.sh && echo "03 ldap done" ./03-setup-kolab.sh && echo "03 kolab done" ./04-reset-mysql-kolab-password.sh && echo "04 done" ./05-adjust-configs.sh && echo "05 done" diff --git a/docker/kolab/kolab.conf b/docker/kolab/kolab.conf --- a/docker/kolab/kolab.conf +++ b/docker/kolab/kolab.conf @@ -12,14 +12,14 @@ virtual_domains = userid [ldap] -ldap_uri = ldap://127.0.0.1:389 +ldap_uri = ldap://LDAP_HOST:389 timeout = 10 supported_controls = 0,2,3 base_dn = dc=mgmt,dc=com -bind_dn = cn=Directory Manager -bind_pw = +bind_dn = LDAP_ADMIN_BIND_DN +bind_pw = LDAP_ADMIN_BIND_PW service_bind_dn = uid=kolab-service,ou=Special Users,dc=mgmt,dc=com -service_bind_pw = +service_bind_pw = LDAP_SERVICE_BIND_PW user_base_dn = dc=hosted,dc=com user_scope = sub user_filter = (objectclass=inetorgperson) @@ -66,8 +66,8 @@ [cyrus-imap] uri = imaps://127.0.0.1:11993 -admin_login = cyrus-admin -admin_password = +admin_login = IMAP_ADMIN_LOGIN +admin_password = IMAP_ADMIN_PASSWORD [cyrus-sasl] result_attribute = mail diff --git a/docker/kolab/utils/03-setup-ldap.sh b/docker/kolab/utils/03-setup-ldap.sh deleted file mode 100755 --- a/docker/kolab/utils/03-setup-ldap.sh +++ /dev/null @@ -1,260 +0,0 @@ -#!/bin/bash - -. ./settings.sh - -cp -av /bin/true /usr/sbin/ds_systemd_ask_password_acl - -if [ -f "/etc/dirsrv/slapd-kolab/dse.ldif" ]; then - echo "LDAP directory exists, nothing to do" - - mkdir -p /var/log/dirsrv/slapd-kolab/ - chmod 777 /var/log/dirsrv/slapd-kolab/ - systemctl start dirsrv@kolab - mkdir /run/dirsrv - chmod 777 /run/dirsrv - mkdir -p /run/lock/dirsrv/slapd-kolab/ - chown dirsrv:dirsrv /run/lock/dirsrv/slapd-kolab/ - chmod 777 /run/lock/dirsrv/slapd-kolab/ - mkdir -p /var/lib/dirsrv/slapd-kolab - chown dirsrv:dirsrv /var/lib/dirsrv/slapd-kolab - - systemctl start dirsrv@kolab -else - sed -i -e 's/sys.exit/print("exit") #sys.exit/' /usr/lib/python3.6/site-packages/pykolab/setup/setup_ldap.py - - echo "LDAP directory does not exist, setting it up." - CMD="$(which setup-kolab) ldap \ - --default ${LDAP_HOST} \ - --fqdn=kolab.${domain} \ - --directory-manager-pwd=${LDAP_ADMIN_BIND_PW}" - ${CMD} 2>&1 | tee -a /root/setup-kolab.log - - - # Create hosted kolab service - ( - echo "dn: uid=hosted-kolab-service,ou=Special Users,${rootdn}" - echo "objectclass: top" - echo "objectclass: inetorgperson" - echo "objectclass: person" - echo "uid: hosted-kolab-service" - echo "cn: Hosted Kolab Service Account" - echo "sn: Service Account" - echo "givenname: Hosted Kolab" - echo "userpassword: ${hosted_kolab_service_pw}" - echo "" - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" - - # Create ou domain - ( - echo "dn: ou=Domains,${rootdn}" - echo "ou: Domains" - echo "objectClass: top" - echo "objectClass: organizationalunit" - echo "" - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" - - # Create management domain - ( - echo "dn: associateddomain=${domain},${domain_base_dn}" - echo "aci: (targetattr = \"*\")(version 3.0;acl \"Deny Rest\";deny (all)(userdn != \"ldap:///uid=kolab-service,ou=Special Users,${rootdn} || ldap:///${rootdn}??sub?(objectclass=*)\");)" - echo "aci: (targetattr = \"*\")(version 3.0;acl \"Deny Hosted Kolab\";deny (all)(userdn = \"ldap:///uid=hosted-kolab-service,ou=Special Users,${rootdn}\");)" - echo "inetDomainStatus: active" - echo "objectClass: top" - echo "objectClass: domainrelatedobject" - echo "objectClass: inetdomain" - echo "associatedDomain: ${domain}" - echo "" - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" - - - # Create hosted domains - ( - echo "dn: associateddomain=${hosted_domain},${domain_base_dn}" - echo "objectclass: top" - echo "objectclass: domainrelatedobject" - echo "objectclass: inetdomain" - echo "inetdomainstatus: active" - echo "associateddomain: ${hosted_domain}" - echo "inetdomainbasedn: ${hosted_domain_rootdn}" - echo "" - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" - - ( - echo "dn: cn=$(echo ${hosted_domain} | sed -e 's/\./_/g'),cn=ldbm database,cn=plugins,cn=config" - echo "objectClass: top" - echo "objectClass: extensibleobject" - echo "objectClass: nsbackendinstance" - echo "cn: $(echo ${hosted_domain} | sed -e 's/\./_/g')" - echo "nsslapd-suffix: ${hosted_domain_rootdn}" - echo "nsslapd-cachesize: -1" - echo "nsslapd-cachememsize: 10485760" - echo "nsslapd-readonly: off" - echo "nsslapd-require-index: off" - echo "nsslapd-directory: /var/lib/dirsrv/slapd-${DS_INSTANCE_NAME:-$(hostname -s)}/db/$(echo ${hosted_domain} | sed -e 's/\./_/g')" - echo "nsslapd-dncachememsize: 10485760" - echo "" - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" - - ( - #On centos7 - #echo "dn: cn=$(echo ${hosted_domain_rootdn} | sed -e 's/=/\\3D/g' -e 's/,/\\2D/g'),cn=mapping tree,cn=config" - #On centos8 - echo "dn: cn=\"${hosted_domain_rootdn}\",cn=mapping tree,cn=config" - echo "objectClass: top" - echo "objectClass: extensibleObject" - echo "objectClass: nsMappingTree" - echo "nsslapd-state: backend" - echo "cn: ${hosted_domain_rootdn}" - echo "nsslapd-backend: $(echo ${hosted_domain} | sed -e 's/\./_/g')" - echo "" - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" - - ( - echo "dn: ${hosted_domain_rootdn}" - echo "aci: (targetattr=\"carLicense || description || displayName || facsimileTelephoneNumber || homePhone || homePostalAddress || initials || jpegPhoto || labeledURI || mobile || pager || photo || postOfficeBox || postalAddress || postalCode || preferredDeliveryMethod || preferredLanguage || registeredAddress || roomNumber || secretary || seeAlso || st || street || telephoneNumber || telexNumber || title || userCertificate || userPassword || userSMIMECertificate || x500UniqueIdentifier\")(version 3.0; acl \"Enable self write for common attributes\"; allow (write) userdn=\"ldap:///self\";)" - echo "aci: (targetattr =\"*\")(version 3.0;acl \"Directory Administrators Group\";allow (all) (groupdn=\"ldap:///cn=Directory Administrators,${hosted_domain_rootdn}\" or roledn=\"ldap:///cn=kolab-admin,${hosted_domain_rootdn}\");)" - echo "aci: (targetattr=\"*\")(version 3.0; acl \"Configuration Administrators Group\"; allow (all) groupdn=\"ldap:///cn=Configuration Administrators,ou=Groups,ou=TopologyManagement,o=NetscapeRoot\";)" - echo "aci: (targetattr=\"*\")(version 3.0; acl \"Configuration Administrator\"; allow (all) userdn=\"ldap:///uid=admin,ou=Administrators,ou=TopologyManagement,o=NetscapeRoot\";)" - echo "aci: (targetattr = \"*\")(version 3.0; acl \"SIE Group\"; allow (all) groupdn = \"ldap:///cn=slapd-$(hostname -s),cn=389 Directory Server,cn=Server Group,cn=$(hostname -f),ou=${domain},o=NetscapeRoot\";)" - echo "aci: (targetattr = \"*\") (version 3.0;acl \"Search Access\";allow (read,compare,search)(userdn = \"ldap:///${hosted_domain_rootdn}??sub?(objectclass=*)\");)" - echo "aci: (targetattr = \"*\") (version 3.0;acl \"Service Search Access\";allow (read,compare,search)(userdn = \"ldap:///uid=kolab-service,ou=Special Users,${rootdn}\");)" - echo "objectClass: top" - echo "objectClass: domain" - echo "dc: $(echo ${hosted_domain} | cut -d'.' -f 1)" - echo "" - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" - - ( - for role in "2fa-user" "activesync-user" "imap-user"; do - echo "dn: cn=${role},${hosted_domain_rootdn}" - echo "cn: ${role}" - echo "description: ${role} role" - echo "objectclass: top" - echo "objectclass: ldapsubentry" - echo "objectclass: nsmanagedroledefinition" - echo "objectclass: nsroledefinition" - echo "objectclass: nssimpleroledefinition" - echo "" - done - - echo "dn: ou=Groups,${hosted_domain_rootdn}" - echo "ou: Groups" - echo "objectClass: top" - echo "objectClass: organizationalunit" - echo "" - - echo "dn: ou=People,${hosted_domain_rootdn}" - echo "aci: (targetattr = \"*\") (version 3.0;acl \"Hosted Kolab Services\";allow (all)(userdn = \"ldap:///uid=hosted-kolab-service,ou=Special Users,${rootdn}\");)" - echo "ou: People" - echo "objectClass: top" - echo "objectClass: organizationalunit" - echo "" - - echo "dn: ou=Special Users,${hosted_domain_rootdn}" - echo "ou: Special Users" - echo "objectClass: top" - echo "objectClass: organizationalunit" - echo "" - - echo "dn: ou=Resources,${hosted_domain_rootdn}" - echo "ou: Resources" - echo "objectClass: top" - echo "objectClass: organizationalunit" - echo "" - - echo "dn: ou=Shared Folders,${hosted_domain_rootdn}" - echo "ou: Shared Folders" - echo "objectClass: top" - echo "objectClass: organizationalunit" - echo "" - - echo "dn: uid=cyrus-admin,ou=Special Users,${hosted_domain_rootdn}" - echo "sn: Administrator" - echo "uid: cyrus-admin" - echo "objectClass: top" - echo "objectClass: person" - echo "objectClass: inetorgperson" - echo "objectClass: organizationalperson" - echo "givenName: Cyrus" - echo "cn: Cyrus Administrator" - echo "" - - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" - - - # Remove cn kolab cn config - ( - echo "associateddomain=${domain},cn=kolab,cn=config" - echo "cn=kolab,cn=config" - ) | ldapdelete -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - - - # Remove hosted service access from mgmt domain - ( - echo "dn: associateddomain=${domain},ou=Domains,${rootdn}" - echo "changetype: modify" - echo "replace: aci" - echo "aci: (targetattr = \"*\")(version 3.0;acl \"Deny Rest\";deny (all)(userdn != \"ldap:///uid=kolab-service,ou=Special Users,${rootdn} || ldap:///${rootdn}??sub?(objectclass=*)\");)" - echo "aci: (targetattr = \"*\")(version 3.0;acl \"Deny Hosted Kolab\";deny (all)(userdn = \"ldap:///uid=hosted-kolab-service,ou=Special Users,${rootdn}\");)" - echo "" - ) | ldapmodify -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" - - - # Add alias attribute index - # - export index_attr=alias - - ( - echo "dn: cn=${index_attr},cn=index,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" - echo "objectclass: top" - echo "objectclass: nsindex" - echo "cn: ${index_attr}" - echo "nsSystemIndex: false" - echo "nsindextype: pres" - echo "nsindextype: eq" - echo "nsindextype: sub" - - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - - - ( - echo "dn: cn=${hosted_domain_db} ${index_attr} index,cn=index,cn=tasks,cn=config" - echo "objectclass: top" - echo "objectclass: extensibleObject" - echo "cn: ${hosted_domain_db} ${index_attr} index" - echo "nsinstance: ${hosted_domain_db}" - echo "nsIndexAttribute: ${index_attr}:pres" - echo "nsIndexAttribute: ${index_attr}:eq" - echo "nsIndexAttribute: ${index_attr}:sub" - echo "" - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - - ldap_complete=0 - - while [ ${ldap_complete} -ne 1 ]; do - result=$( - ldapsearch \ - -x \ - -h "${ldap_host}" \ - -D "${ldap_binddn}" \ - -w "${ldap_bindpw}" \ - -c \ - -LLL \ - -b "cn=${hosted_domain_db} ${index_attr} index,cn=index,cn=tasks,cn=config" \ - '(!(nstaskexitcode=0))' \ - -s base 2>/dev/null - ) - if [ -z "$result" ]; then - ldap_complete=1 - echo "" - else - echo -n "." - sleep 1 - fi - done - - ./50-add-vlv-searches.sh - ./51-add-vlv-indexes.sh - ./52-run-vlv-index-tasks.sh -fi - diff --git a/docker/kolab/utils/50-add-vlv-searches.sh b/docker/kolab/utils/50-add-vlv-searches.sh deleted file mode 100755 --- a/docker/kolab/utils/50-add-vlv-searches.sh +++ /dev/null @@ -1,53 +0,0 @@ -#!/bin/bash - - . ./settings.sh - -( - echo "dn: cn=PVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" - echo "objectClass: top" - echo "objectClass: vlvSearch" - echo "cn: PVS" - echo "vlvBase: ${hosted_domain_rootdn}" - echo "vlvScope: 2" - echo "vlvFilter: (objectclass=inetorgperson)" - echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" - echo "" -) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - -( - echo "dn: cn=RVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" - echo "objectClass: top" - echo "objectClass: vlvSearch" - echo "cn: RVS" - echo "vlvBase: ${hosted_domain_rootdn}" - echo "vlvScope: 2" - echo "vlvFilter: (|(&(objectclass=kolabsharedfolder)(kolabfoldertype=event)(mail=*))(objectclass=groupofuniquenames)(objectclass=groupofurls))" - echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" - echo "" -) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - -( - echo "dn: cn=GVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" - echo "objectClass: top" - echo "objectClass: vlvSearch" - echo "cn: GVS" - echo "vlvBase: ${hosted_domain_rootdn}" - echo "vlvScope: 2" - echo "vlvFilter: (|(objectclass=groupofuniquenames)(objectclass=groupofurls))" - echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" - echo "" -) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - -if [ "${domain_base_dn}" != "cn=kolab,cn=config" ]; then - ( - echo "dn: cn=DVS,cn=${domain_db},cn=ldbm database,cn=plugins,cn=config" - echo "objectClass: top" - echo "objectClass: vlvSearch" - echo "cn: DVS" - echo "vlvBase: ${domain_base_dn}" - echo "vlvScope: 2" - echo "vlvFilter: (objectclass=domainrelatedobject)" - echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" - echo "" - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c -fi diff --git a/docker/kolab/utils/51-add-vlv-indexes.sh b/docker/kolab/utils/51-add-vlv-indexes.sh deleted file mode 100755 --- a/docker/kolab/utils/51-add-vlv-indexes.sh +++ /dev/null @@ -1,45 +0,0 @@ -#!/bin/bash - - . ./settings.sh - -( - echo "dn: cn=PVI,cn=PVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" - echo "objectClass: top" - echo "objectClass: vlvIndex" - echo "cn: PVI" - echo "vlvSort: displayname sn givenname cn" - echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" - echo "" -) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - -( - echo "dn: cn=RVI,cn=RVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" - echo "objectClass: top" - echo "objectClass: vlvIndex" - echo "cn: RVI" - echo "vlvSort: cn" - echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" - echo "" -) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - -( - echo "dn: cn=GVI,cn=GVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" - echo "objectClass: top" - echo "objectClass: vlvIndex" - echo "cn: GVI" - echo "vlvSort: cn" - echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" - echo "" -) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - -if [ "${domain_base_dn}" != "cn=kolab,cn=config" ]; then - ( - echo "dn: cn=DVI,cn=DVS,cn=${domain_db},cn=ldbm database,cn=plugins,cn=config" - echo "objectClass: top" - echo "objectClass: vlvIndex" - echo "cn: DVI" - echo "vlvSort: associatedDomain" - echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" - echo "" - ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c -fi diff --git a/docker/kolab/utils/52-run-vlv-index-tasks.sh b/docker/kolab/utils/52-run-vlv-index-tasks.sh deleted file mode 100755 --- a/docker/kolab/utils/52-run-vlv-index-tasks.sh +++ /dev/null @@ -1,143 +0,0 @@ -#!/bin/bash - - . ./settings.sh - -( - echo "dn: cn=PVI,cn=index,cn=tasks,cn=config" - echo "objectclass: top" - echo "objectclass: extensibleObject" - echo "cn: PVI" - echo "nsinstance: ${hosted_domain_db}" - echo "nsIndexVLVAttribute: PVI" - echo "" -) | ldapmodify -a -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - -ldap_complete=0 - -while [ ${ldap_complete} -ne 1 ]; do - result=$( - ldapsearch \ - -x \ - -h ${ldap_host} \ - -D "${ldap_binddn}" \ - -w "${ldap_bindpw}" \ - -c \ - -LLL \ - -b "cn=PVI,cn=index,cn=tasks,cn=config" \ - '(!(nstaskexitcode=0))' \ - -s base 2>/dev/null - ) - if [ -z "$result" ]; then - ldap_complete=1 - echo "" - else - echo -n "." - sleep 1 - fi -done - -( - echo "dn: cn=RVI,cn=index,cn=tasks,cn=config" - echo "objectclass: top" - echo "objectclass: extensibleObject" - echo "cn: RVI" - echo "nsinstance: ${hosted_domain_db}" - echo "nsIndexVLVAttribute: RVI" - echo "" -) | ldapmodify -a -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - -ldap_complete=0 - -while [ ${ldap_complete} -ne 1 ]; do - result=$( - ldapsearch \ - -x \ - -h ${ldap_host} \ - -D "${ldap_binddn}" \ - -w "${ldap_bindpw}" \ - -c \ - -LLL \ - -b "cn=RVI,cn=index,cn=tasks,cn=config" \ - '(!(nstaskexitcode=0))' \ - -s base 2>/dev/null - ) - if [ -z "$result" ]; then - ldap_complete=1 - echo "" - else - echo -n "." - sleep 1 - fi -done - - - -( - echo "dn: cn=GVI,cn=index,cn=tasks,cn=config" - echo "objectclass: top" - echo "objectclass: extensibleObject" - echo "cn: GVI" - echo "nsinstance: ${hosted_domain_db}" - echo "nsIndexVLVAttribute: GVI" - echo "" -) | ldapmodify -a -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - -ldap_complete=0 - -while [ ${ldap_complete} -ne 1 ]; do - result=$( - ldapsearch \ - -x \ - -h ${ldap_host} \ - -D "${ldap_binddn}" \ - -w "${ldap_bindpw}" \ - -c \ - -LLL \ - -b "cn=GVI,cn=index,cn=tasks,cn=config" \ - '(!(nstaskexitcode=0))' \ - -s base 2>/dev/null - ) - if [ -z "$result" ]; then - ldap_complete=1 - echo "" - else - echo -n "." - sleep 1 - fi -done - -if [ "${domain_base_dn}" != "cn=kolab,cn=config" ]; then - ( - echo "dn: cn=DVI,cn=index,cn=tasks,cn=config" - echo "objectclass: top" - echo "objectclass: extensibleObject" - echo "cn: DVI" - echo "nsinstance: ${domain_db}" - echo "nsIndexVLVAttribute: DVI" - echo "" - ) | ldapmodify -a -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c - - ldap_complete=0 - - while [ ${ldap_complete} -ne 1 ]; do - result=$( - ldapsearch \ - -x \ - -h ${ldap_host} \ - -D "${ldap_binddn}" \ - -w "${ldap_bindpw}" \ - -c \ - -LLL \ - -b "cn=DVI,cn=index,cn=tasks,cn=config" \ - '(!(nstaskexitcode=0))' \ - -s base 2>/dev/null - ) - if [ -z "$result" ]; then - ldap_complete=1 - echo "" - else - echo -n "." - sleep 1 - fi - done -fi diff --git a/docker/kolab/utils/cyrusadmin.sh b/docker/kolab/utils/cyrusadmin.sh new file mode 100755 --- /dev/null +++ b/docker/kolab/utils/cyrusadmin.sh @@ -0,0 +1,3 @@ +#!/bin/bash +# We use port 12143 because it has plain auth enabled +echo "$@" | cyradm --auth PLAIN -u cyrus-admin -w Welcome2KolabSystems --port 12143 localhost diff --git a/docker/kolab/utils/settings.sh b/docker/kolab/utils/settings.sh --- a/docker/kolab/utils/settings.sh +++ b/docker/kolab/utils/settings.sh @@ -11,7 +11,6 @@ export cyrus_admin_pw=${IMAP_ADMIN_PASSWORD} export kolab_service_pw=${LDAP_SERVICE_BIND_PW} -export hosted_kolab_service_pw=${LDAP_HOSTED_BIND_PW} export hosted_domain=${HOSTED_DOMAIN:-"hosted.com"} export hosted_domain_db=${HOSTED_DOMAIN_DB:-"hosted_com"} diff --git a/docker/kolab/Dockerfile b/docker/ldap/Dockerfile copy from docker/kolab/Dockerfile copy to docker/ldap/Dockerfile --- a/docker/kolab/Dockerfile +++ b/docker/ldap/Dockerfile @@ -24,48 +24,17 @@ rpm -Uvh https://mirror.apheleia-it.ch/repos/Kolab:/16/kolab-16-for-el8stream.rpm RUN sed -i -e '/^ssl/d' /etc/yum.repos.d/kolab*.repo && \ dnf config-manager --enable kolab-16-testing &&\ - dnf -y --setopt tsflags= install kolab patch &&\ + dnf -y --setopt tsflags= install kolab-schema 389-ds-base &&\ dnf clean all +COPY init.sh /init.sh COPY kolab-init.service /etc/systemd/system/kolab-init.service COPY kolab-setenv.service /etc/systemd/system/kolab-setenv.service -COPY utils /root/utils - -RUN rm -rf /etc/systemd/system/multi-user.target.wants/{avahi-daemon,sshd}.* && \ - ln -s /etc/systemd/system/kolab-init.service \ - /etc/systemd/system/multi-user.target.wants/kolab-init.service && \ - ln -s /etc/systemd/system/kolab-setenv.service \ - /etc/systemd/system/multi-user.target.wants/kolab-setenv.service +RUN systemctl disable avahi-daemon sshd; systemctl enable kolab-setenv kolab-init RUN sed -i -r -e 's/^SELINUX=.*$/SELINUX=permissive/g' /etc/selinux/config 2>/dev/null || : -COPY /rootfs / - -COPY kolab-init.sh /usr/local/sbin/ -RUN chmod 750 /usr/local/sbin/kolab-init.sh - -COPY kolab.conf /etc/kolab/kolab.conf -COPY cyrus.conf /etc/cyrus.conf -COPY imapd.conf /etc/imapd.conf -COPY imapd.annotations.conf /etc/imapd.annotations.conf -COPY guam.conf /etc/guam/sys.config - -ARG DB_KOLAB_DATABASE -ARG DB_KOLAB_USERNAME -ARG DB_KOLAB_PASSWORD -RUN sed -i -r \ - -e "s|DB_KOLAB_DATABASE|$DB_KOLAB_DATABASE|g" \ - -e "s|DB_KOLAB_USERNAME|$DB_KOLAB_USERNAME|g" \ - -e "s|DB_KOLAB_PASSWORD|$DB_KOLAB_PASSWORD|g" \ - /etc/kolab/kolab.conf - -RUN mkdir -p /imapdata/{spool,lib} && \ - rm -rf /var/spool/imap && ln -s /imapdata/spool /var/spool/imap && \ - mv /var/lib/imap /var/lib/imap-bak && ln -s /imapdata/lib /var/lib/imap && \ - chmod -R 777 /imapdata && \ - chown cyrus:mail /var/spool/imap /var/lib/imap - RUN mkdir -p /ldapdata/{config,ssca,run} /var/run/dirsrv && \ ln -s /ldapdata/config /etc/dirsrv/slapd-kolab && \ ln -s /ldapdata/ssca /etc/dirsrv/ssca && \ @@ -73,11 +42,10 @@ chmod -R 777 /ldapdata /etc/dirsrv VOLUME [ "/sys/fs/cgroup" ] -VOLUME [ "/imapdata" ] VOLUME [ "/ldapdata" ] WORKDIR /root/ CMD ["/lib/systemd/systemd"] -EXPOSE 10143/tcp 10465/tcp 10587/tcp 11143/tcp 11993/tcp +EXPOSE 389/tcp diff --git a/docker/ldap/init.sh b/docker/ldap/init.sh new file mode 100755 --- /dev/null +++ b/docker/ldap/init.sh @@ -0,0 +1,809 @@ +#!/bin/bash + +# Disable password checking +cp -av /bin/true /usr/sbin/ds_systemd_ask_password_acl + +# Make sure all the relvant folders exist in /ldapdata +mkdir -p /ldapdata/{config,ssca,run} +chmod -R 777 /ldapdata + +mkdir -p /var/log/dirsrv/slapd-kolab/ +chmod 777 /var/log/dirsrv/slapd-kolab/ + +mkdir -p /run/dirsrv +chmod 777 /run/dirsrv + +mkdir -p /run/lock/dirsrv/slapd-kolab/ +chown dirsrv:dirsrv /run/lock/dirsrv/slapd-kolab/ +chmod 777 /run/lock/dirsrv/slapd-kolab/ + +mkdir -p /var/lib/dirsrv/slapd-kolab +chown dirsrv:dirsrv /var/lib/dirsrv/slapd-kolab + + +if [ -f "/etc/dirsrv/slapd-kolab/dse.ldif" ]; then + echo "LDAP directory exists, nothing to do" + + # mkdir -p /var/log/dirsrv/slapd-kolab/ + # chmod 777 /var/log/dirsrv/slapd-kolab/ + # systemctl start dirsrv@kolab + # mkdir /run/dirsrv + # chmod 777 /run/dirsrv + # mkdir -p /run/lock/dirsrv/slapd-kolab/ + # chown dirsrv:dirsrv /run/lock/dirsrv/slapd-kolab/ + # chmod 777 /run/lock/dirsrv/slapd-kolab/ + # mkdir -p /var/lib/dirsrv/slapd-kolab + # chown dirsrv:dirsrv /var/lib/dirsrv/slapd-kolab + + systemctl start dirsrv@kolab + exit 0 +fi + +# Used for the graphical console only. +GRAPHICAL_ADMIN_PASSWORD="-22F_EjHut5JCcd" +DS_INSTANCE_NAME="kolab" +DOMAIN="mgmt.com" +FQDN="ldap.mgmt.com" + +cat << EOF > /tmp/dscreateinput +[general] +FullMachineName = ldap.mgmt.com +SuiteSpotUserID = dirsrv +SuiteSpotGroup = dirsrv +AdminDomain = mgmt.com +ConfigDirectoryLdapURL = ldap://ldap.mgmt.com:389/o=NetscapeRoot +ConfigDirectoryAdminID = admin +ConfigDirectoryAdminPwd = $GRAPHICAL_ADMIN_PASSWORD +full_machine_name = ldap.mgmt.com + +[slapd] +SlapdConfigForMC = Yes +UseExistingMC = 0 +ServerPort = 389 +ServerIdentifier = kolab +Suffix = $LDAP_ADMIN_ROOT_DN +RootDN = cn=Directory Manager +RootDNPwd = $LDAP_ADMIN_BIND_PW +ds_bename = mgmt_com +AddSampleEntries = No +instance_name = $DS_INSTANCE_NAME +root_password = $LDAP_ADMIN_BIND_PW +create_suffix_entry = True + +[backend-userroot] +suffix = $LDAP_ADMIN_ROOT_DN +create_suffix_entry = True + +[admin] +Port = 9830 +ServerAdminID = admin +ServerAdminPwd = $GRAPHICAL_ADMIN_PASSWORD + +EOF +dscreate -v from-file /tmp/dscreateinput + +cp /usr/share/dirsrv/data/template.ldif /tmp/templatedata.ldif +sed -i "s/%ds_suffix%/$LDAP_BASE_DN/" /tmp/templatedata.ldif +sed -i "s/%rootdn%/cn=Directory Manager/" /tmp/templatedata.ldif +ldapadd -x -H 'ldap://127.0.0.1:389/' -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/templatedata.ldif + + +#FIXME in kolab container setup kolab.conf entries + + +cp /usr/share/doc/kolab-schema/kolab3.ldif /etc/dirsrv/slapd-kolab/schema/99kolab3.ldif + +systemctl restart dirsrv.target +systemctl restart dirsrv@kolab +systemctl enable dirsrv.target +systemctl enable dirsrv@kolab + + + +# I'm not sure why we need to create those manually +cat << EOF > /tmp/ldapadd + +# Directory Administrators, mgmt.com +dn: cn=Directory Administrators,dc=mgmt,dc=com +objectClass: top +objectClass: groupofuniquenames +cn: Directory Administrators +uniqueMember: cn=Directory Manager + +# Groups, mgmt.com +dn: ou=Groups,dc=mgmt,dc=com +objectClass: top +objectClass: organizationalunit +ou: Groups + +# People, mgmt.com +dn: ou=People,dc=mgmt,dc=com +objectClass: top +objectClass: organizationalunit +ou: People + +# Special Users, mgmt.com +dn: ou=Special Users,dc=mgmt,dc=com +objectClass: top +objectClass: organizationalUnit +ou: Special Users +description: Special Administrative Accounts + +# Accounting Managers, Groups, mgmt.com +dn: cn=Accounting Managers,ou=Groups,dc=mgmt,dc=com +objectClass: top +objectClass: groupOfUniqueNames +cn: Accounting Managers +ou: groups +description: People who can manage accounting entries +uniqueMember: cn=Directory Manager + +# HR Managers, Groups, mgmt.com +dn: cn=HR Managers,ou=Groups,dc=mgmt,dc=com +objectClass: top +objectClass: groupOfUniqueNames +cn: HR Managers +ou: groups +description: People who can manage HR entries +uniqueMember: cn=Directory Manager + +# QA Managers, Groups, mgmt.com +dn: cn=QA Managers,ou=Groups,dc=mgmt,dc=com +objectClass: top +objectClass: groupOfUniqueNames +cn: QA Managers +ou: groups +description: People who can manage QA entries +uniqueMember: cn=Directory Manager + +# PD Managers, Groups, mgmt.com +dn: cn=PD Managers,ou=Groups,dc=mgmt,dc=com +objectClass: top +objectClass: groupOfUniqueNames +cn: PD Managers +ou: groups +description: People who can manage engineer entries +uniqueMember: cn=Directory Manager + +EOF +ldapadd -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd + + +## =========== Start of pykolab changes +# Work that pykolab used to do +# +cat << EOF > /tmp/ldapadd +# cyrus-admin, Special Users, mgmt.com +dn: uid=cyrus-admin,ou=Special Users,dc=mgmt,dc=com +objectClass: top +objectClass: person +objectClass: inetorgperson +objectClass: organizationalperson +uid: cyrus-admin +givenName: Cyrus +sn: Administrator +cn: Cyrus Administrator +userPassword: ${IMAP_ADMIN_PW} + +# kolab-service, Special Users, mgmt.com +dn: uid=kolab-service,ou=Special Users,dc=mgmt,dc=com +objectClass: top +objectClass: person +objectClass: inetorgperson +objectClass: organizationalperson +uid: kolab-service +givenName: Kolab +sn: Service +cn: Kolab Service +userPassword: ${LDAP_SERVICE_BIND_PW} + +# Resources, mgmt.com +dn: ou=Resources,dc=mgmt,dc=com +objectClass: top +objectClass: organizationalunit +ou: Resources + +# Shared Folders, mgmt.com +dn: ou=Shared Folders,dc=mgmt,dc=com +objectClass: top +objectClass: organizationalunit +ou: Shared Folders + +EOF +ldapadd -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd + + +cat << EOF > /tmp/ldapadd +dn: cn=kolab,cn=config +cn: kolab +aci: (targetattr = "*") (version 3.0;acl "Kolab Services";allow (read,compare,search)(userdn = "ldap:///uid=kolab-service,ou=Special Users,$LDAP_ADMIN_ROOT_DN");) +objectClass: top +objectClass: extensibleobject +EOF +ldapadd -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd + +echo "Adding domain $DOMAIN to list of domains for this deployment" +cat << EOF > /tmp/ldapadd +dn: associateddomain=$DOMAIN,cn=kolab,cn=config +objectClass: top +objectClass: domainrelatedobject +associatedDomain: $DOMAIN, $FQDN, localhost.localdomain, localhost +aci: (targetattr = "*") (version 3.0;acl "Read Access for $DOMAIN Users";allow (read,compare,search)(userdn = "ldap:///$LDAP_ADMIN_ROOT_DN??sub?(objectclass=*)");) +EOF +ldapadd -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd +##TODO + ## Add inetdomainbasedn in case the configured root dn is not the same as the + ## standard root dn for the domain name configured + #if not _input['rootdn'] == utils.standard_root_dn(_input['domain']): + # attrs['objectclass'].append('inetdomain') + # attrs['inetdomainbasedn'] = _input['rootdn'] + +echo "Disabling anonymous binds" +cat << EOF > /tmp/ldapadd +dn: cn=config +changetype: modify +replace: nsslapd-allow-anonymous-access +nsslapd-allow-anonymous-access: off +EOF +ldapmodify -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd + + +## TODO: Ensure the uid attribute is unique +## TODO^2: Consider renaming the general "attribute uniqueness to "uid attribute uniqueness" +echo "Enabling attribute uniqueness plugin" +cat << EOF > /tmp/ldapadd +dn: cn=attribute uniqueness,cn=plugins,cn=config +changetype: modify +replace: nsslapd-pluginEnabled +nsslapd-pluginEnabled: on +EOF +ldapmodify -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd + +echo "Enabling referential integrity plugin" +cat << EOF > /tmp/ldapadd +dn: cn=referential integrity postoperation,cn=plugins,cn=config +changetype: modify +replace: nsslapd-pluginEnabled +nsslapd-pluginEnabled: on +EOF +ldapmodify -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd + +echo "Enabling referential integrity plugin" +cat << EOF > /tmp/ldapadd +dn: cn=referential integrity postoperation,cn=plugins,cn=config +changetype: modify +replace: nsslapd-pluginEnabled +nsslapd-pluginEnabled: on +EOF +ldapmodify -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd + +echo "Enabling and configuring account policy plugin" +cat << EOF > /tmp/ldapadd +dn: cn=Account Policy Plugin,cn=plugins,cn=config +changetype: modify +replace: nsslapd-pluginEnabled +nsslapd-pluginEnabled: on + +dn: cn=config,cn=Account Policy Plugin,cn=plugins,cn=config +changetype: modify +replace: alwaysrecordlogin +alwaysrecordlogin: yes +- +add: stateattrname +stateattrname: lastLoginTime +- +add: altstateattrname +altstateattrname: createTimestamp +EOF +ldapmodify -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd + +echo "Adding the kolab-admin role" +cat << EOF > /tmp/ldapadd +dn: cn=kolab-admin,$LDAP_ADMIN_ROOT_DN +description: Kolab Administrator +objectClass: top +objectClass: ldapsubentry +objectClass: nsroledefinition +objectClass: nssimpleroledefinition +objectClass: nsmanagedroledefinition +cn = kolab-admin +EOF +ldapadd -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd + +echo "Setting access control to $LDAP_ADMIN_ROOT_DN" +cat << EOF > /tmp/ldapadd +dn: $LDAP_ADMIN_ROOT_DN +changetype: modify +replace: aci +aci: (targetattr = "carLicense || description || displayName || facsimileTelephoneNumber || homePhone || homePostalAddress || initials || jpegPhoto || l || labeledURI || mobile || o || pager || photo || postOfficeBox || postalAddress || postalCode || preferredDeliveryMethod || preferredLanguage || registeredAddress || roomNumber || secretary || seeAlso || st || street || telephoneNumber || telexNumber || title || userCertificate || userPassword || userSMIMECertificate || x500UniqueIdentifier || kolabDelegate || kolabInvitationPolicy || kolabAllowSMTPSender")(version 3.0; acl "Enable self write for common attributes"; allow (read,compare,search,write)(userdn = "ldap:///self");) +aci: (targetattr = "*")(version 3.0;acl "Directory Administrators Group";allow (all)(groupdn = "ldap:///cn=Directory Administrators,$LDAP_ADMIN_ROOT_DN" or roledn = "ldap:///cn=kolab-admin,$LDAP_ADMIN_ROOT_DN");) +aci: (targetattr="*")(version 3.0; acl "Configuration Administrators Group"; allow (all) groupdn="ldap:///cn=Configuration Administrators,ou=Groups,ou=TopologyManagement,o=NetscapeRoot";) +aci: (targetattr="*")(version 3.0; acl "Configuration Administrator"; allow (all) userdn="ldap:///uid=admin,ou=Administrators,ou=TopologyManagement,o=NetscapeRoot";) +aci: (targetattr = "*")(version 3.0; acl "SIE Group"; allow (all) groupdn = "ldap:///cn=slapd-$DS_INSTANCE_NAME,cn=389 Directory Server,cn=Server Group,cn=$FQDN,ou=$DOMAIN,o=NetscapeRoot";) +aci: (targetattr != "userPassword") (version 3.0;acl "Search Access";allow (read,compare,search)(userdn = "ldap:///all");)') +EOF +ldapadd -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd + +## =========== End of pykolab code + +# Create hosted kolab service +cat << EOF > /tmp/ldapadd +dn: uid=hosted-kolab-service,ou=Special Users,${LDAP_ADMIN_ROOT_DN} +objectclass: top +objectclass: inetorgperson +objectclass: person +uid: hosted-kolab-service +cn: Hosted Kolab Service Account +sn: Service Account +givenname: Hosted Kolab +userpassword: ${LDAP_HOSTED_BIND_PW} + +EOF +ldapadd -x -h 127.0.0.1 -D "cn=Directory Manager" -w "$LDAP_ADMIN_BIND_PW" -f /tmp/ldapadd + +export rootdn=$LDAP_ADMIN_ROOT_DN +export domain=$DOMAIN +export domain_db="mgmt_com" +export ldap_host=127.0.0.1 +export ldap_binddn=${LDAP_ADMIN_BIND_DN} +export ldap_bindpw=${LDAP_ADMIN_BIND_PW} + +export cyrus_admin=${IMAP_ADMIN_LOGIN} +export cyrus_admin_pw=${IMAP_ADMIN_PASSWORD} + +export kolab_service_pw=${LDAP_SERVICE_BIND_PW} +export hosted_kolab_service_pw=${LDAP_HOSTED_BIND_PW} + +export hosted_domain=${HOSTED_DOMAIN:-"hosted.com"} +export hosted_domain_db=${HOSTED_DOMAIN_DB:-"hosted_com"} +export hosted_domain_rootdn=${LDAP_HOSTED_ROOT_DN:-"dc=hosted,dc=com"} + +export domain_base_dn=${LDAP_DOMAIN_BASE_DN:-"ou=Domains,dc=mgmt,dc=com"} + + +# Create ou domain +( + echo "dn: ou=Domains,${rootdn}" + echo "ou: Domains" + echo "objectClass: top" + echo "objectClass: organizationalunit" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" + +# Create management domain +( + echo "dn: associateddomain=${domain},${domain_base_dn}" + echo "aci: (targetattr = \"*\")(version 3.0;acl \"Deny Rest\";deny (all)(userdn != \"ldap:///uid=kolab-service,ou=Special Users,${rootdn} || ldap:///${rootdn}??sub?(objectclass=*)\");)" + echo "aci: (targetattr = \"*\")(version 3.0;acl \"Deny Hosted Kolab\";deny (all)(userdn = \"ldap:///uid=hosted-kolab-service,ou=Special Users,${rootdn}\");)" + echo "inetDomainStatus: active" + echo "objectClass: top" + echo "objectClass: domainrelatedobject" + echo "objectClass: inetdomain" + echo "associatedDomain: ${domain}" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" + + +# Create hosted domains +( + echo "dn: associateddomain=${hosted_domain},${domain_base_dn}" + echo "objectclass: top" + echo "objectclass: domainrelatedobject" + echo "objectclass: inetdomain" + echo "inetdomainstatus: active" + echo "associateddomain: ${hosted_domain}" + echo "inetdomainbasedn: ${hosted_domain_rootdn}" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" + +( + echo "dn: cn=$(echo ${hosted_domain} | sed -e 's/\./_/g'),cn=ldbm database,cn=plugins,cn=config" + echo "objectClass: top" + echo "objectClass: extensibleobject" + echo "objectClass: nsbackendinstance" + echo "cn: $(echo ${hosted_domain} | sed -e 's/\./_/g')" + echo "nsslapd-suffix: ${hosted_domain_rootdn}" + echo "nsslapd-cachesize: -1" + echo "nsslapd-cachememsize: 10485760" + echo "nsslapd-readonly: off" + echo "nsslapd-require-index: off" + echo "nsslapd-directory: /var/lib/dirsrv/slapd-${DS_INSTANCE_NAME}/db/$(echo ${hosted_domain} | sed -e 's/\./_/g')" + echo "nsslapd-dncachememsize: 10485760" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" + +( + #On centos7 + #echo "dn: cn=$(echo ${hosted_domain_rootdn} | sed -e 's/=/\\3D/g' -e 's/,/\\2D/g'),cn=mapping tree,cn=config" + #On centos8 + echo "dn: cn=\"${hosted_domain_rootdn}\",cn=mapping tree,cn=config" + echo "objectClass: top" + echo "objectClass: extensibleObject" + echo "objectClass: nsMappingTree" + echo "nsslapd-state: backend" + echo "cn: ${hosted_domain_rootdn}" + echo "nsslapd-backend: $(echo ${hosted_domain} | sed -e 's/\./_/g')" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" + +( + echo "dn: ${hosted_domain_rootdn}" + echo "aci: (targetattr=\"carLicense || description || displayName || facsimileTelephoneNumber || homePhone || homePostalAddress || initials || jpegPhoto || labeledURI || mobile || pager || photo || postOfficeBox || postalAddress || postalCode || preferredDeliveryMethod || preferredLanguage || registeredAddress || roomNumber || secretary || seeAlso || st || street || telephoneNumber || telexNumber || title || userCertificate || userPassword || userSMIMECertificate || x500UniqueIdentifier\")(version 3.0; acl \"Enable self write for common attributes\"; allow (write) userdn=\"ldap:///self\";)" + echo "aci: (targetattr =\"*\")(version 3.0;acl \"Directory Administrators Group\";allow (all) (groupdn=\"ldap:///cn=Directory Administrators,${hosted_domain_rootdn}\" or roledn=\"ldap:///cn=kolab-admin,${hosted_domain_rootdn}\");)" + echo "aci: (targetattr=\"*\")(version 3.0; acl \"Configuration Administrators Group\"; allow (all) groupdn=\"ldap:///cn=Configuration Administrators,ou=Groups,ou=TopologyManagement,o=NetscapeRoot\";)" + echo "aci: (targetattr=\"*\")(version 3.0; acl \"Configuration Administrator\"; allow (all) userdn=\"ldap:///uid=admin,ou=Administrators,ou=TopologyManagement,o=NetscapeRoot\";)" + echo "aci: (targetattr = \"*\")(version 3.0; acl \"SIE Group\"; allow (all) groupdn = \"ldap:///cn=slapd-${DS_INSTANCE_NAME},cn=389 Directory Server,cn=Server Group,cn=$FQDN,ou=${domain},o=NetscapeRoot\";)" + echo "aci: (targetattr = \"*\") (version 3.0;acl \"Search Access\";allow (read,compare,search)(userdn = \"ldap:///${hosted_domain_rootdn}??sub?(objectclass=*)\");)" + echo "aci: (targetattr = \"*\") (version 3.0;acl \"Service Search Access\";allow (read,compare,search)(userdn = \"ldap:///uid=kolab-service,ou=Special Users,${rootdn}\");)" + echo "objectClass: top" + echo "objectClass: domain" + echo "dc: $(echo ${hosted_domain} | cut -d'.' -f 1)" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" + +( + for role in "2fa-user" "activesync-user" "imap-user"; do + echo "dn: cn=${role},${hosted_domain_rootdn}" + echo "cn: ${role}" + echo "description: ${role} role" + echo "objectclass: top" + echo "objectclass: ldapsubentry" + echo "objectclass: nsmanagedroledefinition" + echo "objectclass: nsroledefinition" + echo "objectclass: nssimpleroledefinition" + echo "" + done + + echo "dn: ou=Groups,${hosted_domain_rootdn}" + echo "ou: Groups" + echo "objectClass: top" + echo "objectClass: organizationalunit" + echo "" + + echo "dn: ou=People,${hosted_domain_rootdn}" + echo "aci: (targetattr = \"*\") (version 3.0;acl \"Hosted Kolab Services\";allow (all)(userdn = \"ldap:///uid=hosted-kolab-service,ou=Special Users,${rootdn}\");)" + echo "ou: People" + echo "objectClass: top" + echo "objectClass: organizationalunit" + echo "" + + echo "dn: ou=Special Users,${hosted_domain_rootdn}" + echo "ou: Special Users" + echo "objectClass: top" + echo "objectClass: organizationalunit" + echo "" + + echo "dn: ou=Resources,${hosted_domain_rootdn}" + echo "ou: Resources" + echo "objectClass: top" + echo "objectClass: organizationalunit" + echo "" + + echo "dn: ou=Shared Folders,${hosted_domain_rootdn}" + echo "ou: Shared Folders" + echo "objectClass: top" + echo "objectClass: organizationalunit" + echo "" + + echo "dn: uid=cyrus-admin,ou=Special Users,${hosted_domain_rootdn}" + echo "sn: Administrator" + echo "uid: cyrus-admin" + echo "objectClass: top" + echo "objectClass: person" + echo "objectClass: inetorgperson" + echo "objectClass: organizationalperson" + echo "givenName: Cyrus" + echo "cn: Cyrus Administrator" + echo "" + +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" + + +# Remove cn kolab cn config +( + echo "associateddomain=${domain},cn=kolab,cn=config" + echo "cn=kolab,cn=config" +) | ldapdelete -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + + +# Remove hosted service access from mgmt domain +( + echo "dn: associateddomain=${domain},ou=Domains,${rootdn}" + echo "changetype: modify" + echo "replace: aci" + echo "aci: (targetattr = \"*\")(version 3.0;acl \"Deny Rest\";deny (all)(userdn != \"ldap:///uid=kolab-service,ou=Special Users,${rootdn} || ldap:///${rootdn}??sub?(objectclass=*)\");)" + echo "aci: (targetattr = \"*\")(version 3.0;acl \"Deny Hosted Kolab\";deny (all)(userdn = \"ldap:///uid=hosted-kolab-service,ou=Special Users,${rootdn}\");)" + echo "" +) | ldapmodify -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" + + +# Add alias attribute index +# +export index_attr=alias + +( + echo "dn: cn=${index_attr},cn=index,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" + echo "objectclass: top" + echo "objectclass: nsindex" + echo "cn: ${index_attr}" + echo "nsSystemIndex: false" + echo "nsindextype: pres" + echo "nsindextype: eq" + echo "nsindextype: sub" + +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + + +( + echo "dn: cn=${hosted_domain_db} ${index_attr} index,cn=index,cn=tasks,cn=config" + echo "objectclass: top" + echo "objectclass: extensibleObject" + echo "cn: ${hosted_domain_db} ${index_attr} index" + echo "nsinstance: ${hosted_domain_db}" + echo "nsIndexAttribute: ${index_attr}:pres" + echo "nsIndexAttribute: ${index_attr}:eq" + echo "nsIndexAttribute: ${index_attr}:sub" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + +ldap_complete=0 + +while [ ${ldap_complete} -ne 1 ]; do + result=$( + ldapsearch \ + -x \ + -h "${ldap_host}" \ + -D "${ldap_binddn}" \ + -w "${ldap_bindpw}" \ + -c \ + -LLL \ + -b "cn=${hosted_domain_db} ${index_attr} index,cn=index,cn=tasks,cn=config" \ + '(!(nstaskexitcode=0))' \ + -s base 2>/dev/null + ) + if [ -z "$result" ]; then + ldap_complete=1 + echo "" + else + echo -n "." + sleep 1 + fi +done + + +# Add VLV searches +( + echo "dn: cn=PVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" + echo "objectClass: top" + echo "objectClass: vlvSearch" + echo "cn: PVS" + echo "vlvBase: ${hosted_domain_rootdn}" + echo "vlvScope: 2" + echo "vlvFilter: (objectclass=inetorgperson)" + echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + +( + echo "dn: cn=RVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" + echo "objectClass: top" + echo "objectClass: vlvSearch" + echo "cn: RVS" + echo "vlvBase: ${hosted_domain_rootdn}" + echo "vlvScope: 2" + echo "vlvFilter: (|(&(objectclass=kolabsharedfolder)(kolabfoldertype=event)(mail=*))(objectclass=groupofuniquenames)(objectclass=groupofurls))" + echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + +( + echo "dn: cn=GVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" + echo "objectClass: top" + echo "objectClass: vlvSearch" + echo "cn: GVS" + echo "vlvBase: ${hosted_domain_rootdn}" + echo "vlvScope: 2" + echo "vlvFilter: (|(objectclass=groupofuniquenames)(objectclass=groupofurls))" + echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + +if [ "${domain_base_dn}" != "cn=kolab,cn=config" ]; then + ( + echo "dn: cn=DVS,cn=${domain_db},cn=ldbm database,cn=plugins,cn=config" + echo "objectClass: top" + echo "objectClass: vlvSearch" + echo "cn: DVS" + echo "vlvBase: ${domain_base_dn}" + echo "vlvScope: 2" + echo "vlvFilter: (objectclass=domainrelatedobject)" + echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" + echo "" + ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c +fi + + + +# Add vlv indexes +( + echo "dn: cn=PVI,cn=PVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" + echo "objectClass: top" + echo "objectClass: vlvIndex" + echo "cn: PVI" + echo "vlvSort: displayname sn givenname cn" + echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + +( + echo "dn: cn=RVI,cn=RVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" + echo "objectClass: top" + echo "objectClass: vlvIndex" + echo "cn: RVI" + echo "vlvSort: cn" + echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + +( + echo "dn: cn=GVI,cn=GVS,cn=${hosted_domain_db},cn=ldbm database,cn=plugins,cn=config" + echo "objectClass: top" + echo "objectClass: vlvIndex" + echo "cn: GVI" + echo "vlvSort: cn" + echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" + echo "" +) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + +if [ "${domain_base_dn}" != "cn=kolab,cn=config" ]; then + ( + echo "dn: cn=DVI,cn=DVS,cn=${domain_db},cn=ldbm database,cn=plugins,cn=config" + echo "objectClass: top" + echo "objectClass: vlvIndex" + echo "cn: DVI" + echo "vlvSort: associatedDomain" + echo "aci: (targetattr = \"*\") (version 3.0;acl \"Read Access\";allow (read,compare,search)(userdn = \"ldap:///anyone\");)" + echo "" + ) | ldapadd -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c +fi + +# Run vlv index tasks +( + echo "dn: cn=PVI,cn=index,cn=tasks,cn=config" + echo "objectclass: top" + echo "objectclass: extensibleObject" + echo "cn: PVI" + echo "nsinstance: ${hosted_domain_db}" + echo "nsIndexVLVAttribute: PVI" + echo "" +) | ldapmodify -a -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + +ldap_complete=0 + +while [ ${ldap_complete} -ne 1 ]; do + result=$( + ldapsearch \ + -x \ + -h ${ldap_host} \ + -D "${ldap_binddn}" \ + -w "${ldap_bindpw}" \ + -c \ + -LLL \ + -b "cn=PVI,cn=index,cn=tasks,cn=config" \ + '(!(nstaskexitcode=0))' \ + -s base 2>/dev/null + ) + if [ -z "$result" ]; then + ldap_complete=1 + echo "" + else + echo -n "." + sleep 1 + fi +done + +( + echo "dn: cn=RVI,cn=index,cn=tasks,cn=config" + echo "objectclass: top" + echo "objectclass: extensibleObject" + echo "cn: RVI" + echo "nsinstance: ${hosted_domain_db}" + echo "nsIndexVLVAttribute: RVI" + echo "" +) | ldapmodify -a -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + +ldap_complete=0 + +while [ ${ldap_complete} -ne 1 ]; do + result=$( + ldapsearch \ + -x \ + -h ${ldap_host} \ + -D "${ldap_binddn}" \ + -w "${ldap_bindpw}" \ + -c \ + -LLL \ + -b "cn=RVI,cn=index,cn=tasks,cn=config" \ + '(!(nstaskexitcode=0))' \ + -s base 2>/dev/null + ) + if [ -z "$result" ]; then + ldap_complete=1 + echo "" + else + echo -n "." + sleep 1 + fi +done + + + +( + echo "dn: cn=GVI,cn=index,cn=tasks,cn=config" + echo "objectclass: top" + echo "objectclass: extensibleObject" + echo "cn: GVI" + echo "nsinstance: ${hosted_domain_db}" + echo "nsIndexVLVAttribute: GVI" + echo "" +) | ldapmodify -a -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + +ldap_complete=0 + +while [ ${ldap_complete} -ne 1 ]; do + result=$( + ldapsearch \ + -x \ + -h ${ldap_host} \ + -D "${ldap_binddn}" \ + -w "${ldap_bindpw}" \ + -c \ + -LLL \ + -b "cn=GVI,cn=index,cn=tasks,cn=config" \ + '(!(nstaskexitcode=0))' \ + -s base 2>/dev/null + ) + if [ -z "$result" ]; then + ldap_complete=1 + echo "" + else + echo -n "." + sleep 1 + fi +done + +if [ "${domain_base_dn}" != "cn=kolab,cn=config" ]; then + ( + echo "dn: cn=DVI,cn=index,cn=tasks,cn=config" + echo "objectclass: top" + echo "objectclass: extensibleObject" + echo "cn: DVI" + echo "nsinstance: ${domain_db}" + echo "nsIndexVLVAttribute: DVI" + echo "" + ) | ldapmodify -a -x -h ${ldap_host} -D "${ldap_binddn}" -w "${ldap_bindpw}" -c + + ldap_complete=0 + + while [ ${ldap_complete} -ne 1 ]; do + result=$( + ldapsearch \ + -x \ + -h ${ldap_host} \ + -D "${ldap_binddn}" \ + -w "${ldap_bindpw}" \ + -c \ + -LLL \ + -b "cn=DVI,cn=index,cn=tasks,cn=config" \ + '(!(nstaskexitcode=0))' \ + -s base 2>/dev/null + ) + if [ -z "$result" ]; then + ldap_complete=1 + echo "" + else + echo -n "." + sleep 1 + fi + done +fi diff --git a/docker/ldap/kolab-init.service b/docker/ldap/kolab-init.service new file mode 100644 --- /dev/null +++ b/docker/ldap/kolab-init.service @@ -0,0 +1,13 @@ +[Unit] +Description=Kolab Setup Service +Requires=kolab-setenv.service +After=kolab-setenv.service ldapdata.mount + +[Service] +Type=oneshot +EnvironmentFile=/etc/openshift-environment +ExecStart=/init.sh +RemainAfterExit=yes + +[Install] +WantedBy=multi-user.target diff --git a/docker/ldap/kolab-setenv.service b/docker/ldap/kolab-setenv.service new file mode 100644 --- /dev/null +++ b/docker/ldap/kolab-setenv.service @@ -0,0 +1,9 @@ +[Unit] +Description=Kolab Set Environment + +[Service] +Type=oneshot +ExecStart=/bin/bash -c "cat /proc/1/environ | tr '\0' '\n' > /etc/openshift-environment" + +[Install] +WantedBy=multi-user.target diff --git a/docker/meet/Dockerfile b/docker/meet/Dockerfile --- a/docker/meet/Dockerfile +++ b/docker/meet/Dockerfile @@ -7,6 +7,7 @@ npm nodejs python3 python3-pip meson ninja-build make gcc g++ git && \ dnf clean all +ARG GIT_REF=master COPY build.sh /build.sh RUN /build.sh COPY init.sh /init.sh diff --git a/docker/meet/build.sh b/docker/meet/build.sh --- a/docker/meet/build.sh +++ b/docker/meet/build.sh @@ -2,7 +2,10 @@ set -e mkdir /src/ cd /src/ -git clone https://git.kolab.org/source/kolab.git kolab +git clone --branch $GIT_REF https://git.kolab.org/source/kolab.git kolab +pushd kolab +git reset --hard $GIT_REF +popd cp -R kolab/meet/server /src/meetsrc rm -Rf /src/meetsrc/node_modules cd /src/meetsrc diff --git a/docker/swoole/Dockerfile b/docker/swoole/Dockerfile --- a/docker/swoole/Dockerfile +++ b/docker/swoole/Dockerfile @@ -1,8 +1,8 @@ -FROM fedora:35 +FROM fedora:37 MAINTAINER Jeroen van Meeuwen -ARG SWOOLE_VERSION=v4.11.1 +ARG SWOOLE_VERSION=v5.0.2 ENV HOME=/opt/app-root/src LABEL io.k8s.description="Platform for serving PHP applications under Swoole" \ @@ -29,7 +29,7 @@ php-mysqlnd \ re2c \ wget && \ - git clone https://github.com/openswoole/swoole-src.git/ /swoole-src.git/ && \ + git clone https://github.com/swoole/swoole-src.git/ /swoole-src.git/ && \ cd /swoole-src.git/ && \ git checkout -f ${SWOOLE_VERSION} && \ git clean -d -f -x && \ @@ -38,7 +38,6 @@ ./configure \ --enable-sockets \ --disable-mysqlnd \ - --enable-http2 \ --enable-openssl && \ make -j4 && \ make install && \ @@ -52,8 +51,8 @@ php-devel \ re2c && \ dnf clean all && \ - echo "extension=openswoole.so" >> /etc/php.d/openswoole.ini && \ - php -m 2>&1 | grep -q openswoole + echo "extension=swoole.so" >> /etc/php.d/swoole.ini && \ + php -m 2>&1 | grep -q swoole RUN id default || (groupadd -g 1001 default && useradd -d /opt/app-root/ -u 1001 -g 1001 default) diff --git a/docker/tests/Dockerfile b/docker/tests/Dockerfile --- a/docker/tests/Dockerfile +++ b/docker/tests/Dockerfile @@ -1,4 +1,4 @@ -FROM apheleia/swoole:4.8.x +FROM apheleia/swoole:latest MAINTAINER Jeroen van Meeuwen diff --git a/docker/webapp/Dockerfile b/docker/webapp/Dockerfile --- a/docker/webapp/Dockerfile +++ b/docker/webapp/Dockerfile @@ -1,15 +1,17 @@ -FROM apheleia/swoole:4.8.x +FROM apheleia/swoole:latest MAINTAINER Jeroen van Meeuwen USER root -RUN dnf -y install findutils gnupg2 git rsync +RUN dnf -y install findutils gnupg2 git rsync procps-ng EXPOSE 8000 +ARG GIT_REF=master COPY build.sh /build.sh RUN /build.sh COPY init.sh /init.sh +COPY update.sh /update.sh CMD [ "/init.sh" ] diff --git a/docker/webapp/build.sh b/docker/webapp/build.sh --- a/docker/webapp/build.sh +++ b/docker/webapp/build.sh @@ -5,7 +5,11 @@ mkdir /src cd /src -git clone https://git.kolab.org/source/kolab.git kolab +git clone --branch $GIT_REF https://git.kolab.org/source/kolab.git kolab +pushd kolab +git reset --hard $GIT_REF +popd + cp -a kolab/src /src/kolabsrc cd /src/kolabsrc diff --git a/docker/webapp/update.sh b/docker/webapp/update.sh new file mode 100755 --- /dev/null +++ b/docker/webapp/update.sh @@ -0,0 +1,22 @@ +#!/bin/bash +set -e +set -x + +rsync -av \ + --exclude=vendor \ + --exclude=composer.lock \ + --exclude=node_modules \ + --exclude=package-lock.json \ + --exclude=public \ + --exclude=storage \ + --exclude=resources/build \ + --exclude=bootstrap \ + --exclude=.gitignore \ + /src/kolabsrc.orig/ /src/kolabsrc/ | tee /tmp/rsync.output + +cd /src/kolabsrc/ +# Only run npm if something relevant was updated +if grep -e "package.json" -e "resources" /tmp/rsync.output; then + npm run dev +fi +./artisan octane:reload diff --git a/src/app/Backends/DAV.php b/src/app/Backends/DAV.php --- a/src/app/Backends/DAV.php +++ b/src/app/Backends/DAV.php @@ -184,7 +184,8 @@ $folder = DAV\Folder::fromDomElement($element); // Note: Addressbooks don't have 'type' specified - if (($component == self::TYPE_VCARD && in_array('addressbook', $folder->types)) + if ( + ($component == self::TYPE_VCARD && in_array('addressbook', $folder->types)) || in_array($component, $folder->components) ) { $folders[] = $folder; @@ -303,7 +304,7 @@ // could fetch "an index" but also any of object's data. $body = '' - .' ' + . ' ' . '' . '' . '' @@ -359,7 +360,7 @@ ]; $body = '' - .' ' + . ' ' . '' . '' . '' diff --git a/src/app/Backends/DAV/CommonObject.php b/src/app/Backends/DAV/CommonObject.php --- a/src/app/Backends/DAV/CommonObject.php +++ b/src/app/Backends/DAV/CommonObject.php @@ -33,7 +33,7 @@ // Extract UID from the URL $href_parts = explode('/', $object->href); - $object->uid = preg_replace('/\.[a-z]+$/', '', $href_parts[count($href_parts)-1]); + $object->uid = preg_replace('/\.[a-z]+$/', '', $href_parts[count($href_parts) - 1]); } if ($etag = $element->getElementsByTagName('getetag')->item(0)) { diff --git a/src/app/Backends/DAV/Vevent.php b/src/app/Backends/DAV/Vevent.php --- a/src/app/Backends/DAV/Vevent.php +++ b/src/app/Backends/DAV/Vevent.php @@ -108,87 +108,85 @@ } switch ($prop->name) { - case 'DTSTART': - case 'DTEND': - case 'DUE': - case 'CREATED': - case 'LAST-MODIFIED': - case 'DTSTAMP': - $key = Str::camel(strtolower($prop->name)); - // These are of type Sabre\VObject\Property\ICalendar\DateTime - $this->{$key} = $prop; - break; - - case 'RRULE': - $params = !empty($this->recurrence) ? $this->recurrence : []; - - foreach ($prop->getParts() as $k => $v) { - $params[Str::camel(strtolower($k))] = is_array($v) ? implode(',', $v) : $v; - } + case 'DTSTART': + case 'DTEND': + case 'DUE': + case 'CREATED': + case 'LAST-MODIFIED': + case 'DTSTAMP': + $key = Str::camel(strtolower($prop->name)); + // These are of type Sabre\VObject\Property\ICalendar\DateTime + $this->{$key} = $prop; + break; - if (!empty($params['until'])) { - $params['until'] = new \DateTime($params['until']); - } + case 'RRULE': + $params = !empty($this->recurrence) ? $this->recurrence : []; - if (empty($params['interval'])) { - $params['interval'] = 1; - } + foreach ($prop->getParts() as $k => $v) { + $params[Str::camel(strtolower($k))] = is_array($v) ? implode(',', $v) : $v; + } - $this->recurrence = array_filter($params); - break; + if (!empty($params['until'])) { + $params['until'] = new \DateTime($params['until']); + } - case 'EXDATE': - case 'RDATE': - $key = strtolower($prop->name); - $dates = []; // TODO + if (empty($params['interval'])) { + $params['interval'] = 1; + } - if (!empty($this->recurrence[$key])) { - $this->recurrence[$key] = array_merge($this->recurrence[$key], $dates); - } - else { - $this->recurrence[$key] = $dates; - } + $this->recurrence = array_filter($params); + break; - break; - - case 'ATTENDEE': - case 'ORGANIZER': - $attendee = [ - 'rsvp' => false, - 'email' => preg_replace('!^mailto:!i', '', (string) $prop), - ]; - - $attendeeProps = ['CN', 'PARTSTAT', 'ROLE', 'CUTYPE', 'RSVP', 'DELEGATED-FROM', 'DELEGATED-TO', - 'SCHEDULE-STATUS', 'SCHEDULE-AGENT', 'SENT-BY']; - - foreach ($prop->parameters() as $name => $value) { - $key = Str::camel(strtolower($name)); - switch ($name) { - case 'RSVP': - $params[$key] = strtolower($value) == 'true'; - break; - case 'CN': - $params[$key] = str_replace('\,', ',', strval($value)); - break; - default: - if (in_array($name, $attendeeProps)) { - $params[$key] = strval($value); - } - break; + case 'EXDATE': + case 'RDATE': + $key = strtolower($prop->name); + $dates = []; // TODO + + if (!empty($this->recurrence[$key])) { + $this->recurrence[$key] = array_merge($this->recurrence[$key], $dates); + } else { + $this->recurrence[$key] = $dates; } - } - if ($prop->name == 'ORGANIZER') { - $attendee['role'] = 'ORGANIZER'; - $attendee['partstat'] = 'ACCEPTED'; + break; - $this->organizer = $attendee; - } - else if (empty($this->organizer) || $attendee['email'] != $this->organizer['email']) { - $this->attendees[] = $attendee; - } + case 'ATTENDEE': + case 'ORGANIZER': + $attendee = [ + 'rsvp' => false, + 'email' => preg_replace('!^mailto:!i', '', (string) $prop), + ]; + + $attendeeProps = ['CN', 'PARTSTAT', 'ROLE', 'CUTYPE', 'RSVP', 'DELEGATED-FROM', 'DELEGATED-TO', + 'SCHEDULE-STATUS', 'SCHEDULE-AGENT', 'SENT-BY']; + + foreach ($prop->parameters() as $name => $value) { + $key = Str::camel(strtolower($name)); + switch ($name) { + case 'RSVP': + $params[$key] = strtolower($value) == 'true'; + break; + case 'CN': + $params[$key] = str_replace('\,', ',', strval($value)); + break; + default: + if (in_array($name, $attendeeProps)) { + $params[$key] = strval($value); + } + break; + } + } + + if ($prop->name == 'ORGANIZER') { + $attendee['role'] = 'ORGANIZER'; + $attendee['partstat'] = 'ACCEPTED'; + + $this->organizer = $attendee; + } elseif (empty($this->organizer) || $attendee['email'] != $this->organizer['email']) { + $this->attendees[] = $attendee; + } - break; + break; } } @@ -217,47 +215,46 @@ $value = strval($prop); switch ($prop->name) { - case 'TRIGGER': - foreach ($prop->parameters as $param) { - if ($param->name == 'VALUE' && $param->getValue() == 'DATE-TIME') { - $trigger = '@' . $prop->getDateTime()->format('U'); - $alarm['trigger'] = $prop->getDateTime(); + case 'TRIGGER': + foreach ($prop->parameters as $param) { + if ($param->name == 'VALUE' && $param->getValue() == 'DATE-TIME') { + $trigger = '@' . $prop->getDateTime()->format('U'); + $alarm['trigger'] = $prop->getDateTime(); + } elseif ($param->name == 'RELATED') { + $alarm['related'] = $param->getValue(); + } } - else if ($param->name == 'RELATED') { - $alarm['related'] = $param->getValue(); + /* + if (!$trigger && ($values = libcalendaring::parse_alarm_value($value))) { + $trigger = $values[2]; } - } -/* - if (!$trigger && ($values = libcalendaring::parse_alarm_value($value))) { - $trigger = $values[2]; - } -*/ - if (empty($alarm['trigger'])) { - $alarm['trigger'] = rtrim(preg_replace('/([A-Z])0[WDHMS]/', '\\1', $value), 'T'); - // if all 0-values have been stripped, assume 'at time' - if ($alarm['trigger'] == 'P') { - $alarm['trigger'] = 'PT0S'; + */ + if (empty($alarm['trigger'])) { + $alarm['trigger'] = rtrim(preg_replace('/([A-Z])0[WDHMS]/', '\\1', $value), 'T'); + // if all 0-values have been stripped, assume 'at time' + if ($alarm['trigger'] == 'P') { + $alarm['trigger'] = 'PT0S'; + } } - } - break; + break; - case 'ACTION': - $action = $alarm['action'] = strtoupper($value); - break; + case 'ACTION': + $action = $alarm['action'] = strtoupper($value); + break; - case 'SUMMARY': - case 'DESCRIPTION': - case 'DURATION': - $alarm[strtolower($prop->name)] = $value; - break; + case 'SUMMARY': + case 'DESCRIPTION': + case 'DURATION': + $alarm[strtolower($prop->name)] = $value; + break; - case 'REPEAT': - $alarm['repeat'] = (int) $value; - break; + case 'REPEAT': + $alarm['repeat'] = (int) $value; + break; - case 'ATTENDEE': - $alarm['attendees'][] = preg_replace('!^mailto:!i', '', $value); - break; + case 'ATTENDEE': + $alarm['attendees'][] = preg_replace('!^mailto:!i', '', $value); + break; } } diff --git a/src/app/Backends/IMAP.php b/src/app/Backends/IMAP.php --- a/src/app/Backends/IMAP.php +++ b/src/app/Backends/IMAP.php @@ -2,7 +2,6 @@ namespace App\Backends; -use App\Domain; use App\Group; use App\Resource; use App\SharedFolder; @@ -76,6 +75,7 @@ // Mailbox already exists if (self::folderExists($imap, $mailbox)) { $imap->closeConnection(); + self::createDefaultFolders($user); return true; } @@ -107,11 +107,36 @@ $imap->setQuota($mailbox, ['storage' => $quota]); } + self::createDefaultFolders($user); + $imap->closeConnection(); return true; } + /** + * Create default folders for the user. + * + * @param \App\User $user User + */ + public static function createDefaultFolders(User $user): void + { + if ($defaultFolders = \config('imap.default_folders')) { + $config = self::getConfig(); + // Log in as user to set private annotations and subscription state + $imap = self::initIMAP($config, $user->email); + foreach ($defaultFolders as $name => $folderconfig) { + try { + $mailbox = self::toUTF7($name); + self::createFolder($imap, $mailbox, true, $folderconfig['metadata']); + } catch (\Exception $e) { + \Log::warning("Failed to create the default folder" . $e->getMessage()); + } + } + $imap->closeConnection(); + } + } + /** * Delete a mailbox. * @@ -184,28 +209,13 @@ $settings = $resource->getSettings(['invitation_policy', 'folder']); $mailbox = self::toUTF7($settings['folder']); - // Mailbox already exists - if (self::folderExists($imap, $mailbox)) { - $imap->closeConnection(); - return true; - } - - // Create the shared folder - if (!$imap->createFolder($mailbox)) { - \Log::error("Failed to create mailbox {$mailbox}"); - $imap->closeConnection(); - return false; - } - - // Set folder type - $imap->setMetadata($mailbox, ['/shared/vendor/kolab/folder-type' => 'event']); - - // Set ACL + $acl = null; if (!empty($settings['invitation_policy'])) { if (preg_match('/^manual:(\S+@\S+)$/', $settings['invitation_policy'], $m)) { - self::aclUpdate($imap, $mailbox, ["{$m[1]}, full"]); + $acl = ["{$m[1]}, full"]; } } + self::createFolder($imap, $mailbox, false, ['/shared/vendor/kolab/folder-type' => 'event'], $acl); $imap->closeConnection(); @@ -299,24 +309,7 @@ $acl = !empty($settings['acl']) ? json_decode($settings['acl'], true) : null; $mailbox = self::toUTF7($settings['folder']); - // Mailbox already exists - if (self::folderExists($imap, $mailbox)) { - $imap->closeConnection(); - return true; - } - - // Create the mailbox - if (!$imap->createFolder($mailbox)) { - \Log::error("Failed to create mailbox {$mailbox}"); - $imap->closeConnection(); - return false; - } - - // Set folder type - $imap->setMetadata($mailbox, ['/shared/vendor/kolab/folder-type' => $folder->type]); - - // Set ACL - self::aclUpdate($imap, $mailbox, $acl); + self::createFolder($imap, $mailbox, false, ['/shared/vendor/kolab/folder-type' => $folder->type], $acl); $imap->closeConnection(); @@ -459,6 +452,30 @@ return false; } + /** + * Check if an account is set up + * + * @param string $username User login (email address) + * + * @return bool True if an account exists and is set up, False otherwise + */ + public static function verifyDefaultFolders(string $username): bool + { + $config = self::getConfig(); + $imap = self::initIMAP($config, $username); + + foreach (\config('imap.default_folders') as $mb => $_metadata) { + $mailbox = self::toUTF7($mb); + if (!self::folderExists($imap, $mailbox)) { + $imap->closeConnection(); + return false; + } + } + + $imap->closeConnection(); + return true; + } + /** * Check if we can connect to the imap server * @@ -515,6 +532,43 @@ $imap->closeConnection(); } + /** + * Create a folder and set some default properties + * + * @param \rcube_imap_generic $imap The imap instance + * @param string $mailbox Mailbox name + * @param bool $subscribe Subscribe to the folder + * @param array $metadata Metadata to set on the folder + * @param array $acl Acl to set on the folder + * + * @return bool True when having a folder created, False if it already existed. + * @throws \Exception + */ + private static function createFolder($imap, string $mailbox, $subscribe = false, $metadata = null, $acl = null) + { + if (self::folderExists($imap, $mailbox)) { + return false; + } + + if (!$imap->createFolder($mailbox)) { + throw new \Exception("Failed to create mailbox {$mailbox}"); + } + + if ($acl) { + self::aclUpdate($imap, $mailbox, $acl, true); + } + + if ($subscribe) { + $imap->subscribe($mailbox); + } + + foreach ($metadata as $key => $value) { + $imap->setMetadata($mailbox, [$key => $value]); + } + + return true; + } + /** * Convert Kolab ACL into IMAP user->rights array */ diff --git a/src/app/Console/Commands/User/ResyncCommand.php b/src/app/Console/Commands/User/ResyncCommand.php --- a/src/app/Console/Commands/User/ResyncCommand.php +++ b/src/app/Console/Commands/User/ResyncCommand.php @@ -45,8 +45,9 @@ if ($deleted_only) { $users->whereNotNull('deleted_at') - ->where(function($query) { - $query->where('status', '&', User::STATUS_IMAP_READY)->orWhere('status', '&', User::STATUS_LDAP_READY); + ->where(function ($query) { + $query->where('status', '&', User::STATUS_IMAP_READY) + ->orWhere('status', '&', User::STATUS_LDAP_READY); }); } @@ -88,7 +89,7 @@ } \App\Jobs\User\CreateJob::dispatch($user->id); - } else if (!empty($req_user)) { + } elseif (!empty($req_user)) { if ($dry_run) { $this->info("{$user->email}: will be pushed"); continue; diff --git a/src/app/Domain.php b/src/app/Domain.php --- a/src/app/Domain.php +++ b/src/app/Domain.php @@ -44,6 +44,15 @@ // domain has been created in LDAP public const STATUS_LDAP_READY = 1 << 6; + /** @var int The allowed states for this object used in StatusPropertyTrait */ + private int $allowed_states = self::STATUS_NEW | + self::STATUS_ACTIVE | + self::STATUS_SUSPENDED | + self::STATUS_DELETED | + self::STATUS_CONFIRMED | + self::STATUS_VERIFIED | + self::STATUS_LDAP_READY; + // open for public registration public const TYPE_PUBLIC = 1 << 0; // zone hosted with us @@ -170,29 +179,13 @@ */ public function setStatusAttribute($status) { - $new_status = 0; - - $allowed_values = [ - self::STATUS_NEW, - self::STATUS_ACTIVE, - self::STATUS_SUSPENDED, - self::STATUS_DELETED, - self::STATUS_CONFIRMED, - self::STATUS_VERIFIED, - self::STATUS_LDAP_READY, - ]; - - foreach ($allowed_values as $value) { - if ($status & $value) { - $new_status |= $value; - $status ^= $value; - } - } - - if ($status > 0) { + // Detect invalid flags + if ($status & ~$this->allowed_states) { throw new \Exception("Invalid domain status: {$status}"); } + $new_status = $status; + if ($this->isPublic()) { $this->attributes['status'] = $new_status; return; diff --git a/src/app/Group.php b/src/app/Group.php --- a/src/app/Group.php +++ b/src/app/Group.php @@ -44,6 +44,13 @@ // group has been created in LDAP public const STATUS_LDAP_READY = 1 << 4; + /** @var int The allowed states for this object used in StatusPropertyTrait */ + private int $allowed_states = self::STATUS_NEW | + self::STATUS_ACTIVE | + self::STATUS_SUSPENDED | + self::STATUS_DELETED | + self::STATUS_LDAP_READY; + /** @var array The attributes that should be cast */ protected $casts = [ 'created_at' => 'datetime:Y-m-d H:i:s', diff --git a/src/app/Http/Controllers/API/V4/PaymentsController.php b/src/app/Http/Controllers/API/V4/PaymentsController.php --- a/src/app/Http/Controllers/API/V4/PaymentsController.php +++ b/src/app/Http/Controllers/API/V4/PaymentsController.php @@ -376,6 +376,7 @@ $mandate['amount'] = $mandate['minAmount'] = (int) ceil(Payment::MIN_AMOUNT / 100); $mandate['balance'] = 0; $mandate['isDisabled'] = !empty($mandate['id']) && $settings['mandate_disabled']; + $mandate['isValid'] = !empty($mandate['isValid']); foreach (['amount', 'balance'] as $key) { if (($value = $settings["mandate_{$key}"]) !== null) { @@ -523,16 +524,16 @@ $request['vat_rate_id'] = $rate->id; switch (\config('app.vat.mode')) { - case 1: - // In this mode tax is added on top of the payment. The amount - // to pay grows, but we keep wallet balance without tax. - $request['amount'] = $request['amount'] + round($request['amount'] * $rate->rate / 100); - break; - - default: - // In this mode tax is "swallowed" by the vendor. The payment - // amount does not change - break; + case 1: + // In this mode tax is added on top of the payment. The amount + // to pay grows, but we keep wallet balance without tax. + $request['amount'] = $request['amount'] + round($request['amount'] * $rate->rate / 100); + break; + + default: + // In this mode tax is "swallowed" by the vendor. The payment + // amount does not change + break; } } } diff --git a/src/app/Http/Controllers/API/V4/UsersController.php b/src/app/Http/Controllers/API/V4/UsersController.php --- a/src/app/Http/Controllers/API/V4/UsersController.php +++ b/src/app/Http/Controllers/API/V4/UsersController.php @@ -203,7 +203,7 @@ 'enableUsers' => $isController, 'enableWallets' => $isController, 'enableWalletMandates' => $isController, - 'enableWalletPayments' => !$plan || $plan->mode != 'mandate', + 'enableWalletPayments' => $isController && (!$plan || $plan->mode != 'mandate'), 'enableCompanionapps' => $hasBeta, ]; diff --git a/src/app/Http/Middleware/ContentSecurityPolicy.php b/src/app/Http/Middleware/ContentSecurityPolicy.php --- a/src/app/Http/Middleware/ContentSecurityPolicy.php +++ b/src/app/Http/Middleware/ContentSecurityPolicy.php @@ -21,6 +21,11 @@ 'xfo' => 'X-Frame-Options', ]; + //Exclude horizon routes, per https://github.com/laravel/horizon/issues/576 + if ($request->is('horizon*')) { + $headers = []; + } + $next = $next($request); foreach ($headers as $opt => $header) { diff --git a/src/app/Payment.php b/src/app/Payment.php --- a/src/app/Payment.php +++ b/src/app/Payment.php @@ -106,7 +106,15 @@ */ public function credit($method): void { - // TODO: Possibly we should sanity check that payment is paid, and not negative? + if (empty($this->wallet)) { + throw new \Exception("Cannot credit a payment not assigned to a wallet"); + } + + if ($this->credit_amount < 0) { + throw new \Exception("Cannot credit a payment with negative amount"); + } + + // TODO: Possibly we should sanity check that payment is paid? // TODO: Localization? $description = $this->type == self::TYPE_RECURRING ? 'Auto-payment' : 'Payment'; $description .= " transaction {$this->id} using {$method}"; diff --git a/src/app/Providers/AuthServiceProvider.php b/src/app/Providers/AuthServiceProvider.php --- a/src/app/Providers/AuthServiceProvider.php +++ b/src/app/Providers/AuthServiceProvider.php @@ -2,10 +2,7 @@ namespace App\Providers; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Route; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; -use Laravel\Passport\Passport; class AuthServiceProvider extends ServiceProvider { @@ -25,17 +22,5 @@ public function boot() { $this->registerPolicies(); - - Passport::tokensCan([ - 'api' => 'Access API', - 'mfa' => 'Access MFA API', - ]); - - Passport::tokensExpireIn(now()->addMinutes(\config('auth.token_expiry_minutes'))); - Passport::refreshTokensExpireIn(now()->addMinutes(\config('auth.refresh_token_expiry_minutes'))); - Passport::personalAccessTokensExpireIn(now()->addMonths(6)); - - Passport::useClientModel(\App\Auth\PassportClient::class); - Passport::tokenModel()::observe(\App\Observers\Passport\TokenObserver::class); } } diff --git a/src/app/Providers/PassportServiceProvider.php b/src/app/Providers/PassportServiceProvider.php --- a/src/app/Providers/PassportServiceProvider.php +++ b/src/app/Providers/PassportServiceProvider.php @@ -10,6 +10,26 @@ class PassportServiceProvider extends \Laravel\Passport\PassportServiceProvider { + /** + * Register any authentication / authorization services. + * + * @return void + */ + public function boot() + { + Passport::tokensCan([ + 'api' => 'Access API', + 'mfa' => 'Access MFA API', + ]); + + Passport::tokensExpireIn(now()->addMinutes(\config('auth.token_expiry_minutes'))); + Passport::refreshTokensExpireIn(now()->addMinutes(\config('auth.refresh_token_expiry_minutes'))); + Passport::personalAccessTokensExpireIn(now()->addMonths(6)); + + Passport::useClientModel(\App\Auth\PassportClient::class); + Passport::tokenModel()::observe(\App\Observers\Passport\TokenObserver::class); + } + /** * Make the authorization service instance. * diff --git a/src/app/Resource.php b/src/app/Resource.php --- a/src/app/Resource.php +++ b/src/app/Resource.php @@ -45,6 +45,13 @@ // resource has been created in IMAP public const STATUS_IMAP_READY = 1 << 8; + /** @var int The allowed states for this object used in StatusPropertyTrait */ + private int $allowed_states = self::STATUS_NEW | + self::STATUS_ACTIVE | + self::STATUS_DELETED | + self::STATUS_LDAP_READY | + self::STATUS_IMAP_READY; + // A template for the email attribute on a resource creation public const EMAIL_TEMPLATE = 'resource-{id}@{domainName}'; diff --git a/src/app/SharedFolder.php b/src/app/SharedFolder.php --- a/src/app/SharedFolder.php +++ b/src/app/SharedFolder.php @@ -48,6 +48,13 @@ // folder has been created in IMAP public const STATUS_IMAP_READY = 1 << 8; + /** @var int The allowed states for this object used in StatusPropertyTrait */ + private int $allowed_states = self::STATUS_NEW | + self::STATUS_ACTIVE | + self::STATUS_DELETED | + self::STATUS_LDAP_READY | + self::STATUS_IMAP_READY; + /** @const array Supported folder type labels */ public const SUPPORTED_TYPES = ['mail', 'event', 'contact', 'task', 'note', 'file']; diff --git a/src/app/Traits/StatusPropertyTrait.php b/src/app/Traits/StatusPropertyTrait.php --- a/src/app/Traits/StatusPropertyTrait.php +++ b/src/app/Traits/StatusPropertyTrait.php @@ -101,34 +101,10 @@ */ public function setStatusAttribute($status) { - $new_status = 0; - - $allowed_states = [ - 'STATUS_NEW', - 'STATUS_ACTIVE', - 'STATUS_SUSPENDED', - 'STATUS_DELETED', - 'STATUS_LDAP_READY', - 'STATUS_IMAP_READY', - ]; - - foreach ($allowed_states as $const) { - if (!defined("static::$const")) { - continue; - } - - $value = constant("static::$const"); - - if ($status & $value) { - $new_status |= $value; - $status ^= $value; - } - } - - if ($status > 0) { + if ($status & ~$this->allowed_states) { throw new \Exception("Invalid status: {$status}"); } - $this->attributes['status'] = $new_status; + $this->attributes['status'] = $status; } } diff --git a/src/app/User.php b/src/app/User.php --- a/src/app/User.php +++ b/src/app/User.php @@ -61,6 +61,16 @@ // a restricted user public const STATUS_RESTRICTED = 1 << 7; + /** @var int The allowed states for this object used in StatusPropertyTrait */ + private int $allowed_states = self::STATUS_NEW | + self::STATUS_ACTIVE | + self::STATUS_SUSPENDED | + self::STATUS_DELETED | + self::STATUS_LDAP_READY | + self::STATUS_IMAP_READY | + self::STATUS_DEGRADED | + self::STATUS_RESTRICTED; + /** @var array The attributes that are mass assignable */ protected $fillable = [ 'id', @@ -547,7 +557,7 @@ /** * Un-restrict this user. * - * @param bool $deep Unrestrinct also all users in the account + * @param bool $deep Unrestrict also all users in the account * * @return void */ @@ -632,40 +642,6 @@ $this->setPasswordAttribute($password); } - /** - * User status mutator - * - * @throws \Exception - */ - public function setStatusAttribute($status) - { - $new_status = 0; - - $allowed_values = [ - self::STATUS_NEW, - self::STATUS_ACTIVE, - self::STATUS_SUSPENDED, - self::STATUS_DELETED, - self::STATUS_LDAP_READY, - self::STATUS_IMAP_READY, - self::STATUS_DEGRADED, - self::STATUS_RESTRICTED, - ]; - - foreach ($allowed_values as $value) { - if ($status & $value) { - $new_status |= $value; - $status ^= $value; - } - } - - if ($status > 0) { - throw new \Exception("Invalid user status: {$status}"); - } - - $this->attributes['status'] = $new_status; - } - /** * Validate the user credentials * diff --git a/src/config/imap.php b/src/config/imap.php --- a/src/config/imap.php +++ b/src/config/imap.php @@ -9,4 +9,52 @@ 'host' => env('IMAP_HOST', '172.18.0.5'), 'imap_port' => env('IMAP_PORT', 12143), 'guam_port' => env('IMAP_GUAM_PORT', 9143), + 'default_folders' => [ + 'Drafts' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'mail.drafts', + ], + ], + 'Sent' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'mail.sentitems', + ], + ], + 'Trash' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'mail.wastebasket', + ], + ], + 'Spam' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'mail.junkemail', + ], + ], + + 'Calendar' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'event.default' + ], + ], + 'Contacts' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'contact.default', + ], + ], + 'Tasks' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'task.default', + ], + ], + 'Notes' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'note.default', + ], + ], + 'Files' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'file.default', + ], + ], + ] ]; diff --git a/src/database/migrations/2023_02_17_100000_vat_rates_table.php b/src/database/migrations/2023_02_17_100000_vat_rates_table.php --- a/src/database/migrations/2023_02_17_100000_vat_rates_table.php +++ b/src/database/migrations/2023_02_17_100000_vat_rates_table.php @@ -61,7 +61,7 @@ ->from('user_settings') ->where('key', 'country') ->where('value', $country); - }); + }); }) ->update(['vat_rate_id' => $vatRate->id]); } diff --git a/src/tests/Browser/Pages/PaymentMollie.php b/src/tests/Browser/Pages/PaymentMollie.php --- a/src/tests/Browser/Pages/PaymentMollie.php +++ b/src/tests/Browser/Pages/PaymentMollie.php @@ -25,7 +25,7 @@ */ public function assert($browser) { - $browser->waitFor('form#body table, form#body iframe'); + $browser->waitFor('form#body table, form#body iframe', 10); } /** @@ -64,7 +64,7 @@ $browser->type('#expiryDate', '12/' . (date('y') + 1)); }) ->withinFrame('#cvv iframe', function ($browser) { - $browser->type('#verificationCode', '123'); + $browser->click('#verificationCode')->type('#verificationCode', '123'); }) ->click('#submit-button'); } diff --git a/src/tests/Browser/Pages/PaymentStripe.php b/src/tests/Browser/Pages/PaymentStripe.php --- a/src/tests/Browser/Pages/PaymentStripe.php +++ b/src/tests/Browser/Pages/PaymentStripe.php @@ -40,7 +40,7 @@ '@title' => '.App-Overview .ProductSummary', '@amount' => '#ProductSummary-totalAmount', '@description' => '#ProductSummary-Description', - '@email-input' => '.App-Payment #email', + '@email' => '.App-Payment .ReadOnlyFormField-email .ReadOnlyFormField-content', '@cardnumber-input' => '.App-Payment #cardNumber', '@cardexpiry-input' => '.App-Payment #cardExpiry', '@cardcvc-input' => '.App-Payment #cardCvc', diff --git a/src/tests/Browser/PaymentMollieTest.php b/src/tests/Browser/PaymentMollieTest.php --- a/src/tests/Browser/PaymentMollieTest.php +++ b/src/tests/Browser/PaymentMollieTest.php @@ -108,6 +108,7 @@ ->on(new WalletPage()) ->assertMissing('@body #mandate-form .alert') ->click('@main #mandate-form button') +/* ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Set up auto-payment') ->waitFor('#payment-method-selection .link-creditcard svg') @@ -115,8 +116,10 @@ ->assertMissing('#payment-method-selection .link-banktransfer') ->click('#payment-method-selection .link-creditcard'); }) +*/ ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Set up auto-payment') + ->waitFor('@body #mandate_amount') ->assertSeeIn('@body label[for="mandate_amount"]', 'Fill up by') ->assertValue('@body #mandate_amount', Payment::MIN_AMOUNT / 100) ->assertSeeIn('@body label[for="mandate_balance"]', 'when account balance is below') // phpcs:ignore @@ -229,13 +232,16 @@ $browser->on(new WalletPage()) ->assertMissing('@body #mandate-form .alert') ->click('@main #mandate-form button') +/* ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Set up auto-payment') ->waitFor('#payment-method-selection .link-creditcard') ->click('#payment-method-selection .link-creditcard'); }) +*/ ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Set up auto-payment') + ->waitFor('@body #mandate_amount') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@button-action', 'Continue') // Submit valid data @@ -259,13 +265,16 @@ // Create a new mandate ->click('@main #mandate-form button') +/* ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Set up auto-payment') ->waitFor('#payment-method-selection .link-creditcard') ->click('#payment-method-selection .link-creditcard'); }) +*/ ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Set up auto-payment') + ->waitFor('@body #mandate_amount') ->assertSeeIn('@button-cancel', 'Cancel') ->assertSeeIn('@button-action', 'Continue') // Submit valid data diff --git a/src/tests/Browser/PaymentStripeTest.php b/src/tests/Browser/PaymentStripeTest.php --- a/src/tests/Browser/PaymentStripeTest.php +++ b/src/tests/Browser/PaymentStripeTest.php @@ -80,7 +80,7 @@ ->on(new PaymentStripe()) ->assertSeeIn('@title', $user->tenant->title . ' Payment') ->assertSeeIn('@amount', 'CHF 12.34') - ->assertValue('@email-input', $user->email) + ->assertSeeIn('@email', $user->email) ->submitValidCreditCard(); // Now it should redirect back to wallet page and in background @@ -115,6 +115,7 @@ ->on(new WalletPage()) ->assertMissing('@body #mandate-form .alert') ->click('@main #mandate-form button') +/* ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Set up auto-payment') ->waitFor('#payment-method-selection .link-creditcard') @@ -122,8 +123,10 @@ ->assertMissing('#payment-method-selection .link-banktransfer') ->click('#payment-method-selection .link-creditcard'); }) +*/ ->with(new Dialog('@payment-dialog'), function (Browser $browser) { $browser->assertSeeIn('@title', 'Set up auto-payment') + ->waitFor('@body #mandate_amount') ->assertSeeIn('@body label[for="mandate_amount"]', 'Fill up by') ->assertValue('@body #mandate_amount', Payment::MIN_AMOUNT / 100) ->assertSeeIn('@body label[for="mandate_balance"]', 'when account balance is below') // phpcs:ignore @@ -157,7 +160,7 @@ ->on(new PaymentStripe()) ->assertMissing('@title') ->assertMissing('@amount') - ->assertValue('@email-input', $user->email) + ->assertSeeIn('@email', $user->email) ->submitValidCreditCard() ->waitForLocation('/wallet', 30) // need more time than default 5 sec. ->visit('/wallet?paymentProvider=stripe') diff --git a/src/tests/Browser/SignupTest.php b/src/tests/Browser/SignupTest.php --- a/src/tests/Browser/SignupTest.php +++ b/src/tests/Browser/SignupTest.php @@ -12,7 +12,9 @@ use Tests\Browser\Components\Menu; use Tests\Browser\Components\Toast; use Tests\Browser\Pages\Dashboard; +use Tests\Browser\Pages\Home; use Tests\Browser\Pages\Signup; +use Tests\Browser\Pages\Wallet; use Tests\TestCaseDusk; use Illuminate\Foundation\Testing\DatabaseMigrations; @@ -29,7 +31,7 @@ $this->deleteTestUser('admin@user-domain-signup.com'); $this->deleteTestDomain('user-domain-signup.com'); - Plan::where('mode', 'token')->update(['mode' => 'email']); + Plan::whereIn('mode', ['token', 'mandate'])->update(['mode' => 'email']); } /** @@ -42,7 +44,7 @@ $this->deleteTestDomain('user-domain-signup.com'); SignupInvitation::truncate(); - Plan::where('mode', 'token')->update(['mode' => 'email']); + Plan::whereIn('mode', ['token', 'mandate'])->update(['mode' => 'email']); @unlink(storage_path('signup-tokens.txt')); @@ -518,6 +520,67 @@ }); } + /** + * Test signup with a mandate plan, also the wallet lock + */ + public function testSignupMandate(): void + { + // Test the individual plan + $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); + $plan->mode = 'mandate'; + $plan->save(); + + $this->browse(function (Browser $browser) { + $browser->visit(new Signup()) + ->waitFor('@step0 .plan-individual button') + ->click('@step0 .plan-individual button') + // Test Back button + ->whenAvailable('@step3', function ($browser) { + $browser->click('button[type=button]'); + }) + ->whenAvailable('@step0', function ($browser) { + $browser->click('.plan-individual button'); + }) + // Test submit + ->whenAvailable('@step3', function ($browser) { + $domains = Domain::getPublicDomains(); + $domains_count = count($domains); + + $browser->assertMissing('.card-title') + ->assertElementsCount('select#signup_domain option', $domains_count, false) + ->assertText('select#signup_domain option:nth-child(1)', $domains[0]) + ->assertValue('select#signup_domain option:nth-child(1)', $domains[0]) + ->type('#signup_login', 'signuptestdusk') + ->type('#signup_password', '12345678') + ->type('#signup_password_confirmation', '12345678') + ->click('[type=submit]'); + }) + ->waitUntilMissing('@step3') + ->on(new Wallet()) + ->assertSeeIn('#lock-alert', "The account is locked") + ->within(new Menu(), function ($browser) { + $browser->clickMenuItem('logout'); + }); + }); + + $user = User::where('email', 'signuptestdusk@' . \config('app.domain'))->first(); + $this->assertSame($plan->id, $user->getSetting('plan_id')); + + // Login again and see that the account is still locked + $this->browse(function (Browser $browser) use ($user) { + $browser->on(new Home()) + ->submitLogon($user->email, '12345678', false) + ->waitForLocation('/wallet') + ->on(new Wallet()) + ->assertSeeIn('#lock-alert', "The account is locked") + ->within(new Menu(), function ($browser) { + $browser->clickMenuItem('logout'); + }); + + // TODO: Test automatic UI unlock after creating a valid auto-payment mandate + }); + } + /** * Test signup with a token plan */ diff --git a/src/tests/Feature/Backends/IMAPTest.php b/src/tests/Feature/Backends/IMAPTest.php --- a/src/tests/Feature/Backends/IMAPTest.php +++ b/src/tests/Feature/Backends/IMAPTest.php @@ -107,6 +107,7 @@ $result = IMAP::createUser($user); $this->assertTrue($result); $this->assertTrue(IMAP::verifyAccount($user->email)); + $this->assertTrue(IMAP::verifyDefaultFolders($user->email)); $imap = $this->getImap(); $quota = $imap->getQuota('user/' . $user->email); @@ -128,6 +129,7 @@ $result = IMAP::verifyAccount($user->email); $this->assertFalse($result); + $this->assertFalse(IMAP::verifyDefaultFolders($user->email)); } /** @@ -150,7 +152,9 @@ $imap = $this->getImap(); $expectedAcl = ['john@kolab.org' => str_split('lrswipkxtecdn')]; - $this->assertSame($expectedAcl, $imap->getACL(IMAP::toUTF7($imapFolder))); + $acl = $imap->getACL(IMAP::toUTF7($imapFolder)); + $this->assertTrue(is_array($acl) && isset($acl['john@kolab.org'])); + $this->assertSame($expectedAcl['john@kolab.org'], $acl['john@kolab.org']); // Update the resource (rename) $resource->name = 'Resource1 ©' . time(); @@ -160,7 +164,9 @@ $this->assertTrue(IMAP::updateResource($resource, ['folder' => $imapFolder])); $this->assertTrue($imapFolder != $newImapFolder); $this->assertTrue(IMAP::verifySharedFolder($newImapFolder)); - $this->assertSame($expectedAcl, $imap->getACL(IMAP::toUTF7($newImapFolder))); + $acl = $imap->getACL(IMAP::toUTF7($newImapFolder)); + $this->assertTrue(is_array($acl) && isset($acl['john@kolab.org'])); + $this->assertSame($expectedAcl['john@kolab.org'], $acl['john@kolab.org']); // Update the resource (acl change) $resource->setSetting('invitation_policy', 'accept'); @@ -196,7 +202,11 @@ 'jack@kolab.org' => str_split('lrs') ]; - $this->assertSame($expectedAcl, $imap->getACL(IMAP::toUTF7($imapFolder))); + $acl = $imap->getACL(IMAP::toUTF7($imapFolder)); + $this->assertTrue(is_array($acl) && isset($acl['john@kolab.org'])); + $this->assertSame($expectedAcl['john@kolab.org'], $acl['john@kolab.org']); + $this->assertTrue(is_array($acl) && isset($acl['jack@kolab.org'])); + $this->assertSame($expectedAcl['jack@kolab.org'], $acl['jack@kolab.org']); // Update shared folder (acl) $folder->setSetting('acl', json_encode(['jack@kolab.org, read-only'])); @@ -205,7 +215,10 @@ $expectedAcl = ['jack@kolab.org' => str_split('lrs')]; - $this->assertSame($expectedAcl, $imap->getACL(IMAP::toUTF7($imapFolder))); + $acl = $imap->getACL(IMAP::toUTF7($imapFolder)); + $this->assertTrue(is_array($acl) && isset($acl['jack@kolab.org'])); + $this->assertSame($expectedAcl['jack@kolab.org'], $acl['jack@kolab.org']); + $this->assertTrue(!isset($acl['john@kolab.org'])); // Update the shared folder (rename) $folder->name = 'SharedFolder1 ©' . time(); @@ -215,7 +228,10 @@ $this->assertTrue(IMAP::updateSharedFolder($folder, ['folder' => $imapFolder])); $this->assertTrue($imapFolder != $newImapFolder); $this->assertTrue(IMAP::verifySharedFolder($newImapFolder)); - $this->assertSame($expectedAcl, $imap->getACL(IMAP::toUTF7($newImapFolder))); + + $acl = $imap->getACL(IMAP::toUTF7($newImapFolder)); + $this->assertTrue(is_array($acl) && isset($acl['jack@kolab.org'])); + $this->assertSame($expectedAcl['jack@kolab.org'], $acl['jack@kolab.org']); // Delete the shared folder $this->assertTrue(IMAP::deleteSharedFolder($folder)); diff --git a/src/tests/Feature/Controller/PaymentsMollieTest.php b/src/tests/Feature/Controller/PaymentsMollieTest.php --- a/src/tests/Feature/Controller/PaymentsMollieTest.php +++ b/src/tests/Feature/Controller/PaymentsMollieTest.php @@ -4,6 +4,7 @@ use App\Http\Controllers\API\V4\PaymentsController; use App\Payment; +use App\Plan; use App\Providers\PaymentProvider; use App\Transaction; use App\Wallet; @@ -47,6 +48,7 @@ Transaction::WALLET_CHARGEBACK, ]; Transaction::where('object_id', $wallet->id)->whereIn('type', $types)->delete(); + Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email', 'months' => 1]); } /** @@ -68,6 +70,7 @@ Transaction::WALLET_CHARGEBACK, ]; Transaction::where('object_id', $wallet->id)->whereIn('type', $types)->delete(); + Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email', 'months' => 1]); Utils::setTestExchangeRates([]); parent::tearDown(); @@ -337,6 +340,44 @@ $this->assertNull($wallet->fresh()->getSetting('mollie_mandate_id')); } + /** + * Test fetching an outo-payment mandate parameters + * + * @group mollie + */ + public function testMandateParams(): void + { + $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); + $user = $this->getTestUser('payment-test@' . \config('app.domain')); + $wallet = $user->wallets()->first(); + + $response = $this->actingAs($user)->get("api/v4/payments/mandate"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame((int) ceil(Payment::MIN_AMOUNT / 100), $json['amount']); + $this->assertSame($json['amount'], $json['minAmount']); + $this->assertSame(0, $json['balance']); + $this->assertFalse($json['isValid']); + $this->assertFalse($json['isDisabled']); + + $plan->months = 12; + $plan->save(); + $user->setSetting('plan_id', $plan->id); + + $response = $this->actingAs($user)->get("api/v4/payments/mandate"); + $response->assertStatus(200); + + $json = $response->json(); + + $this->assertSame((int) ceil(Payment::MIN_AMOUNT / 100), $json['amount']); + $this->assertSame((int) ceil(($plan->cost() * $plan->months) / 100), $json['minAmount']); + + // TODO: Test more cases + // TODO: Test user unrestricting if mandate is valid + } + /** * Test creating a payment and receiving a status via webhook * diff --git a/src/tests/Feature/Controller/SignupTest.php b/src/tests/Feature/Controller/SignupTest.php --- a/src/tests/Feature/Controller/SignupTest.php +++ b/src/tests/Feature/Controller/SignupTest.php @@ -84,11 +84,29 @@ return $this->domain; } + /** + * Test fetching public domains for signup + */ + public function testSignupDomains(): void + { + $response = $this->get('/api/auth/signup/domains'); + $json = $response->json(); + + $response->assertStatus(200); + + $this->assertCount(2, $json); + $this->assertSame('success', $json['status']); + $this->assertSame(Domain::getPublicDomains(), $json['domains']); + } + /** * Test fetching plans for signup */ public function testSignupPlans(): void { + $individual = Plan::withEnvTenantContext()->where('title', 'individual')->first(); + $group = Plan::withEnvTenantContext()->where('title', 'group')->first(); + $response = $this->get('/api/auth/signup/plans'); $json = $response->json(); @@ -96,10 +114,16 @@ $this->assertSame('success', $json['status']); $this->assertCount(2, $json['plans']); - $this->assertArrayHasKey('title', $json['plans'][0]); - $this->assertArrayHasKey('name', $json['plans'][0]); - $this->assertArrayHasKey('description', $json['plans'][0]); + $this->assertSame($individual->title, $json['plans'][0]['title']); + $this->assertSame($individual->name, $json['plans'][0]['name']); + $this->assertSame($individual->description, $json['plans'][0]['description']); + $this->assertFalse($json['plans'][0]['isDomain']); $this->assertArrayHasKey('button', $json['plans'][0]); + $this->assertSame($group->title, $json['plans'][1]['title']); + $this->assertSame($group->name, $json['plans'][1]['name']); + $this->assertSame($group->description, $json['plans'][1]['description']); + $this->assertTrue($json['plans'][1]['isDomain']); + $this->assertArrayHasKey('button', $json['plans'][1]); } /** diff --git a/src/tests/Feature/Controller/UsersTest.php b/src/tests/Feature/Controller/UsersTest.php --- a/src/tests/Feature/Controller/UsersTest.php +++ b/src/tests/Feature/Controller/UsersTest.php @@ -6,6 +6,7 @@ use App\Domain; use App\Http\Controllers\API\V4\UsersController; use App\Package; +use App\Plan; use App\Sku; use App\Tenant; use App\User; @@ -47,7 +48,10 @@ $wallet->save(); $user->settings()->whereIn('key', ['greylist_enabled', 'guam_enabled'])->delete(); $user->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY; + $user->status &= ~User::STATUS_RESTRICTED; $user->save(); + Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email']); + $user->setSettings(['plan_id' => null]); } /** @@ -78,7 +82,10 @@ $wallet->save(); $user->settings()->whereIn('key', ['greylist_enabled', 'guam_enabled'])->delete(); $user->status |= User::STATUS_IMAP_READY | User::STATUS_LDAP_READY; + $user->status &= ~User::STATUS_RESTRICTED; $user->save(); + Plan::withEnvTenantContext()->where('title', 'individual')->update(['mode' => 'email']); + $user->setSettings(['plan_id' => null]); parent::tearDown(); } @@ -1372,14 +1379,15 @@ public function testUserResponse(): void { $provider = \config('services.payment_provider') ?: 'mollie'; - $user = $this->getTestUser('john@kolab.org'); - $wallet = $user->wallets()->first(); + $john = $this->getTestUser('john@kolab.org'); + $wallet = $john->wallets()->first(); $wallet->setSettings(['mollie_id' => null, 'stripe_id' => null]); - $result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]); + $wallet->owner->setSettings(['plan_id' => null]); + $result = $this->invokeMethod(new UsersController(), 'userResponse', [$john]); - $this->assertEquals($user->id, $result['id']); - $this->assertEquals($user->email, $result['email']); - $this->assertEquals($user->status, $result['status']); + $this->assertEquals($john->id, $result['id']); + $this->assertEquals($john->email, $result['email']); + $this->assertEquals($john->status, $result['status']); $this->assertTrue(is_array($result['statusInfo'])); $this->assertTrue(is_array($result['settings'])); @@ -1392,13 +1400,20 @@ $this->assertCount(1, $result['wallets']); $this->assertSame($wallet->id, $result['wallet']['id']); $this->assertArrayNotHasKey('discount', $result['wallet']); + $this->assertFalse($result['isLocked']); $this->assertTrue($result['statusInfo']['enableDomains']); $this->assertTrue($result['statusInfo']['enableWallets']); + $this->assertTrue($result['statusInfo']['enableWalletMandates']); + $this->assertTrue($result['statusInfo']['enableWalletPayments']); $this->assertTrue($result['statusInfo']['enableUsers']); $this->assertTrue($result['statusInfo']['enableSettings']); // Ned is John's wallet controller + $plan = Plan::withEnvTenantContext()->where('title', 'individual')->first(); + $plan->mode = 'mandate'; + $plan->save(); + $wallet->owner->setSettings(['plan_id' => $plan->id]); $ned = $this->getTestUser('ned@kolab.org'); $ned_wallet = $ned->wallets()->first(); $result = $this->invokeMethod(new UsersController(), 'userResponse', [$ned]); @@ -1414,9 +1429,12 @@ $this->assertSame($ned_wallet->id, $result['wallets'][0]['id']); $this->assertSame($provider, $result['wallet']['provider']); $this->assertSame($provider, $result['wallets'][0]['provider']); + $this->assertFalse($result['isLocked']); $this->assertTrue($result['statusInfo']['enableDomains']); $this->assertTrue($result['statusInfo']['enableWallets']); + $this->assertTrue($result['statusInfo']['enableWalletMandates']); + $this->assertFalse($result['statusInfo']['enableWalletPayments']); $this->assertTrue($result['statusInfo']['enableUsers']); $this->assertTrue($result['statusInfo']['enableSettings']); @@ -1426,11 +1444,11 @@ $wallet->save(); $mod_provider = $provider == 'mollie' ? 'stripe' : 'mollie'; $wallet->setSetting($mod_provider . '_id', 123); - $user->refresh(); + $john->refresh(); - $result = $this->invokeMethod(new UsersController(), 'userResponse', [$user]); + $result = $this->invokeMethod(new UsersController(), 'userResponse', [$john]); - $this->assertEquals($user->id, $result['id']); + $this->assertEquals($john->id, $result['id']); $this->assertSame($discount->id, $result['wallet']['discount_id']); $this->assertSame($discount->discount, $result['wallet']['discount']); $this->assertSame($discount->description, $result['wallet']['discount_description']); @@ -1439,6 +1457,7 @@ $this->assertSame($discount->discount, $result['wallets'][0]['discount']); $this->assertSame($discount->description, $result['wallets'][0]['discount_description']); $this->assertSame($mod_provider, $result['wallets'][0]['provider']); + $this->assertFalse($result['isLocked']); // Jack is not a John's wallet controller $jack = $this->getTestUser('jack@kolab.org'); @@ -1446,8 +1465,17 @@ $this->assertFalse($result['statusInfo']['enableDomains']); $this->assertFalse($result['statusInfo']['enableWallets']); + $this->assertFalse($result['statusInfo']['enableWalletMandates']); + $this->assertFalse($result['statusInfo']['enableWalletPayments']); $this->assertFalse($result['statusInfo']['enableUsers']); $this->assertFalse($result['statusInfo']['enableSettings']); + $this->assertFalse($result['isLocked']); + + // Test locked user + $john->restrict(); + $result = $this->invokeMethod(new UsersController(), 'userResponse', [$john]); + + $this->assertTrue($result['isLocked']); } /** diff --git a/src/tests/Feature/PaymentTest.php b/src/tests/Feature/PaymentTest.php --- a/src/tests/Feature/PaymentTest.php +++ b/src/tests/Feature/PaymentTest.php @@ -82,7 +82,8 @@ $wallet->balance = -5000; $wallet->save(); - // Credit the 2nd payment + // Credit the 2nd payment (restricted user) + $user->restrict(); $payment2->credit('Test2'); $wallet->refresh(); $transaction = $wallet->transactions()->first(); @@ -91,6 +92,7 @@ $this->assertSame('1', $wallet->getSetting('mandate_disabled')); $this->assertSame($payment2->credit_amount, $transaction->amount); $this->assertSame("Auto-payment transaction {$payment2->id} using Test2", $transaction->description); + $this->assertFalse($user->refresh()->isRestricted()); } /** diff --git a/src/tests/Feature/UserTest.php b/src/tests/Feature/UserTest.php --- a/src/tests/Feature/UserTest.php +++ b/src/tests/Feature/UserTest.php @@ -1183,7 +1183,8 @@ \App\Jobs\User\UpdateJob::class, function ($job) use ($user) { return TestCase::getObjectProperty($job, 'userId') == $user->id; - }); + } + ); $userB->restrict(); $this->assertTrue($userB->fresh()->isRestricted()); @@ -1200,7 +1201,8 @@ \App\Jobs\User\UpdateJob::class, function ($job) use ($user) { return TestCase::getObjectProperty($job, 'userId') == $user->id; - }); + } + ); Queue::fake(); // reset queue state @@ -1213,7 +1215,8 @@ \App\Jobs\User\UpdateJob::class, function ($job) use ($userB) { return TestCase::getObjectProperty($job, 'userId') == $userB->id; - }); + } + ); } /** diff --git a/src/tests/Infrastructure/ActivesyncTest.php b/src/tests/Infrastructure/ActivesyncTest.php --- a/src/tests/Infrastructure/ActivesyncTest.php +++ b/src/tests/Infrastructure/ActivesyncTest.php @@ -38,15 +38,38 @@ { parent::setUp(); - if (!self::$user) { - self::$user = $this->getTestUser('activesynctest@kolab.org', ['password' => 'simple123'], true); - } if (!self::$deviceId) { // By always creating a new device we force syncroton to initialize. // Otherwise we work against uninitialized metadata (subscription states), // because the account has been removed, but syncroton doesn't reinitalize the metadata for known devices. self::$deviceId = (string) Str::uuid(); } + + $deviceId = self::$deviceId; + \config(['imap.default_folders' => [ + 'Drafts' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'mail.drafts', + '/private/vendor/kolab/activesync' => "{\"FOLDER\":{\"{$deviceId}\":{\"S\":1}}}" + ], + ], + 'Calendar' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'event.default', + '/private/vendor/kolab/activesync' => "{\"FOLDER\":{\"{$deviceId}\":{\"S\":1}}}" + ], + ], + 'Contacts' => [ + 'metadata' => [ + '/private/vendor/kolab/folder-type' => 'contact.default', + '/private/vendor/kolab/activesync' => "{\"FOLDER\":{\"{$deviceId}\":{\"S\":1}}}" + ], + ], + ]]); + + if (!self::$user) { + self::$user = $this->getTestUser('activesynctest@kolab.org', ['password' => 'simple123'], true); + } if (!self::$client) { self::$client = new \GuzzleHttp\Client([ 'http_errors' => false, // No exceptions @@ -103,12 +126,9 @@ // The hash is based on the name, so it's always the same $inboxId = '38b950ebd62cd9a66929c89615d0fc04'; $this->assertStringContainsString($inboxId, $xml); - //TODO for this to work we need to create the default folders in IMAP::createUser - // $this->assertStringContainsString('Drafts', $result); - // $this->assertStringContainsString('Sent', $result); - // $this->assertStringContainsString('Trash', $result); - // $this->assertStringContainsString('Calendar', $result); - // $this->assertStringContainsString('Contacts', $result); + $this->assertStringContainsString('Drafts', $xml); + $this->assertStringContainsString('Calendar', $xml); + $this->assertStringContainsString('Contacts', $xml); // Find the inbox for the next step // $collectionIds = $dom->getElementsByTagName('ServerId'); diff --git a/src/tests/Unit/Backends/DAV/FolderTest.php b/src/tests/Unit/Backends/DAV/FolderTest.php --- a/src/tests/Unit/Backends/DAV/FolderTest.php +++ b/src/tests/Unit/Backends/DAV/FolderTest.php @@ -14,7 +14,11 @@ { $xml = << - + /dav/calendars/user/alec@aphy.io/Default/ diff --git a/utils/kolabendpointtester.py b/utils/kolabendpointtester.py --- a/utils/kolabendpointtester.py +++ b/utils/kolabendpointtester.py @@ -352,10 +352,10 @@ print(' autodiscover CNAME', rdata.target) except dns.resolver.NXDOMAIN: success = False - print(" ERROR on autodiscover. CNAME entry") + print(f" ERROR on autodiscover.{host} CNAME entry") except dns.resolver.NoAnswer: success = False - print(" ERROR on autodiscover. CNAME entry") + print(f" ERROR on autodiscover.{host} CNAME entry") srv_records = [ f"_autodiscover._tcp.{host}",