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"
+}