add myoffice scripts

This commit is contained in:
aleksandr.vodyanov
2024-04-09 10:19:48 +03:00
parent 6596117571
commit 9f2c40e688
28 changed files with 1310 additions and 0 deletions

View 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`

View File

@@ -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

View 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>

View File

@@ -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());
}
}
}

View File

@@ -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() {
}
}

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -0,0 +1,2 @@
# SPI class implementation
team.hyperus.PgsStorageProviderFactory