add myoffice scripts
This commit is contained in:
18
myoffice_projects/README.md
Normal file
18
myoffice_projects/README.md
Normal file
@@ -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
|
||||
`
|
||||
9
myoffice_projects/co_scripts/README.md
Normal file
9
myoffice_projects/co_scripts/README.md
Normal file
@@ -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 |
|
||||
10
myoffice_projects/co_scripts/pgs_avatar_sync/Dockerfile
Normal file
10
myoffice_projects/co_scripts/pgs_avatar_sync/Dockerfile
Normal file
@@ -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}"]
|
||||
52
myoffice_projects/co_scripts/pgs_avatar_sync/README.md
Normal file
52
myoffice_projects/co_scripts/pgs_avatar_sync/README.md
Normal file
@@ -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 .. ">.");
|
||||
```
|
||||
8
myoffice_projects/co_scripts/pgs_avatar_sync/app.py
Normal file
8
myoffice_projects/co_scripts/pgs_avatar_sync/app.py
Normal file
@@ -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"])
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
flask
|
||||
flask-restful
|
||||
pip
|
||||
python-freeipa
|
||||
requests
|
||||
12
myoffice_projects/co_scripts/pgs_group_sync/Dockerfile
Normal file
12
myoffice_projects/co_scripts/pgs_group_sync/Dockerfile
Normal file
@@ -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"]
|
||||
30
myoffice_projects/co_scripts/pgs_group_sync/README.md
Normal file
30
myoffice_projects/co_scripts/pgs_group_sync/README.md
Normal file
@@ -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
|
||||
```
|
||||
@@ -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 * * * *"
|
||||
147
myoffice_projects/co_scripts/pgs_group_sync/group_sync_pgs.py
Normal file
147
myoffice_projects/co_scripts/pgs_group_sync/group_sync_pgs.py
Normal file
@@ -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)
|
||||
20
myoffice_projects/co_scripts/pgs_ldap_provider/README.md
Normal file
20
myoffice_projects/co_scripts/pgs_ldap_provider/README.md
Normal file
@@ -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`
|
||||
@@ -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
|
||||
99
myoffice_projects/co_scripts/pgs_ldap_provider/pom.xml
Normal file
99
myoffice_projects/co_scripts/pgs_ldap_provider/pom.xml
Normal file
@@ -0,0 +1,99 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<groupId>team.hyperus</groupId>
|
||||
<artifactId>pgs-ldap-provider</artifactId>
|
||||
<version>0.0.1</version>
|
||||
<packaging>jar</packaging>
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<maven.compiler.source>1.8</maven.compiler.source>
|
||||
<maven.compiler.target>1.8</maven.compiler.target>
|
||||
<keycloak.version>13.0.1</keycloak.version>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-core</artifactId>
|
||||
<version>${keycloak.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-server-spi</artifactId>
|
||||
<version>${keycloak.version}</version>
|
||||
<scope>provided</scope>
|
||||
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-server-spi-private</artifactId>
|
||||
<version>${keycloak.version}</version>
|
||||
<scope>provided</scope>
|
||||
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-kerberos-federation</artifactId>
|
||||
<version>${keycloak.version}</version>
|
||||
<scope>provided</scope>
|
||||
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.keycloak</groupId>
|
||||
<artifactId>keycloak-ldap-federation</artifactId>
|
||||
<version>${keycloak.version}</version>
|
||||
<scope>provided</scope>
|
||||
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.resteasy</groupId>
|
||||
<artifactId>resteasy-jaxrs</artifactId>
|
||||
<version>3.9.0.Final</version>
|
||||
<scope>provided</scope>
|
||||
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.logging</groupId>
|
||||
<artifactId>jboss-logging</artifactId>
|
||||
<version>3.4.1.Final</version>
|
||||
<scope>provided</scope>
|
||||
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>provided</scope>
|
||||
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.spec.javax.transaction</groupId>
|
||||
<artifactId>jboss-transaction-api_1.2_spec</artifactId>
|
||||
<version>1.1.1.Final</version>
|
||||
<scope>provided</scope>
|
||||
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.jboss.spec.javax.ejb</groupId>
|
||||
<artifactId>jboss-ejb-api_3.2_spec</artifactId>
|
||||
<version>2.0.0.Final</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
<build>
|
||||
<finalName>pgs-ldap-provider</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<artifactId>maven-assembly-plugin</artifactId>
|
||||
<configuration>
|
||||
<descriptorRefs>
|
||||
<descriptorRef>jar-with-dependencies</descriptorRef>
|
||||
</descriptorRefs>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
|
||||
</build>
|
||||
</project>
|
||||
@@ -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<String, String> 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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<GroupModel> 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() {
|
||||
}
|
||||
}
|
||||
@@ -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<ComponentModel, LDAPConfigDecorator> configDecorators = getLDAPConfigDecorators(session, model);
|
||||
|
||||
LDAPIdentityStore ldapIdentityStore = this.ldapStoreRegistry.getLdapStore(session, model,
|
||||
configDecorators);
|
||||
return new PgsStorageProvider(this, session, model, ldapIdentityStore);
|
||||
}
|
||||
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
List<ProviderConfigProperty> 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<ComponentModel> 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<String, String> mappersMap = new HashMap<String, String>();
|
||||
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<String, String> 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<String, String> 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<LDAPObject> 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<GroupModel> groups = (List<GroupModel>)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<String>)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<String> 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<String, String> getOperationalInfo() {
|
||||
Map<String, String> ret = new LinkedHashMap<>();
|
||||
ret.put("custom-ldap", "pgsldapnew");
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<jboss-deployment-structure>
|
||||
<deployment>
|
||||
<dependencies>
|
||||
<module name="org.keycloak.keycloak-core" />
|
||||
<module name="org.keycloak.keycloak-server-spi" />
|
||||
<module name="org.keycloak.keycloak-server-spi-private" />
|
||||
<module name="org.keycloak.keycloak-kerberos-federation" />
|
||||
<module name="org.keycloak.keycloak-ldap-federation" />
|
||||
<module name="org.keycloak.keycloak-model-jpa" />
|
||||
<module name="org.keycloak.keycloak-common" />
|
||||
<module name="org.keycloak.keycloak-model-infinispan" />
|
||||
<module name="org.keycloak.keycloak-services" />
|
||||
</dependencies>
|
||||
</deployment>
|
||||
</jboss-deployment-structure>
|
||||
@@ -0,0 +1,2 @@
|
||||
# SPI class implementation
|
||||
team.hyperus.PgsStorageProviderFactory
|
||||
94
myoffice_projects/house_configs/autoconfig_psn.conf
Normal file
94
myoffice_projects/house_configs/autoconfig_psn.conf
Normal file
@@ -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
|
||||
}
|
||||
107
myoffice_projects/house_configs/default.conf
Normal file
107
myoffice_projects/house_configs/default.conf
Normal file
@@ -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
|
||||
|
||||
}
|
||||
89
myoffice_projects/import_mailion/export_groups_from_ldap.py
Executable file
89
myoffice_projects/import_mailion/export_groups_from_ldap.py
Executable file
@@ -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 = '<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()
|
||||
27
myoffice_projects/import_mailion/import_config.json
Normal file
27
myoffice_projects/import_mailion/import_config.json
Normal file
@@ -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": []
|
||||
}
|
||||
7
myoffice_projects/import_mailion/mailion_import.sh
Executable file
7
myoffice_projects/import_mailion/mailion_import.sh
Executable file
@@ -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
|
||||
27
myoffice_projects/import_mailion/settings.json
Normal file
27
myoffice_projects/import_mailion/settings.json
Normal file
@@ -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"
|
||||
}
|
||||
27
myoffice_projects/import_mailion/settings_links.json
Normal file
27
myoffice_projects/import_mailion/settings_links.json
Normal file
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user