diff --git a/myoffice_projects/README.md b/myoffice_projects/README.md new file mode 100644 index 0000000..32a8ea7 --- /dev/null +++ b/myoffice_projects/README.md @@ -0,0 +1,18 @@ +В директории import_mailion содержатся скрипты для импорта пользователей и групп из freeipa в mailion + +В директории house_configs содержатся кастомные конфиги для сервера Mailion + +В директории co_scripts содержатся плагины и скрипты для CO+pgs + +## import_mailion USAGE +Изменить 'ldap_admin_password' в export_groups_from_ldap.py и запустить скрипт: + +` +mailion.import.sh +` + +## house_configs USAGE (deprecated) +` +cp house_configs/* /srv/docker/house/conf/conf.d +docker restart house +` diff --git a/myoffice_projects/co_scripts/README.md b/myoffice_projects/co_scripts/README.md new file mode 100644 index 0000000..8004f33 --- /dev/null +++ b/myoffice_projects/co_scripts/README.md @@ -0,0 +1,9 @@ +# co_scripts + +Репозиторий для хранения скриптов расширяющих базовый функционал CO\PGS + +| Имя скрипта | Документация | Описание | +| ----------------- | ------------------------------------- | ----------------------------------------------- | +| pgs_avatar_sync | [README](pgs_avatar_sync/README.md) | Синхронизация фотографий профиля из IPA | +| pgs_group_sync | [README](pgs_group_sync/README.md) | Синхронизация групп пользователей из IPA | +| pgs_ldap_provider | [README](pgs_ldap_provider/README.md) | KeyCloak провайдер PGS для синхронизации с LDAP | diff --git a/myoffice_projects/co_scripts/pgs_avatar_sync/Dockerfile b/myoffice_projects/co_scripts/pgs_avatar_sync/Dockerfile new file mode 100644 index 0000000..8e5ed37 --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_avatar_sync/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.11-alpine +ARG FLASK_PORT=8085 +ENV FLASK_PORT=$FLASK_PORT +WORKDIR /opt/pgs_avatar +COPY . /opt/pgs_avatar +RUN python -m venv venv && \ + source venv/bin/activate && \ + pip install -U -r requirements.txt +EXPOSE $FLASK_PORT +CMD ["/bin/sh", "-c", "source venv/bin/activate; flask run --host 0.0.0.0 --port ${FLASK_PORT}"] diff --git a/myoffice_projects/co_scripts/pgs_avatar_sync/README.md b/myoffice_projects/co_scripts/pgs_avatar_sync/README.md new file mode 100644 index 0000000..75efe4f --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_avatar_sync/README.md @@ -0,0 +1,52 @@ +# PGS_AVATAR_SYNC + +Скрипт использует Flask для REST API. + +Позволяет при входе пользователей в CO обращаться на IPA сервер для получения фотографии пользователя, а затем, при необходимости, отправляет её в COAPI для обновления в профиле пользователя. + +## Переменные окружения + +| Название | Значение по умолчанию | Описание | +| ------------ | ---------------------------------------- | --------------------------------------------------- | +| CO_API_URL | "https://coapi.hyperus.team/api/v1/auth" | Конечная точка COAPI для работы с "profile/picture" | +| IPA_ADDRESS | ipa.hyperus.team | Адрес сервера IPA | +| IPA_LOGIN | automated.carbon | Учетная запись подключения к IPA | +| IPA_PASSWORD | - | Пароль учетной записи подключения к IPA | + +## Установка + +1. Собрать образ и запустить контейнер на сервере CO. +```bash +docker build . --build-arg FLASK_PORT=8085 --tag pgs_avatar_sync:0.0.1 +docker run -d -e IPA_PASSWORD="securepassword" --name pgs_avatar_sync --network host --restart always pgs_avatar_sync:0.0.1 +``` + +2. Добавить дополнительную обработку на стороне `openresty-lb-core-auth`: + +#### `/opt/openresty/nginx/conf/co/lua/auth/co_auth_login.lua` + +Ищем строку в конце файла (~60): +```lua +ngx.say(cjson.encode({ success = "true", token = token })); +``` + +Добавляем перед ней: +```lua +-- Send data for update avatars +local httpc = http:new(); +local request = { + method = "POST", + body = cjson.encode({ login = login, token = token }), + headers = { + ["Content-Type"] = "application/json; charset utf-8" + }, + ssl_verify = false +}; +--- Необходимо указать корректный порт, FLASK_PORT переданный при сборке +local avatar_res, avatar_err = httpc:request_uri("http://172.17.0.1:8085/avatar", request); +if not avatar_res then + ngx.log(ngx.ERR, "Request failed: ", avatar_err) +end +httpc:close(); +ngx.log(ngx_INFO, "Update avatar for <" .. login .. ">."); +``` diff --git a/myoffice_projects/co_scripts/pgs_avatar_sync/app.py b/myoffice_projects/co_scripts/pgs_avatar_sync/app.py new file mode 100644 index 0000000..612fea8 --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_avatar_sync/app.py @@ -0,0 +1,8 @@ +from flask import Flask +from flask_restful import Api +from modules.flask_view import PGSAvatarListener + +app = Flask(__name__) +api = Api(app) + +api.add_resource(PGSAvatarListener, "/avatar", methods=["POST"]) diff --git a/myoffice_projects/co_scripts/pgs_avatar_sync/modules/__init__.py b/myoffice_projects/co_scripts/pgs_avatar_sync/modules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myoffice_projects/co_scripts/pgs_avatar_sync/modules/flask_model.py b/myoffice_projects/co_scripts/pgs_avatar_sync/modules/flask_model.py new file mode 100644 index 0000000..f49f91a --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_avatar_sync/modules/flask_model.py @@ -0,0 +1,48 @@ +import base64 +import os +import python_freeipa +import requests +import warnings + +warnings = warnings.filterwarnings("ignore") + +class PGSAvatarModule: + def __init__(self): + self.CO_API_URL = os.getenv("COAPI_URL", "https://coapi.hyperus.team/api/v1/auth") + self.IPA_ADDRESS = os.getenv("IPA_ADDRESS", "ipa01.hyperus.team") + self.IPA_LOGIN = os.getenv("IPA_LOGIN", "automated.carbon") + self.IPA_PASSWORD = os.getenv("IPA_PASSWORD") + + def get_avatar_ipa(self, login) -> str: + ipa_user = {} + ipa_user_avatar = "" + ipa_client = python_freeipa.ClientMeta(host=self.IPA_ADDRESS, verify_ssl=False) + ipa_client.login(self.IPA_LOGIN, self.IPA_PASSWORD) + ipa_users = ipa_client.user_find(o_uid=login) + ipa_client.logout() + if ipa_users["count"] == 1: + ipa_user = ipa_users["result"][0] + if "jpegphoto" in ipa_user: + ipa_user_avatar = ipa_user["jpegphoto"][-1]["__base64__"] + else: + return "User doesnt have avatar photo", 406 + else: + return "User not found", 404 + return base64.b64decode(ipa_user_avatar), 200 + + def get_avatar_pgs(self, token) -> str: + header = {"X-co-auth-token": token} + request = requests.get(url=f"{self.CO_API_URL}/profile/picture", headers=header) + if request.status_code == 200: + return request.content, 200 + elif request.status_code == 204: + return "Avatar not exist", 204 + return "Bad response from COAPI", 400 + + def update_avatar(self, login, token, photo) -> list: + header = {"X-co-auth-token": token} + file = [("file", ("avatar.jpg", photo, "image/jpeg"))] + request = requests.post(url=f"{self.CO_API_URL}/profile/picture", headers=header, data={}, files=file) + if request.status_code == 200: + return f"Avatar has been updated for user <{login}>", 200 + return "Something bad with update process", 401 diff --git a/myoffice_projects/co_scripts/pgs_avatar_sync/modules/flask_view.py b/myoffice_projects/co_scripts/pgs_avatar_sync/modules/flask_view.py new file mode 100644 index 0000000..f3502b7 --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_avatar_sync/modules/flask_view.py @@ -0,0 +1,30 @@ +from flask_restful import reqparse, Resource +from .flask_model import PGSAvatarModule + +class PGSAvatarListener(Resource): + parser = reqparse.RequestParser() + def get(self): + return "Method not allowed", 405 + + def delete(self): + return "Method not allowed", 405 + + def post(self): + self.parser.add_argument("login") + self.parser.add_argument("token") + args = self.parser.parse_args() + if not args["login"] or not args["token"]: + return "Not correct query", 400 + login = args["login"].split("@")[0] + token = args["token"] + avatar_module = PGSAvatarModule() + ipa_photo = avatar_module.get_avatar_ipa(login) + pgs_photo = avatar_module.get_avatar_pgs(token) + if 400 in ipa_photo: + return ipa_photo + if 400 in pgs_photo: + return pgs_photo + if ipa_photo != pgs_photo: + return avatar_module.update_avatar(login, token, ipa_photo[0]) + else: + return f"Avatar photo of user <{login}> is actual", 208 diff --git a/myoffice_projects/co_scripts/pgs_avatar_sync/requirements.txt b/myoffice_projects/co_scripts/pgs_avatar_sync/requirements.txt new file mode 100644 index 0000000..e0fccc5 --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_avatar_sync/requirements.txt @@ -0,0 +1,5 @@ +flask +flask-restful +pip +python-freeipa +requests diff --git a/myoffice_projects/co_scripts/pgs_group_sync/Dockerfile b/myoffice_projects/co_scripts/pgs_group_sync/Dockerfile new file mode 100644 index 0000000..c332c3f --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_group_sync/Dockerfile @@ -0,0 +1,12 @@ +FROM python:3.10-alpine + +WORKDIR /srv/hyperus_apps/pgs_sync +COPY group_sync_pgs.py ./ + +RUN python3 -m venv venv && \ + source venv/bin/activate && \ + pip install \ + python-freeipa==1.0.6 \ + requests + +CMD ["/bin/sh", "-c", "source venv/bin/activate; python ./group_sync_pgs.py"] diff --git a/myoffice_projects/co_scripts/pgs_group_sync/README.md b/myoffice_projects/co_scripts/pgs_group_sync/README.md new file mode 100644 index 0000000..0669430 --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_group_sync/README.md @@ -0,0 +1,30 @@ +# PGS_GROUP_SYNC + +- Скрипт синхронизирует группы в PGS из IPA +- Актуализирует состав групп пользователей + +## Переменные окружения +| Название | Значение по умолчанию | Описание | +| --------------------- | ------------------------------------- | ---------------------------------------------- | +| IPA_ADDRESS | "ipa01.hyperus.team" | Адрес сервера IPA | +| IPA_GROUP_ATTR | "description" | Атрибут используемый в качестве имени группы | +| IPA_USERNAME | "automated.carbon" | Учетная запись подключения к IPA | +| IPA_PASSWORD | | Пароль учетной записи подключения к IPA | +| PGS_ADMINAPI_URL | "https://admin.hyperus.team/adminapi" | Адрес AdminAPI PGS | +| PGS_ADMINAPI_PASSWORD | | Пароль учетной записи с правами администратора | +| PGS_ADMINAPI_TENANT | "default" | Тенант к которому принадлежит домен | +| PGS_ADMINAPI_USERNAME | "admin@hyperus.team" | Учетная запись с правами администратора | + +## Установка + +1. Собрать образ и запустить контейнер. +После аргумента `-e` указать переменные окружения и их значения + +```bash +docker build . --tag pgs_group_sync:0.0.1 +docker run -d -e IPA_PASSWORD="securepassword" -e PGS_ADMINAPI_PASSWORD="securepassword" --name pgs_group_sync pgs_group_sync:0.0.1 +``` +2. Добавить в cron задачу по запуску контейнера с необходимым интервалом. +``` +*/5 * * * * docker start pgs_group_sync 2>%1 1>/dev/null +``` diff --git a/myoffice_projects/co_scripts/pgs_group_sync/group_sync_cronjob.yaml b/myoffice_projects/co_scripts/pgs_group_sync/group_sync_cronjob.yaml new file mode 100644 index 0000000..3e7a148 --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_group_sync/group_sync_cronjob.yaml @@ -0,0 +1,29 @@ +apiVersion: "batch/v1" +kind: "CronJob" +metadata: + name: "groupsync-cronjob" +spec: + concurrencyPolicy: "Forbid" + failedJobsHistoryLimit: 1 + jobTemplate: + spec: + backoffLimit: 0 + template: + spec: + containers: + - name: "group_sync_pgs" + image: "nexus.hyperus.team/groupsync:latest" + imagePullPolicy: "IfNotPresent" + env: + - name: "PGS_ADMINAPI_PASSWORD" + valueFrom: + secretKeyRef: + name: "infrastructure/pgs" + key: "ADMIN_PASSWORD" + - name: "IPA_PASSWORD" + valueFrom: + secretKeyRef: + name: "infrastructure/service_accounts/carbon" + key: "password" + restartPolicy: "Never" + schedule: "*/5 * * * *" diff --git a/myoffice_projects/co_scripts/pgs_group_sync/group_sync_pgs.py b/myoffice_projects/co_scripts/pgs_group_sync/group_sync_pgs.py new file mode 100644 index 0000000..48fec62 --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_group_sync/group_sync_pgs.py @@ -0,0 +1,147 @@ +import os +import requests +import python_freeipa as freeipa +import warnings + +warnings.filterwarnings("ignore") + + +class GroupSyncPGS: + """ + Реализует: + - Получение списка групп с участниками из FreeIPA + - Получение списка групп с участниками из PGS + - Получение списка пользователей из PGS + - Сравнение групп и членства участников + - Добавление групп и участников в PGS + - Удаление групп и участников в PGS + """ + + def __init__(self): + self.ipa_address = os.environ.get("IPA_ADDRESS", "ipa01.hyperus.team") + self.ipa_group_attr = os.environ.get("IPA_GROUP_ATTR", "description") + self.ipa_password = os.environ.get("IPA_PASSWORD") + self.ipa_username = os.environ.get("IPA_USERNAME", "automated.carbon") + self.pgs_adminapi_address = os.environ.get("PGS_ADMINAPI_URL", "https://admin.hyperus.team/adminapi") + self.pgs_adminapi_password = os.environ.get("PGS_ADMINAPI_PASSWORD") + self.pgs_adminapi_tenant = os.environ.get("PGS_ADMINAPI_TENANT", "default") + self.pgs_adminapi_username = os.environ.get("PGS_ADMINAPI_USERNAME", "admin@hyperus.team") + + def pgs_auth(self) -> None: + pgs_credentials = {"username": self.pgs_adminapi_username, "password": self.pgs_adminapi_password} + result = requests.post(url=f"{self.pgs_adminapi_address}/auth", data=pgs_credentials, verify=False) + if result.status_code == 200: + self.pgs_adminapi_token = result.json()["token"] + else: + raise ConnectionRefusedError("Authentication failed") + self.__pgs_adminapi_header = {"Authorization": self.pgs_adminapi_token} + + def get_ipa_groups(self) -> dict: + ipa_groups_formated = {} + ipa_client = freeipa.ClientMeta(host=self.ipa_address, verify_ssl=False) + ipa_client.login(self.ipa_username, self.ipa_password) + ipa_groups_find = ipa_client.group_find() + ipa_client.logout() + ipa_groups_find = list(filter( lambda ipa_group: "member_user" in ipa_group.keys(), ipa_groups_find["result"] )) + for ipa_group in ipa_groups_find: + ipa_group_name = "".join(ipa_group[self.ipa_group_attr]) + ipa_groups_formated[ipa_group_name] = ["{}@hyperus.team".format(user) for user in ipa_group["member_user"]] + return ipa_groups_formated + + def get_pgs_groups(self) -> dict: + pgs_groups_formated = {} + response = requests.get(url=f"{self.pgs_adminapi_address}/tenants/{self.pgs_adminapi_tenant}/groups", headers=self.__pgs_adminapi_header, verify=False) + if response.status_code == 200: + pgs_groups_finded = response.json()["groups"] + else: + raise Exception("PGS API: can't get groups list") + for pgs_group in pgs_groups_finded: + pgs_groups_formated[pgs_group["name"]] = {} + pgs_groups_formated[pgs_group["name"]]["id"] = pgs_group["id"] + pgs_groups_formated[pgs_group["name"]]["members"] = ["{}".format(user["username"]) for user in pgs_group["users"]] + return pgs_groups_formated + + def get_pgs_users(self) -> dict: + pgs_users_formated = {} + response = requests.get(url=f"{self.pgs_adminapi_address}/tenants/{self.pgs_adminapi_tenant}/users", headers=self.__pgs_adminapi_header, verify=False) + if response.status_code == 200: + pgs_users_finded = response.json()["users"] + else: + raise Exception("PGS API: can't get users list") + for pgs_user in pgs_users_finded: + pgs_users_formated[pgs_user["username"]] = pgs_user["id"] + return pgs_users_formated + + def compare_groups(self, source={}, destination={}, pgs_users={}) -> None: + if not source: + source = self.get_ipa_groups() + if not destination: + destination = self.get_pgs_groups() + if not pgs_users: + pgs_users = self.get_pgs_users() + + to_create_groups = list(filter(lambda group: group not in destination, source)) + to_delete_groups = list(filter(lambda group: group not in source, destination)) + for group in to_create_groups: + group_members = [pgs_users[username] for username in source[group]] + self.create_pgs_group(group, group_members) + for group in to_delete_groups: + self.delete_pgs_group(destination[group]["id"]) + + def compare_members(self, source={}, destination={}, pgs_users={}) -> None: + if not source: + source = self.get_ipa_groups() + if not destination: + destination = self.get_pgs_groups() + if not pgs_users: + pgs_users = self.get_pgs_users() + + for group in source: + to_create_membership = list(filter( lambda member: member not in destination[group]["members"], source[group] )) + to_remove_membership = list(filter( lambda member: member not in source[group], destination[group]["members"] )) + if len(to_create_membership) > 0: + group_members = [pgs_users[username] for username in to_create_membership] + response = self.set_pgs_group_members("POST", destination[group]["id"], group_members) + if response.status_code == 200: + print("Users has been added to group <{}>".format(group)) + else: + raise Exception("Error during adding members to group <{}>".format(group)) + if len(to_remove_membership) > 0: + group_members = [pgs_users[username] for username in to_remove_membership] + response = self.set_pgs_group_members("DELETE", destination[group]["id"], group_members) + if response.status_code == 200: + print("Users has been removed from group <{}>".format(group)) + else: + raise Exception("Error during removing members from group <{}>".format(group)) + + def create_pgs_group(self, pgs_group_name, pgs_users={}): + pgs_group_data = {"name": pgs_group_name, "users": pgs_users} + response = requests.post(url=f"{self.pgs_adminapi_address}/tenants/{self.pgs_adminapi_tenant}/groups", headers=self.__pgs_adminapi_header, data=pgs_group_data, verify=False) + if response.status_code == 200: + print("Group <{}> was created".format(pgs_group_name)) + else: + raise Exception("Error during creation group <{}>".format(pgs_group_name)) + return response + + def delete_pgs_group(self, pgs_group_name): + response = requests.delete(url=f"{self.pgs_adminapi_address}/tenants/{self.pgs_adminapi_tenant}/groups/{pgs_group_name}", headers=self.__pgs_adminapi_header, verify=False) + if response.status_code == 200: + print("Group <{}> was deleted".format(pgs_group_name)) + else: + raise Exception("Error during removing group <{}>".format(pgs_group_name)) + return response + + def set_pgs_group_members(self, method, pgs_group_id, pgs_users={}): + pgs_data = {"users": pgs_users} + response = requests.request(method, f"{self.pgs_adminapi_address}/tenants/{self.pgs_adminapi_tenant}/groups/{pgs_group_id}/users", headers=self.__pgs_adminapi_header, data=pgs_data, verify=False) + return response + + +if __name__ == "__main__": + sync_manager = GroupSyncPGS() + sync_manager.pgs_auth() + pgs_users = sync_manager.get_pgs_users() + ipa_groups = sync_manager.get_ipa_groups() + pgs_groups = sync_manager.get_pgs_groups() + sync_manager.compare_groups(ipa_groups, pgs_groups, pgs_users) + sync_manager.compare_members(ipa_groups, pgs_users=pgs_users) diff --git a/myoffice_projects/co_scripts/pgs_ldap_provider/README.md b/myoffice_projects/co_scripts/pgs_ldap_provider/README.md new file mode 100644 index 0000000..b691b25 --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_ldap_provider/README.md @@ -0,0 +1,20 @@ +# PGS LDAP PROVIDER + +Провайдер для KeyCloak (компонент PGS) отвечающий за синхронизацию пользователей по LDAP. + +## Протестирован +- PGS 2.4 +- PGS 2.5 +- PGS 2.6 + +## Изменения относительно PGS +- Добавлена возможность указания LDAP атрибутов для полей ФИО +- Добавлена возможность указания дополнительных атрибутов (город, должность, департамент) +- Исправлено заполнение резервной почты тем же адресом что и основная +- Исправлено поведение, при котором после внесения изменений в провайдер приходилось удалять и заново добавлять синхронизировать пользователей +- При изменении маппинга атрибутов не требуется пересоздание провайдера + +## Сборка +- Установить Maven +- Выполнить `mvn clean package` +- В каталоге `target` будет собранный `pgs-ldap-provider.jar` diff --git a/myoffice_projects/co_scripts/pgs_ldap_provider/pom.properties b/myoffice_projects/co_scripts/pgs_ldap_provider/pom.properties new file mode 100644 index 0000000..10b7c6a --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_ldap_provider/pom.properties @@ -0,0 +1,5 @@ +#Generated by Maven +#Wed Apr 26 16:49:11 UTC 2023 +groupId=team.hyperus +artifactId=pgs-ldap-provider +version=0.0.1 diff --git a/myoffice_projects/co_scripts/pgs_ldap_provider/pom.xml b/myoffice_projects/co_scripts/pgs_ldap_provider/pom.xml new file mode 100644 index 0000000..dde7b1f --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_ldap_provider/pom.xml @@ -0,0 +1,99 @@ + + 4.0.0 + team.hyperus + pgs-ldap-provider + 0.0.1 + jar + + UTF-8 + 1.8 + 1.8 + 13.0.1 + + + + + org.keycloak + keycloak-core + ${keycloak.version} + provided + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + provided + + + + org.keycloak + keycloak-server-spi-private + ${keycloak.version} + provided + + + + org.keycloak + keycloak-kerberos-federation + ${keycloak.version} + provided + + + + org.keycloak + keycloak-ldap-federation + ${keycloak.version} + provided + + + + org.jboss.resteasy + resteasy-jaxrs + 3.9.0.Final + provided + + + + org.jboss.logging + jboss-logging + 3.4.1.Final + provided + + + + junit + junit + 4.12 + provided + + + + org.jboss.spec.javax.transaction + jboss-transaction-api_1.2_spec + 1.1.1.Final + provided + + + + + org.jboss.spec.javax.ejb + jboss-ejb-api_3.2_spec + 2.0.0.Final + + + + + pgs-ldap-provider + + + maven-assembly-plugin + + + jar-with-dependencies + + + + + + + diff --git a/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/java/team/hyperus/EuclidConnector.java b/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/java/team/hyperus/EuclidConnector.java new file mode 100644 index 0000000..a111fbc --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/java/team/hyperus/EuclidConnector.java @@ -0,0 +1,54 @@ +package team.hyperus; + +import java.io.IOException; +import java.net.URI; +import java.net.URLEncoder; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.stream.Collectors; +import org.jboss.logging.Logger; + +public class EuclidConnector { + private static final String EUCLID_URL = "http://euclid:8852"; + private static final String EUCLID_BASE_LOGIN = System.getenv("KEYCLOAK_USER"); + private static final String EUCLID_BASE_PASS = System.getenv("KEYCLOAK_PASSWORD"); + private static final String URL_ENC_HEADER = "application/x-www-form-urlencoded"; + private String usersURL; + private static final Logger logger = Logger.getLogger(EuclidConnector.class); + private static final HttpClient httpClient = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .connectTimeout(Duration.ofSeconds(60L)).build(); + + public EuclidConnector(String realm) { + this.usersURL = String.format("%s/tenants/%s/users/", EUCLID_URL, realm); + } + + private String constructAuthJSON() { + return String.format("{\"login\": \"%s\", \"password\": \"%s\"}", EUCLID_BASE_LOGIN, EUCLID_BASE_PASS); + } + + public void createUser(String id, String username, String quota, String recoveryEmail) { + HashMap userParams = new HashMap<>(); + userParams.put("id", id); + userParams.put("username", username); + userParams.put("email", username); + userParams.put("quota", quota); + userParams.put("recovery_email", recoveryEmail); + userParams.put("basic_auth", constructAuthJSON()); + String requestBody = userParams.entrySet().stream() + .map(e -> e.getKey() + "=" + URLEncoder.encode(e.getValue(), StandardCharsets.UTF_8)) + .collect(Collectors.joining("&")); + HttpRequest request = HttpRequest.newBuilder().POST(HttpRequest.BodyPublishers.ofString(requestBody)) + .uri(URI.create(this.usersURL)).headers("Content-Type", URL_ENC_HEADER) + .build(); + try { + httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (InterruptedException | IOException e) { + logger.infof("Can't create user in euclid %s", e.getMessage()); + } + } +} diff --git a/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/java/team/hyperus/PgsStorageProvider.java b/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/java/team/hyperus/PgsStorageProvider.java new file mode 100644 index 0000000..a9f737d --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/java/team/hyperus/PgsStorageProvider.java @@ -0,0 +1,146 @@ +package team.hyperus; + +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; +import javax.ejb.Remove; +import org.jboss.logging.Logger; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.storage.StorageId; +import org.keycloak.storage.adapter.InMemoryUserAdapter; +import org.keycloak.storage.ldap.LDAPStorageProvider; +import org.keycloak.storage.ldap.LDAPUtils; +import org.keycloak.storage.ldap.idm.model.LDAPObject; +import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore; +import org.keycloak.storage.ldap.mappers.LDAPMappersComparator; +import org.keycloak.storage.ldap.mappers.LDAPStorageMapper; + +public class PgsStorageProvider extends LDAPStorageProvider { + protected Long quota = 0L; + protected String domain = ""; + private LDAPMappersComparator ldapMappersComparator; + private static final Logger logger = Logger.getLogger(PgsStorageProvider.class); + + public PgsStorageProvider(PgsStorageProviderFactory factory, KeycloakSession session, ComponentModel model, LDAPIdentityStore ldapIdentityStore) { + super(factory, session, model, ldapIdentityStore); + } + + @Override + protected UserModel importUserFromLDAP(KeycloakSession session, RealmModel realm, LDAPObject ldapUser) { + EuclidConnector euclidClient = new EuclidConnector(realm.getName()); + List groups = (List)realm.getGroupsStream().collect(Collectors.toList()); + if (this.quota == 0L) { + Iterator var6 = groups.iterator(); + while(var6.hasNext()) { + GroupModel gr = (GroupModel)var6.next(); + if (gr.getAttributes().containsKey("default")) { + this.quota = Long.parseLong((String)((List)gr.getAttributes().get("quota")).get(0)); + this.domain = gr.getName(); + break; + } + } + } + String tmp = ""; + if (!ldapUser.getAttributeAsString(this.ldapIdentityStore.getConfig().getUsernameLdapAttribute()).contains("@")) { + tmp = ldapUser.getAttributeAsString(this.ldapIdentityStore.getConfig().getUsernameLdapAttribute()) + "@" + this.domain; + Set attributeSet = new HashSet(); + attributeSet.add(tmp); + ldapUser.setAttribute(this.ldapIdentityStore.getConfig().getUsernameLdapAttribute(), attributeSet); + } + + String ldapUsername = LDAPUtils.getUsername(ldapUser, this.ldapIdentityStore.getConfig()); + if (!ldapUsername.contains("@")) { + ldapUsername = ldapUsername + "@" + this.domain; + } + HashSet imported; + if (ldapUser.getAttributeAsString("sn") == null || ldapUser.getAttributeAsString("sn").equals("")) { + imported = new HashSet(); + imported.add(ldapUsername.split("@")[0]); + ldapUser.setAttribute("sn", imported); + } + + if (ldapUser.getAttributeAsString("mail") == null || ldapUser.getAttributeAsString("mail").equals("")) { + imported = new HashSet(); + imported.add(ldapUsername); + ldapUser.setAttribute("mail", imported); + } + + LDAPUtils.checkUuid(ldapUser, this.ldapIdentityStore.getConfig()); + imported = null; + UserModel finalImported; + if (this.model.isImportEnabled()) { + UserModel existingLocalUser = session.userLocalStorage().searchForUserByUserAttributeStream(realm, "LDAP_ID", ldapUser.getUuid()).findFirst().orElse(null); + if (existingLocalUser != null) { + finalImported = existingLocalUser; + session.userCache().evict(realm, existingLocalUser); + } else { + finalImported = session.userLocalStorage().addUser(realm, ldapUsername); + } + } else { + InMemoryUserAdapter adapter = new InMemoryUserAdapter(session, realm, (new StorageId(this.model.getId(), ldapUsername)).getId()); + adapter.addDefaults(); + finalImported = adapter; + } + + finalImported.setEnabled(true); + this.ldapMappersComparator = new LDAPMappersComparator(this.getLdapIdentityStore().getConfig()); + realm.getComponentsStream(this.model.getId(), LDAPStorageMapper.class.getName()).sorted(this.ldapMappersComparator.sortDesc()).forEachOrdered((mapperModel) -> { + if (logger.isTraceEnabled()) { + logger.tracef("Using mapper %s during import user from LDAP", mapperModel); + } + + LDAPStorageMapper ldapMapper = this.mapperManager.getMapper(mapperModel); + ldapMapper.onImportUserFromLDAP(ldapUser, finalImported, realm, true); + }); + String userDN = ldapUser.getDn().toString(); + if (this.model.isImportEnabled()) { + finalImported.setFederationLink(this.model.getId()); + } + finalImported.setSingleAttribute("LDAP_ID", ldapUser.getUuid()); + finalImported.setSingleAttribute("LDAP_ENTRY_DN", userDN); + finalImported.setSingleAttribute("quota", this.quota.toString()); + finalImported.setSingleAttribute("recovery_email", (ldapUser.getUuid() + "@not-set-recovery.mail")); + finalImported.setSingleAttribute("is_admin_user", "0"); + finalImported.setSingleAttribute("eula_accept_required", "0"); + if (!this.model.get("cnSplitNames", false)){ + finalImported.setFirstName(ldapUser.getAttributeAsString(this.model.get("fnLDAPAttribute"))); + finalImported.setLastName(ldapUser.getAttributeAsString(this.model.get("lnLDAPAttribute"))); + } else { + String cn = ldapUser.getAttributeAsString("cn"); + finalImported.setLastName(ldapUser.getAttributeAsString("sn")); + if (finalImported.getLastName() == null) { + finalImported.setLastName(ldapUsername.split("@")[0]); + } + if ((cn.split(" ")).length == 3) { + finalImported.setFirstName(cn.split(" ")[1]); + finalImported.setSingleAttribute("middle_name", cn.split(" ")[2]); + finalImported.setLastName(cn.split(" ")[0]); + } + if (finalImported.getFirstName() == null) { + finalImported.setFirstName(ldapUsername.split("@")[0]); + } + } + + if (this.getLdapIdentityStore().getConfig().isTrustEmail()) { + ((UserModel)finalImported).setEmailVerified(true); + } + + UserModel proxy = this.proxy(realm, (UserModel)finalImported, ldapUser, false); + euclidClient.createUser(((UserModel)finalImported).getId(), ((UserModel)finalImported).getUsername(), Long.toString(this.quota), this.replaceDomain("admin", this.domain)); + return proxy; + } + + protected String replaceDomain(String username, String newDomain) { + return username.split("@")[0] + "@" + newDomain; + } + + @Remove + public void close() { + } +} diff --git a/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/java/team/hyperus/PgsStorageProviderFactory.java b/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/java/team/hyperus/PgsStorageProviderFactory.java new file mode 100644 index 0000000..0b6ab62 --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/java/team/hyperus/PgsStorageProviderFactory.java @@ -0,0 +1,192 @@ +package team.hyperus; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import org.keycloak.Config; +import org.keycloak.component.ComponentModel; +import org.keycloak.models.GroupModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.KeycloakSessionTask; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.provider.ServerInfoAwareProviderFactory; +import org.keycloak.storage.UserStorageProvider; +import org.keycloak.storage.ldap.LDAPIdentityStoreRegistry; +import org.keycloak.storage.ldap.LDAPStorageProvider; +import org.keycloak.storage.ldap.LDAPStorageProviderFactory; +import org.keycloak.storage.ldap.LDAPUtils; +import org.keycloak.storage.ldap.idm.model.LDAPObject; +import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore; +import org.keycloak.storage.ldap.mappers.LDAPConfigDecorator; +import org.keycloak.storage.ldap.mappers.LDAPStorageMapper; +import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapper; +import org.keycloak.storage.ldap.mappers.UserAttributeLDAPStorageMapperFactory; +import org.keycloak.storage.user.SynchronizationResult; + +public class PgsStorageProviderFactory extends LDAPStorageProviderFactory implements ServerInfoAwareProviderFactory { + private LDAPIdentityStoreRegistry ldapStoreRegistry; + + @Override + public String getId() { + return "pgsldapnew"; + } + + public String getName() { + return "PgsLDAPNew"; + } + + @Override + public LDAPStorageProvider create(KeycloakSession session, ComponentModel model) { + Map configDecorators = getLDAPConfigDecorators(session, model); + + LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(session, model, + configDecorators); + return new PgsStorageProvider(this, session, model, ldapIdentityStore); + } + + public List getConfigProperties() { + List props = new LinkedList<>(); + props.add(new ProviderConfigProperty("vendor", "LDAP Vendor", "ActiveDirectory, RHDS", "List", "rhds", "rhds", "ad")); + props.add(new ProviderConfigProperty("connectionUrl", "LDAP URL", "URL address for LDAP. eg: ldap://10.0.0.1", "String", "ldap://")); + props.add(new ProviderConfigProperty("usersDn", "Base search DN", "DN for searching users", "String", "")); + props.add(new ProviderConfigProperty("userObjectClasses", "Object classes for users", "AD: person, organizationalPerson, user || RHDS: inetOrgPerson, organizationalPerson", "String", "inetOrgPerson, organizationalPerson")); + props.add(new ProviderConfigProperty("bindDn", "Bind DN account", "Username or full DN to bind account", "String", "")); + props.add(new ProviderConfigProperty("bindCredential", "Bind account password", "Password of bind account", "Password", "", true)); + props.add(new ProviderConfigProperty("uuidLDAPAttribute", "UUID attribute", "Unique attribute by user", "String", "uid")); + props.add(new ProviderConfigProperty("usernameLDAPAttribute", "Username attribute", "Username LDAP attribute", "String", "uid")); + props.add(new ProviderConfigProperty("cnSplitNames", "Generate names", "Split CN for First,Last and Middle names", "boolean", "false")); + props.add(new ProviderConfigProperty("fnLDAPAttribute", "First name", "LDAP Attribute", "String", "givenname")); + props.add(new ProviderConfigProperty("mnLDAPAttribute", "Middle name", "LDAP Attribute", "String", "middlename")); + props.add(new ProviderConfigProperty("lnLDAPAttribute", "Last name", "LDAP Attribute", "String", "sn")); + props.add(new ProviderConfigProperty("locLDAPAttribute", "Location", "City. LDAP Attribute", "String", "l")); + props.add(new ProviderConfigProperty("ouLDAPAttribute", "Department", "Department. LDAP Attribute", "String", "ou")); + props.add(new ProviderConfigProperty("posLDAPAttribute", "Job title", "Job title/position. LDAP Attribute", "String", "title")); + return props; + } + public ComponentModel getComponentModelByName(RealmModel realm, String name){ + List components = realm.getComponents(); + for (ComponentModel component : components) { + if (component.getName().equals(name)) { + return component; + } + } + return null; + } + public ComponentModel createMapper(ComponentModel model, String name, String key){ + ComponentModel mapperModel; + mapperModel = KeycloakModelUtils.createComponentModel(name, model.getId(), + UserAttributeLDAPStorageMapperFactory.PROVIDER_ID, + LDAPStorageMapper.class.getName(), + UserAttributeLDAPStorageMapper.USER_MODEL_ATTRIBUTE, name, + UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, model.get(key), + UserAttributeLDAPStorageMapper.READ_ONLY, "true", + UserAttributeLDAPStorageMapper.ALWAYS_READ_VALUE_FROM_LDAP, "true", + UserAttributeLDAPStorageMapper.IS_MANDATORY_IN_LDAP, "false"); + return mapperModel; + } + public RealmModel updateMappers(RealmModel realm, ComponentModel model) { + ComponentModel mapperModel; + Map mappersMap = new HashMap(); + mappersMap.put("firstName", "fnLDAPAttribute"); + mappersMap.put("middle_name", "mnLDAPAttribute"); + mappersMap.put("lastName", "lnLDAPAttribute"); + mappersMap.put("city", "locLDAPAttribute"); + mappersMap.put("position", "posLDAPAttribute"); + mappersMap.put("unit", "ouLDAPAttribute"); + + if (!model.get("cnSplitNames", false)) { + for(Map.Entry mapper : mappersMap.entrySet()){ + mapperModel = getComponentModelByName(realm, mapper.getKey()); + if(mapperModel != null){ + mapperModel.put(UserAttributeLDAPStorageMapper.LDAP_ATTRIBUTE, model.get(mapper.getValue())); + realm.updateComponent(mapperModel); + } else { + realm.addComponentModel(createMapper(model, mapper.getKey(), mapper.getValue())); + } + } + } else { + for(Map.Entry mapper : mappersMap.entrySet()){ + mapperModel = getComponentModelByName(realm, mapper.getKey()); + if(mapperModel != null){ + realm.removeComponent(mapperModel); + } + } + realm.addComponentModel(createMapper(model, "city", "locLDAPAttribute")); + realm.addComponentModel(createMapper(model, "position", "posLDAPAttribute")); + realm.addComponentModel(createMapper(model, "unit", "ouLDAPAttribute")); + } + return (realm); + } + + public void onCreate(KeycloakSession session, RealmModel realm, ComponentModel model) { + model.getConfig().putSingle("editMode", UserStorageProvider.EditMode.UNSYNCED.name()); + realm = updateMappers(realm, model); + + super.onCreate(session, realm, model); + } + + @Override + public void onUpdate(KeycloakSession session, RealmModel realm, ComponentModel oldModel, ComponentModel newModel) { + realm = updateMappers(realm, newModel); + super.onUpdate(session, realm, oldModel, newModel); + } + + @Override + public void init(Config.Scope config) { + this.ldapStoreRegistry = new LDAPIdentityStoreRegistry(); + super.getConfigProperties(); + } + + protected SynchronizationResult importLdapUsers(KeycloakSessionFactory sessionFactory, final String realmId, final ComponentModel fedModel, final List ldapUsers) { + try { + KeycloakModelUtils.runJobInTransaction(sessionFactory, new KeycloakSessionTask() { + public void run(KeycloakSession session) { + Long quota = Long.valueOf(0L); + String domain = ""; + RealmModel currentRealm = session.realms().getRealm(realmId); + LDAPStorageProvider ldapFedProvider = (LDAPStorageProvider)session.getProvider(UserStorageProvider.class, fedModel); + List groups = (List)currentRealm.getGroupsStream().collect(Collectors.toList()); + String usernameAttr = ldapFedProvider.getLdapIdentityStore().getConfig().getUsernameLdapAttribute(); + if (quota.longValue() == 0L) + for (GroupModel gr : groups) { + if (gr.getAttributes().containsKey("default")) { + quota = Long.valueOf(Long.parseLong(((List)gr.getAttributes().get("quota")).get(0))); + domain = gr.getName(); + break; + } + } + for (LDAPObject ldapUser : ldapUsers) { + String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig()); + if (!ldapUsername.contains("@")) { + ldapUsername = ldapUsername + "@" + domain; + } else { + ldapUsername = ldapUsername.split("@")[0] + domain; + } + Set set = new HashSet<>(); + set.add(ldapUsername); + ldapUser.setAttribute("mail", set); + ldapUser.setAttribute(usernameAttr, set); + } + } + }); + } catch (Exception exception) {} + return super.importLdapUsers(sessionFactory, realmId, fedModel, ldapUsers); + } + + @Override + public Map getOperationalInfo() { + Map ret = new LinkedHashMap<>(); + ret.put("custom-ldap", "pgsldapnew"); + return ret; + } + +} diff --git a/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/resources/META-INF/jboss-deployment-structure.xml b/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/resources/META-INF/jboss-deployment-structure.xml new file mode 100644 index 0000000..7d4b2ef --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/resources/META-INF/jboss-deployment-structure.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory new file mode 100644 index 0000000..2f7874c --- /dev/null +++ b/myoffice_projects/co_scripts/pgs_ldap_provider/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory @@ -0,0 +1,2 @@ +# SPI class implementation +team.hyperus.PgsStorageProviderFactory diff --git a/myoffice_projects/house_configs/autoconfig_psn.conf b/myoffice_projects/house_configs/autoconfig_psn.conf new file mode 100644 index 0000000..d4d8ac2 --- /dev/null +++ b/myoffice_projects/house_configs/autoconfig_psn.conf @@ -0,0 +1,94 @@ +https://autoconfig-app.avroid.tech, +https://grpc-app.avroid.tech, +http://autoconfig.app.avroid.tech, +https://app.avroid.tech/autodiscover, +https://app.avroid.tech/Autodiscover +{ + + tls /etc/pki/tls/certs/bundle_server.crt /etc/pki/tls/private/server.nopass.key + + grpcmeta + + autodiscover { + client_type auto + clients_configuration { + display_name "New Cloud Tech Mail Subsystem" + display_short_name NCT + smtp_authentication password-cleartext + smtp_uri smtp-app.avroid.tech + smtp_port 587 + smtp_ssl STARTTLS + imap_authentication password-cleartext + imap_uri imap-app.avroid.tech + imap_port 143 + imap_ssl STARTTLS + addressbook_uri https://carddav-app.avroid.tech:6787 + calendar_uri https://caldav-app.avroid.tech:6787 + ldap_uri ldap://ldap-app.avroid.tech:389 + grpc_uri grpc-app.avroid.tech + grpc_port 3142 + grpc_ssl SSL + http_api_uri api-app.avroid.tech + http_api_port 443 + http_api_ssl SSL + documentation_uri https://mail-app.avroid.tech/info + web_mail_login_page_uri https://mail-app.avroid.tech + } + dafnis { + balancer_endpoints { + hydra.ucs-infra-1.installation.example.net:50053 + } + compression none + load_balanced true + request_timeout 10s + service_name dafnis + use_tls true + use_tls_balancer true + } + erakles { + balancer_endpoints { + hydra.ucs-infra-1.installation.example.net:50053 + } + compression none + load_balanced true + request_timeout 10s + service_name erakles + use_tls true + use_tls_balancer true + } + minos { + balancer_endpoints { + hydra.ucs-infra-1.installation.example.net:50053 + } + compression none + load_balanced true + request_timeout 10s + service_name minos + use_tls true + use_tls_balancer true + } + perseus { + balancer_endpoints { + hydra.ucs-infra-1.installation.example.net:50053 + } + compression none + load_balanced true + request_timeout 10s + service_name perseus + use_tls true + use_tls_balancer true + } + tls_settings { + ca_file /etc/pki/tls/certs/ucs-infra-1.installation.example.net-main-ca.pem + client_auth_type require_and_verify_client_cert + client_cert_file /etc/pki/tls/certs/house.ucs-infra-1.installation.example.net-main-client.pem + key_file /etc/pki/tls/private/house.ucs-infra-1.installation.example.net-main-key.pem + tls_min_version tls1_2 + } + } + logging + + request-report + + tracing +} diff --git a/myoffice_projects/house_configs/default.conf b/myoffice_projects/house_configs/default.conf new file mode 100644 index 0000000..745f56b --- /dev/null +++ b/myoffice_projects/house_configs/default.conf @@ -0,0 +1,107 @@ +https://app.avroid.tech/.well-known/caldav { + tls /etc/pki/tls/certs/bundle_server.crt /etc/pki/tls/private/server.nopass.key + + + redir 301 { + / https://caldav-app.avroid.tech:6787/.well-known/caldav + } + + logging + + request-report + +} + +https://app.avroid.tech/.well-known/carddav { + tls /etc/pki/tls/certs/bundle_server.crt /etc/pki/tls/private/server.nopass.key + + + redir 301 { + / https://carddav-app.avroid.tech:6787/.well-known/carddav + } + + logging + + request-report + +} + +https://app.avroid.tech, https://www.app.avroid.tech { + tls /etc/pki/tls/certs/bundle_server.crt /etc/pki/tls/private/server.nopass.key + + + redir 301 { + / https://mail-app.avroid.tech{uri} + } + + logging + + request-report + +} + +http://app.avroid.tech, http://www.app.avroid.tech { + + tls off + + redir 301 { + / https://mail-app.avroid.tech{uri} + } + + logging + + request-report + +} + +https://avroid.tech, +https://www.avroid.tech, +https://relay-app.avroid.tech, +https://www.relay-app.avroid.tech, +https://mx-app.avroid.tech, +https://www.mx-app.avroid.tech, +https://smtp-app.avroid.tech, +https://www.smtp-app.avroid.tech, +https://secured-app.avroid.tech, +https://www.secured-app.avroid.tech, +https://imap-app.avroid.tech, +https://www.imap-app.avroid.tech, +https://settings-app.avroid.tech, +https://www.settings-app.avroid.tech, +https://info-app.avroid.tech, +https://www.info-app.avroid.tech, +https://caldav-app.avroid.tech, +https://www.caldav-app.avroid.tech, +https://carddav-app.avroid.tech, +https://www.carddav-app.avroid.tech, +https://squadus-app.avroid.tech, +https://www.squadus-app.avroid.tech, +https://www.coappacts-app.avroid.tech, +https://coappacts-app.avroid.tech { + tls /etc/pki/tls/certs/bundle_server.crt /etc/pki/tls/private/server.nopass.key + + + redir 301 { + / https://avroid.ru{uri} + } + + logging + + request-report + +} + +https://www.mail.avroid.tech, +https://mail.avroid.tech { + tls /etc/pki/tls/certs/bundle_server.crt /etc/pki/tls/private/server.nopass.key + + + redir 301 { + / https://app.avroid.tech{uri} + } + + logging + + request-report + +} diff --git a/myoffice_projects/import_mailion/export_groups_from_ldap.py b/myoffice_projects/import_mailion/export_groups_from_ldap.py new file mode 100755 index 0000000..d20f009 --- /dev/null +++ b/myoffice_projects/import_mailion/export_groups_from_ldap.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python3 + +import ldap +import json +import re + +def write_to_file(filename, group_list): + pattern = r'child_\d+' + + #Save group's list in JSON file + with open(filename, 'w') as file: + json.dump(group_list, file, indent=4) + + with open(filename, 'r') as file: + lines = file.readlines() + + lines = lines[1:-1] # delete first and last lines + + # Change "}," to "}" + for i in range(len(lines)): + lines[i] = lines[i].replace('},', '}') + lines[i] = re.sub(pattern, 'child', lines[i]) + + with open(filename, 'w') as file: + for line in lines: + file.write(line) + +# OpenLDAP server config +ldap_server = 'ldap://ds.avroid.tech' +ldap_port = 389 +ldap_base_dn = 'dc=avroid,dc=tech' +ldap_admin_user = 'uid=ipa,cn=users,cn=accounts,dc=avroid,dc=tech' +ldap_admin_password = '' + +# LDAP query +ldap_filter = '(objectClass=ipantgroupattrs)' + +# Connect to the LDAP server +ldap_connection = ldap.initialize(ldap_server + ':' + str(ldap_port)) + +try: + # Bind to the LDAP server using admin credentials + ldap_connection.simple_bind_s(ldap_admin_user, ldap_admin_password) + results = ldap_connection.search_s(ldap_base_dn, ldap.SCOPE_SUBTREE, ldap_filter) + + # Prepare a lists for groups + groups = [] + group_links = [] + i = 0 + # Extract groups information + for dn, entry in results: + group = {} + + # Extract and store group attributes + if ' ' not in entry.get('cn', [None])[0].decode('utf-8'): + group['correlation_id'] = entry.get('gidNumber', [None])[0].decode('utf-8') if entry.get('gidNumber') else None + group['name'] = entry.get('cn', [None])[0].decode('utf-8') if entry.get('cn') else None + group['description'] = entry.get('description', [None])[0].decode('utf-8') if entry.get('description') else None + group['email'] = group['name'] + '@' + dn.split(',')[3].split('=')[1] + '.' + dn.split(',')[4].split('=')[1] + + groups.append(group) + + # Extract and store group attributes + for child in entry.get('member', [None]): + group_link = {} + if child is not None and child.decode('utf-8').split(',')[1].split('=')[1] in ['groups', 'users']: + num = str(i) + group_link['correlation_id'] = num + group_link['parent'] = group['email'] + child = child.decode('utf-8') + group_link['child'] = child.split(',')[0].split('=')[1] + group_link['child'] += '@' + group_link['child'] += child.split(',')[3].split('=')[1] + group_link['child'] += '.' + group_link['child'] += child.split(',')[4].split('=')[1] + + group_links.append(group_link) + i += 1 + write_to_file("groups.json", groups) + write_to_file("group_links.json", group_links) + + print('Successfully exported groups.') + +except ldap.LDAPError as e: + print('LDAP Error:', e) + +finally: + # Unbind from the LDAP server + ldap_connection.unbind() diff --git a/myoffice_projects/import_mailion/import_config.json b/myoffice_projects/import_mailion/import_config.json new file mode 100644 index 0000000..8ebc209 --- /dev/null +++ b/myoffice_projects/import_mailion/import_config.json @@ -0,0 +1,27 @@ +{ + "token-name": "ucs-access-token", + "admin": { + "login": "admin.myoffice", + "password": "ideapheeF2Ru3niZ" + }, + "cox": { + "compression": "none", + "endpoint": "grpc-app.avroid.tech:3142", + "load_balanced": false, + "request_timeout": "10s", + "use_tls": true + }, + "tls_settings": { + "ca_file": "/srv/tls/certs/ucs-infra-1.installation.example.net-main-ca.pem", + "client_cert_file": "/srv/tls/certs/ministerium.ucs-infra-1.installation.example.net-main-client.pem", + "key_file": "/srv/tls/keys/ministerium.ucs-infra-1.installation.example.net-main-key.pem" + }, + "tenant_id": "c25c71b4-5f87-4d58-a38b-a504bf43585e", + "region_id": "4ba3c930-5ff9-4933-b0a9-20ff328e2fc5", + "gal_tags": [ + "05ea1eea-e273-55a6-86dd-3a932860211e" ], + "user_data_path": "user_profiles.json", + "user_data_format": "json", + "rejected_users_path": "rejected_profiles.json", + "roles": [] +} diff --git a/myoffice_projects/import_mailion/mailion_import.sh b/myoffice_projects/import_mailion/mailion_import.sh new file mode 100755 index 0000000..996818a --- /dev/null +++ b/myoffice_projects/import_mailion/mailion_import.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +pushd /root/import_mailion + ./export_groups_from_ldap.py + nct_ministerium import_groups --config settings.json + nct_ministerium import_groups_links --config settings_links.json +popd diff --git a/myoffice_projects/import_mailion/settings.json b/myoffice_projects/import_mailion/settings.json new file mode 100644 index 0000000..4467138 --- /dev/null +++ b/myoffice_projects/import_mailion/settings.json @@ -0,0 +1,27 @@ +{ + "token-name": "ucs-access-token", + "admin": { + "login": "admin.myoffice", + "password": "ideapheeF2Ru3niZ" + }, + "cox": { + "endpoint": "grpc-app.avroid.tech:3142", + "service_name": "cox", + "load_balanced": false, + "use_tls": true, + "use_tls_balancer": false, + "compression": "none" + }, + "tls_settings": { + "ca_file": "/srv/tls/certs/ucs-infra-1.installation.example.net-main-ca.pem", + "client_cert_file": "/srv/tls/certs/ministerium.ucs-infra-1.installation.example.net-main-client.pem", + "key_file": "/srv/tls/keys/ministerium.ucs-infra-1.installation.example.net-main-key.pem" + }, + "tenant_id": "c25c71b4-5f87-4d58-a38b-a504bf43585e", + "region_id": "4ba3c930-5ff9-4933-b0a9-20ff328e2fc5", + "gal_tags": [ + "05ea1eea-e273-55a6-86dd-3a932860211e" ], + "groups_data_path": "groups.json", + "groups_data_format": "json", + "rejected_groups_path": "rejected_groups.json" +} diff --git a/myoffice_projects/import_mailion/settings_links.json b/myoffice_projects/import_mailion/settings_links.json new file mode 100644 index 0000000..61d0643 --- /dev/null +++ b/myoffice_projects/import_mailion/settings_links.json @@ -0,0 +1,27 @@ +{ + "token-name": "ucs-access-token", + "admin": { + "login": "admin.myoffice", + "password": "ideapheeF2Ru3niZ" + }, + "cox": { + "endpoint": "grpc-app.avroid.tech:3142", + "service_name": "cox", + "load_balanced": false, + "use_tls": true, + "use_tls_balancer": false, + "compression": "none" + }, + "tls_settings": { + "ca_file": "/srv/tls/certs/ucs-infra-1.installation.example.net-main-ca.pem", + "client_cert_file": "/srv/tls/certs/ministerium.ucs-infra-1.installation.example.net-main-client.pem", + "key_file": "/srv/tls/keys/ministerium.ucs-infra-1.installation.example.net-main-key.pem" + }, + "tenant_id": "c25c71b4-5f87-4d58-a38b-a504bf43585e", + "region_id": "4ba3c930-5ff9-4933-b0a9-20ff328e2fc5", + "gal_tags": [ + "05ea1eea-e273-55a6-86dd-3a932860211e" ], + "group_links_data_path": "group_links.json", + "group_links_data_format": "json", + "rejected_groups_path": "rejected_groups.json" +}