Add initial IPA-Tuura federation (#35467)

* Add initial federation ipatuura plugin

Closes #35325

Signed-off-by: Justin Stephenson <jstephen@redhat.com>
Signed-off-by: Stefan Guilhen <sguilhen@redhat.com>
Co-authored-by: Stefan Guilhen <sguilhen@redhat.com>
This commit is contained in:
Stefan Guilhen
2024-12-02 14:59:21 -03:00
committed by GitHub
parent ff939d6b28
commit 3c33a7180e
15 changed files with 1699 additions and 0 deletions

View File

@@ -125,6 +125,8 @@ public class Profile {
CACHE_EMBEDDED_REMOTE_STORE("Support for remote-store in embedded Infinispan caches", Type.EXPERIMENTAL),
USER_EVENT_METRICS("Collect metrics based on user events", Type.PREVIEW),
IPA_TUURA_FEDERATION("IPA-Tuura user federation provider", Type.EXPERIMENTAL)
;
private final Type type;

View File

@@ -86,6 +86,10 @@
<groupId>org.keycloak</groupId>
<artifactId>keycloak-sssd-federation</artifactId>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-ipatuura-federation</artifactId>
</dependency>
<!-- Built-in Authorization Policy Providers -->
<dependency>

View File

@@ -0,0 +1,64 @@
<!--
~ Copyright 2024 Red Hat, Inc. and/or its affiliates
~ and other contributors as indicated by the @author tags.
~
~ Licensed under the Apache License, Version 2.0 (the "License");
~ you may not use this file except in compliance with the License.
~ You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>999.0.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-ipatuura-federation</artifactId>
<name>Keycloak Ipatuura Federation</name>
<description />
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-server-spi-private</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-model-storage-private</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-services</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,414 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.ipatuura_user_spi;
import com.fasterxml.jackson.databind.JsonNode;
import org.jboss.logging.Logger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.broker.provider.util.SimpleHttp.Response;
import org.keycloak.ipatuura_user_spi.schemas.SCIMSearchRequest;
import org.keycloak.ipatuura_user_spi.schemas.SCIMUser;
import org.keycloak.models.UserModel;
public class Ipatuura {
private static final Logger logger = Logger.getLogger(Ipatuura.class);
private final ComponentModel model;
public static final String SCHEMA_CORE_USER = "urn:ietf:params:scim:schemas:core:2.0:User";
public static final String SCHEMA_API_MESSAGES_SEARCHREQUEST = "urn:ietf:params:scim:api:messages:2.0:SearchRequest";
String sessionid_cookie;
String csrf_cookie;
String csrf_value;
Boolean logged_in = false;
private final KeycloakSession session;
public Ipatuura(KeycloakSession session, ComponentModel model) {
this.model = model;
this.session = session;
}
private void parseSetCookie(Response response) throws IOException {
List<String> setCookieHeaders = response.getHeader("Set-Cookie");
for (String h : setCookieHeaders) {
String[] kv = h.split(";");
for (String s : kv) {
if (s.contains("csrftoken")) {
/* key=value */
csrf_cookie = s;
csrf_value = s.substring(s.lastIndexOf("=") + 1);
} else if (s.contains("sessionid")) {
/* key=value */
sessionid_cookie = s;
csrf_cookie += String.format("; %s", sessionid_cookie);
}
}
}
}
public Integer csrfAuthLogin() {
Response response;
/* Get inputs */
String server = model.getConfig().getFirst("scimurl");
String username = model.getConfig().getFirst("loginusername");
String password = model.getConfig().getFirst("loginpassword");
/* Execute GET to get initial csrftoken */
String url = String.format("https://%s%s", server, "/admin/login/");
try {
response = SimpleHttp.doGet(url, session).asResponse();
parseSetCookie(response);
response.close();
} catch (Exception e) {
logger.errorv("Error: {0}", e.getMessage());
throw new RuntimeException(e);
}
/* Perform login POST */
try {
/* Here we retrieve the Response sessionid and csrftoken cookie */
response = SimpleHttp.doPost(url, session).header("X-CSRFToken", csrf_value).header("Cookie", csrf_cookie)
.header("referer", url).param("username", username).param("password", password).asResponse();
parseSetCookie(response);
response.close();
} catch (Exception e) {
logger.error("Error: " + e.getMessage());
throw new RuntimeException(e);
}
this.logged_in = true;
return 0;
}
public boolean isValid(String username, String password) {
if (!this.logged_in) {
this.csrfAuthLogin();
}
/* Build URL */
String server = model.getConfig().getFirst("scimurl");
String endpointurl = String.format("https://%s/creds/simple_pwd", server);
logger.debugv("Sending POST request to {0}", endpointurl);
SimpleHttp simpleHttp = SimpleHttp.doPost(endpointurl, session).header("X-CSRFToken", this.csrf_value)
.header("Cookie", this.csrf_cookie).header("SessionId", sessionid_cookie).header("referer", endpointurl)
.param("username", username).param("password", password);
try (Response response = simpleHttp.asResponse()){
JsonNode result = response.asJson();
return (result.get("result").get("validated").asBoolean());
} catch (Exception e) {
logger.debugv("Failed to authenticate user {0}: {1}", username, e);
return false;
}
}
public String gssAuth(String spnegoToken) {
String server = model.getConfig().getFirst("scimurl");
String endpointurl = String.format("https://%s/bridge/login_kerberos/", server);
logger.debugv("Sending POST request to {0}", endpointurl);
SimpleHttp simpleHttp = SimpleHttp.doPost(endpointurl, session).header("Authorization", "Negotiate " + spnegoToken)
.param("username", "");
try (Response response = simpleHttp.asResponse()) {
logger.debugv("Response status is {0}", response.getStatus());
return response.getFirstHeader("Remote-User");
} catch (Exception e) {
logger.debugv("Failed to authenticate user with GSSAPI: {0}", e.toString());
return null;
}
}
public <T> Response clientRequest(String endpoint, String method, T entity) throws Exception {
Response response = null;
if (!this.logged_in) {
this.csrfAuthLogin();
}
/* Build URL */
String server = model.getConfig().getFirst("scimurl");
String endpointurl;
if (endpoint.contains("domain")) {
endpointurl = String.format("https://%s/domains/v1/%s/", server, endpoint);
} else {
endpointurl = String.format("https://%s/scim/v2/%s", server, endpoint);
}
logger.debugv("Sending {0} request to {1}", method, endpointurl);
try {
switch (method) {
case "GET":
response = SimpleHttp.doGet(endpointurl, session).header("X-CSRFToken", csrf_value)
.header("Cookie", csrf_cookie).header("SessionId", sessionid_cookie).asResponse();
break;
case "DELETE":
response = SimpleHttp.doDelete(endpointurl, session).header("X-CSRFToken", csrf_value)
.header("Cookie", csrf_cookie).header("SessionId", sessionid_cookie).header("referer", endpointurl)
.asResponse();
break;
case "POST":
/* Header is needed for domains endpoint only, but use it here anyway */
response = SimpleHttp.doPost(endpointurl, session).header("X-CSRFToken", this.csrf_value)
.header("Cookie", this.csrf_cookie).header("SessionId", sessionid_cookie)
.header("referer", endpointurl).json(entity).asResponse();
break;
case "PUT":
response = SimpleHttp.doPut(endpointurl, session).header("X-CSRFToken", this.csrf_value)
.header("SessionId", sessionid_cookie).header("Cookie", this.csrf_cookie).json(entity).asResponse();
break;
default:
logger.warn("Unknown HTTP method, skipping");
break;
}
} catch (Exception e) {
throw new Exception();
}
/* Caller is responsible for executing .close() */
return response;
}
private SCIMSearchRequest setupSearch(String username, String attribute) {
List<String> schemas = new ArrayList<String>();
SCIMSearchRequest search = new SCIMSearchRequest();
String filter;
schemas.add(SCHEMA_API_MESSAGES_SEARCHREQUEST);
search.setSchemas(schemas);
filter = String.format("%s eq \"%s\"", attribute, username);
search.setFilter(filter);
logger.debugv("filter: {0}", filter);
logger.debugv("Schema: {0}", SCHEMA_API_MESSAGES_SEARCHREQUEST);
return search;
}
private SCIMUser getUserByAttr(String username, String attribute) {
SCIMSearchRequest newSearch = setupSearch(username, attribute);
String usersSearchUrl = "Users/.search";
SCIMUser user = null;
Response response;
try {
response = clientRequest(usersSearchUrl, "POST", newSearch);
user = response.asJson(SCIMUser.class);
response.close();
} catch (Exception e) {
logger.errorv("Error: {0}", e.getMessage());
throw new RuntimeException(e);
}
return user;
}
public SCIMUser getUserByUsername(String username) {
String attribute = "userName";
return getUserByAttr(username, attribute);
}
public SCIMUser getUserByEmail(String username) {
String attribute = "emails.value";
return getUserByAttr(username, attribute);
}
public SCIMUser getUserByFirstName(String username) {
String attribute = "name.givenName";
return getUserByAttr(username, attribute);
}
public SCIMUser getUserByLastName(String username) {
String attribute = "name.familyName";
return getUserByAttr(username, attribute);
}
public Response deleteUser(String username) {
SCIMUser userobj = getUserByUsername(username);
SCIMUser.Resource user = userobj.getResources().get(0);
String userIdUrl = String.format("Users/%s", user.getId());
Response response;
try {
response = clientRequest(userIdUrl, "DELETE", null);
} catch (Exception e) {
logger.errorv("Error: {0}", e.getMessage());
throw new RuntimeException(e);
}
return response;
}
/*
* Keycloak UserRegistrationProvider addUser() method only provides username as input, here we provide mostly dummy values
* which will be replaced by actual user input via appropriate setter methods once in the returned UserModel
*/
private SCIMUser.Resource setupUser(String username) {
SCIMUser.Resource user = new SCIMUser.Resource();
SCIMUser.Resource.Name name = new SCIMUser.Resource.Name();
SCIMUser.Resource.Email email = new SCIMUser.Resource.Email();
List<String> schemas = new ArrayList<String>();
List<SCIMUser.Resource.Email> emails = new ArrayList<SCIMUser.Resource.Email>();
List<SCIMUser.Resource.Group> groups = new ArrayList<SCIMUser.Resource.Group>();
schemas.add(SCHEMA_CORE_USER);
user.setSchemas(schemas);
user.setUserName(username);
user.setActive(true);
user.setGroups(groups);
name.setGivenName("dummyfirstname");
name.setMiddleName("");
name.setFamilyName("dummylastname");
user.setName(name);
email.setPrimary(true);
email.setType("work");
email.setValue("dummy@example.com");
emails.add(email);
user.setEmails(emails);
return user;
}
public Response createUser(String username) {
String usersUrl = "Users";
SCIMUser.Resource newUser = setupUser(username);
Response response;
try {
response = clientRequest(usersUrl, "POST", newUser);
} catch (Exception e) {
logger.errorv("Error: {0}", e.getMessage());
return null;
}
return response;
}
private void setUserAttr(SCIMUser.Resource user, String attr, String value) {
SCIMUser.Resource.Name name = user.getName();
SCIMUser.Resource.Email email = new SCIMUser.Resource.Email();
List<SCIMUser.Resource.Email> emails = new ArrayList<SCIMUser.Resource.Email>();
switch (attr) {
case UserModel.FIRST_NAME:
name.setGivenName(value);
user.setName(name);
break;
case UserModel.LAST_NAME:
name.setFamilyName(value);
user.setName(name);
break;
case UserModel.EMAIL:
email.setValue(value);
emails.add(email);
user.setEmails(emails);
break;
case UserModel.USERNAME:
/* Changing username not supported */
break;
default:
logger.debug("Unknown user attribute to set: " + attr);
break;
}
}
public Response updateUser(Ipatuura ipatuura, String username, String attr, List<String> values) {
logger.debug(String.format("Updating %s attribute for %s", attr, username));
/* Get existing user */
if (ipatuura.csrfAuthLogin() == null) {
logger.error("Error during login");
}
SCIMUser userobj = getUserByUsername(username);
SCIMUser.Resource user = userobj.getResources().get(0);
/* Modify attributes */
setUserAttr(user, attr, values.get(0));
/* Update user in SCIM */
String modifyUrl = String.format("Users/%s", user.getId());
Response response;
try {
response = clientRequest(modifyUrl, "PUT", user);
} catch (Exception e) {
logger.errorv("Error: {0}", e.getMessage());
throw new RuntimeException(e);
}
return response;
}
public boolean getActive(SCIMUser user) {
return user.getResources().get(0).getActive();
}
public String getEmail(SCIMUser user) {
return user.getResources().get(0).getEmails().get(0).getValue();
}
public String getFirstName(SCIMUser user) {
return user.getResources().get(0).getName().getGivenName();
}
public String getLastName(SCIMUser user) {
return user.getResources().get(0).getName().getFamilyName();
}
public String getUserName(SCIMUser user) {
return user.getResources().get(0).getUserName();
}
public String getId(SCIMUser user) {
return user.getResources().get(0).getId();
}
public List<String> getGroupsList(SCIMUser user) {
List<SCIMUser.Resource.Group> groups = user.getResources().get(0).getGroups();
List<String> groupnames = new ArrayList<String>();
for (SCIMUser.Resource.Group group : groups) {
logger.debug("Retrieving group: " + group.getDisplay());
groupnames.add(group.getDisplay());
}
return groupnames;
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.ipatuura_user_spi;
import org.jboss.logging.Logger;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.UserModelDelegate;
import java.io.IOException;
import java.util.List;
import org.apache.http.HttpStatus;
public class IpatuuraUserModelDelegate extends UserModelDelegate {
private static final Logger logger = Logger.getLogger(IpatuuraUserModelDelegate.class);
private ComponentModel model;
private final Ipatuura ipatuura;
public IpatuuraUserModelDelegate(Ipatuura ipatuura, UserModel delegate, ComponentModel model) {
super(delegate);
this.model = model;
this.ipatuura = ipatuura;
}
@Override
public void setAttribute(String attr, List<String> values) {
SimpleHttp.Response resp = this.ipatuura.updateUser(ipatuura, this.getUsername(), attr, values);
try {
if (resp.getStatus() != HttpStatus.SC_OK && resp.getStatus() != HttpStatus.SC_NO_CONTENT) {
logger.warn("Unexpected PUT status code returned");
resp.close();
return;
}
resp.close();
} catch (IOException e) {
logger.errorv("Error: {0}", e.getMessage());
throw new RuntimeException(e);
}
super.setAttribute(attr, values);
}
@Override
public void setSingleAttribute(String name, String value) {
super.setSingleAttribute(name, value);
}
@Override
public void setUsername(String username) {
super.setUsername(username);
}
@Override
public void setLastName(String lastName) {
super.setLastName(lastName);
}
@Override
public void setFirstName(String first) {
super.setFirstName(first);
}
@Override
public void setEmail(String email) {
super.setFirstName(email);
}
@Override
public void setEnabled(boolean enabled) {
super.setEnabled(enabled);
}
}

View File

@@ -0,0 +1,329 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.ipatuura_user_spi;
import org.jboss.logging.Logger;
import org.keycloak.component.ComponentModel;
import org.keycloak.credential.CredentialAuthentication;
import org.keycloak.credential.CredentialInput;
import org.keycloak.credential.CredentialInputValidator;
import org.keycloak.credential.UserCredentialManager;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.GroupModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.credential.PasswordCredentialModel;
import org.keycloak.storage.StorageId;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.storage.UserStoragePrivateUtil;
import org.keycloak.storage.user.ImportedUserValidation;
import org.keycloak.storage.user.UserLookupProvider;
import org.keycloak.storage.user.UserRegistrationProvider;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.ipatuura_user_spi.authenticator.IpatuuraAuthenticator;
import org.keycloak.ipatuura_user_spi.schemas.SCIMError;
import org.keycloak.ipatuura_user_spi.schemas.SCIMUser;
import org.keycloak.storage.user.UserQueryProvider;
import java.io.IOException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import org.apache.http.HttpStatus;
/**
* @author <a href="mailto:jstephen@redhat.com">Justin Stephenson</a>
* @version $Revision: 1 $
*/
public class IpatuuraUserStorageProvider implements UserStorageProvider, UserLookupProvider, CredentialInputValidator,
CredentialAuthentication, UserRegistrationProvider, UserQueryProvider, ImportedUserValidation {
protected KeycloakSession session;
protected ComponentModel model;
protected Ipatuura ipatuura;
private static final Logger logger = Logger.getLogger(IpatuuraUserStorageProvider.class);
protected final Set<String> supportedCredentialTypes = new HashSet<>();
protected IpatuuraUserStorageProviderFactory factory;
public IpatuuraUserStorageProvider(KeycloakSession session, ComponentModel model, Ipatuura ipatuura,
IpatuuraUserStorageProviderFactory factory) {
this.session = session;
this.model = model;
this.ipatuura = ipatuura;
this.factory = factory;
supportedCredentialTypes.add(PasswordCredentialModel.TYPE);
}
@Override
public UserModel getUserByEmail(RealmModel realm, String email) {
return null;
}
@Override
public UserModel getUserById(RealmModel realm, String id) {
StorageId storageId = new StorageId(id);
String username = storageId.getExternalId();
return getUserByUsername(realm, username);
}
@Override
public UserModel getUserByUsername(RealmModel realm, String username) {
/*
* Remove @realm, this is needed as GSSAPI auth users reach here as user@realm
*/
int idx = username.indexOf("@");
if (idx != -1) {
username = username.substring(0, idx);
}
UserModel user = UserStoragePrivateUtil.userLocalStorage(session).getUserByUsername(realm, username);
if (user != null) {
logger.debug("User already exists in keycloak");
return user;
} else {
return createUserInKeycloak(realm, username);
}
}
protected UserModel createUserInKeycloak(RealmModel realm, String username) {
SCIMUser scimuser = ipatuura.getUserByUsername(username);
if (scimuser.getTotalResults() == 0) {
return null;
}
UserModel user = UserStoragePrivateUtil.userLocalStorage(session).addUser(realm, username);
user.setEmail(ipatuura.getEmail(scimuser));
user.setFirstName(ipatuura.getFirstName(scimuser));
user.setLastName(ipatuura.getLastName(scimuser));
user.setFederationLink(model.getId());
user.setEnabled(ipatuura.getActive(scimuser));
for (String name : ipatuura.getGroupsList(scimuser)) {
Stream<GroupModel> groupsStream = session.groups().searchForGroupByNameStream(realm, name, false, null, null);
GroupModel group = groupsStream.findFirst().orElse(null);
if (group == null) {
logger.debugv("No group found, creating group: {0}", name);
group = session.groups().createGroup(realm, name);
}
user.joinGroup(group);
}
logger.debugv("Creating SCIM user {0} in keycloak", username);
return new IpatuuraUserModelDelegate(ipatuura, user, model);
}
@Override
public void close() {
}
public Set<String> getSupportedCredentialTypes() {
return new HashSet<String>(this.supportedCredentialTypes);
}
@Override
public boolean isConfiguredFor(RealmModel realm, UserModel user, String credentialType) {
return getSupportedCredentialTypes().contains(credentialType);
}
@Override
public boolean supportsCredentialType(String credentialType) {
return getSupportedCredentialTypes().contains(credentialType);
}
@Override
public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) {
if (!supportsCredentialType(input.getType()) || !(input instanceof UserCredentialModel))
return false;
/*
* The password can either be validated locally in keycloak (tried first) or in the SCIM server
*/
if (((UserCredentialManager) user.credentialManager()).isConfiguredLocally(input.getType())) {
logger.debugv("Local password validation for {0}", user.getUsername());
/* return false in order to fallback to the next validator */
return false;
} else {
logger.debugv("Delegated password validation for {0}", user.getUsername());
Ipatuura ipatuura = this.ipatuura;
return ipatuura.isValid(user.getUsername(), input.getChallengeResponse());
}
}
@Override
public UserModel validate(RealmModel realm, UserModel local) {
Ipatuura ipatuura = this.ipatuura;
SCIMUser scimuser = ipatuura.getUserByUsername(local.getUsername());
String fname = ipatuura.getFirstName(scimuser);
String lname = ipatuura.getLastName(scimuser);
String email = ipatuura.getEmail(scimuser);
if (!local.getFirstName().equals(fname)) {
local.setFirstName(fname);
}
if (!local.getLastName().equals(lname)) {
local.setLastName(lname);
}
if (!local.getEmail().equals(email)) {
local.setEmail(email);
}
return new IpatuuraUserModelDelegate(this.ipatuura, local, model);
}
@Override
public UserModel addUser(RealmModel realm, String username) {
Ipatuura ipatuura = this.ipatuura;
SimpleHttp.Response resp = ipatuura.createUser(username);
try {
if (resp.getStatus() != HttpStatus.SC_CREATED) {
logger.warn("Unexpected create status code returned");
SCIMError error = resp.asJson(SCIMError.class);
logger.warn(error.getDetail());
resp.close();
return null;
}
resp.close();
} catch (IOException e) {
logger.errorv("Error: {0}", e.getMessage());
throw new RuntimeException(e);
}
return createUserInKeycloak(realm, username);
}
@Override
public boolean removeUser(RealmModel realm, UserModel user) {
logger.debugv("Removing user: {0}", user.getUsername());
Ipatuura ipatuura = this.ipatuura;
SimpleHttp.Response resp = ipatuura.deleteUser(user.getUsername());
Boolean status = false;
try {
status = resp.getStatus() == HttpStatus.SC_NO_CONTENT;
resp.close();
} catch (IOException e) {
logger.errorv("Error: {0}", e.getMessage());
throw new RuntimeException(e);
}
return status;
}
private Stream<UserModel> performSearch(RealmModel realm, String search) {
List<UserModel> users = new LinkedList<>();
Ipatuura ipatuura = this.ipatuura;
SCIMUser scimuser = ipatuura.getUserByUsername(search);
if (scimuser.getTotalResults() > 0) {
logger.debug("User found by username!");
if (UserStoragePrivateUtil.userLocalStorage(session).getUserByUsername(realm, search) == null) {
UserModel user = getUserByUsername(realm, ipatuura.getUserName(scimuser));
users.add(user);
} else {
logger.debug("User exists!");
}
return users.stream();
}
return users.stream();
}
@Override
public Stream<UserModel> getGroupMembersStream(RealmModel arg0, GroupModel arg1, Integer arg2, Integer arg3) {
return Stream.empty();
}
@Override
public int getUsersCount(RealmModel realm) {
Ipatuura ipatuura = this.ipatuura;
SCIMUser user = null;
SimpleHttp.Response response;
try {
response = ipatuura.clientRequest("/Users", "GET", null);
user = response.asJson(SCIMUser.class);
response.close();
} catch (Exception e) {
logger.errorv("Error: {0}", e.getMessage());
throw new RuntimeException(e);
}
return user.getTotalResults();
}
@Override
public Stream<UserModel> searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) {
return Stream.empty();
}
@Override
public Stream<UserModel> searchForUserStream(RealmModel realm, Map<String, String> params, Integer firstResult,
Integer maxResults) {
String search = params.get(UserModel.SEARCH);
/* only supports searching by username */
if (search == null)
return Stream.empty();
return performSearch(realm, search);
}
@Override
public boolean supportsCredentialAuthenticationFor(String type) {
return UserCredentialModel.KERBEROS.equals(type);
}
@Override
public CredentialValidationOutput authenticate(RealmModel realm, CredentialInput input) {
Map<String, String> state = new HashMap<>();
String username = null;
IpatuuraAuthenticator ipatuuraAuthenticator = factory.createSCIMAuthenticator();
String token = ipatuuraAuthenticator.getToken(session);
if (token != null) {
username = ipatuura.gssAuth(token);
/* Remove realm */
int idx = username.indexOf("@");
if (idx != -1) {
username = username.substring(0, idx);
}
logger.debug("GSSAPI authenticating with user " + username);
}
UserModel user = getUserByUsername(realm, username);
if (user == null) {
logger.debug("CredentialValidationOutput failed");
return CredentialValidationOutput.failed();
}
logger.debug("CredentialValidationOutput success!");
return new CredentialValidationOutput(user, CredentialValidationOutput.Status.AUTHENTICATED, state);
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.ipatuura_user_spi;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.Profile;
import org.keycloak.component.ComponentModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.component.ComponentValidationException;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.storage.UserStorageProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderConfigurationBuilder;
import org.keycloak.ipatuura_user_spi.authenticator.IpatuuraAuthenticator;
import java.util.LinkedList;
import java.util.List;
/**
* @author <a href="mailto:jstephen@redhat.com">Justin Stephenson</a>
* @version $Revision: 1 $
*/
public class IpatuuraUserStorageProviderFactory implements UserStorageProviderFactory<IpatuuraUserStorageProvider>, EnvironmentDependentProviderFactory {
private static final Logger logger = Logger.getLogger(IpatuuraUserStorageProviderFactory.class);
public static final String PROVIDER_NAME = "ipatuura";
protected static final List<String> PROVIDERS = new LinkedList<>();
protected static final List<ProviderConfigProperty> configMetadata;
static {
PROVIDERS.add("ipa");
PROVIDERS.add("ad");
PROVIDERS.add("ldap");
configMetadata = ProviderConfigurationBuilder.create()
/* SCIMv2 server url */
.property().name("scimurl").type(ProviderConfigProperty.STRING_TYPE).label("Ipatuura Server URL")
.helpText("Backend ipatuura server URL in the format: server.example.com:8080").add()
/* Login username, used to auth to make HTTP requests */
.property().name("loginusername").type(ProviderConfigProperty.STRING_TYPE).label("Login username")
.helpText("username to authenticate through the login page").add()
/* Login password, used to auth to make HTTP requests */
.property().name("loginpassword").type(ProviderConfigProperty.STRING_TYPE).label("Login password")
.helpText("password to authenticate through the login page").add().build();
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return configMetadata;
}
@Override
public void validateConfiguration(KeycloakSession session, RealmModel realm, ComponentModel config)
throws ComponentValidationException {
Ipatuura ipatuura = new Ipatuura(session, config);
SimpleHttp.Response response;
try {
response = ipatuura.clientRequest("", "GET", null);
response.close();
} catch (Exception e) {
throw new ComponentValidationException("Cannot connect to provided URL!", e);
}
}
@Override
public String getId() {
return PROVIDER_NAME;
}
@Override
public IpatuuraUserStorageProvider create(KeycloakSession session, ComponentModel model) {
Ipatuura ipatuura = new Ipatuura(session, model);
return new IpatuuraUserStorageProvider(session, model, ipatuura, this);
}
protected IpatuuraAuthenticator createSCIMAuthenticator() {
return new IpatuuraAuthenticator();
}
@Override
public boolean isSupported(Config.Scope config) {
return Profile.isFeatureEnabled(Profile.Feature.IPA_TUURA_FEDERATION);
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.ipatuura_user_spi.authenticator;
import jakarta.ws.rs.core.HttpHeaders;
import org.jboss.logging.Logger;
import org.keycloak.common.constants.KerberosConstants;
import org.keycloak.models.KeycloakSession;
/**
* @author <a href="mailto:jstephen@redhat.com.com">Justin Stephenson</a>
* @version $Revision: 1 $
*/
public class IpatuuraAuthenticator {
private static final Logger logger = Logger.getLogger(IpatuuraAuthenticator.class);
public String getToken(KeycloakSession session) {
HttpHeaders headers = session.getContext().getHttpRequest().getHttpHeaders();
String authHeader = headers.getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null) {
logger.debug("authHeader == NULL()!");
return null;
}
String[] tokens = authHeader.split(" ");
if (tokens.length == 0) { // assume not supported
logger.debug("Invalid length of tokens: " + tokens.length);
return null;
}
if (!KerberosConstants.NEGOTIATE.equalsIgnoreCase(tokens[0])) {
logger.debug("Unknown scheme " + tokens[0]);
return null;
}
if (tokens.length != 2) {
logger.debug("Invalid credentials tokens.length != 2");
return null;
}
String spnegoToken = tokens[1];
return spnegoToken;
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.ipatuura_user_spi.schemas;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Generated;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({ "schemas", "detail", "status" })
@Generated("jsonschema2pojo")
public class SCIMError {
@JsonProperty("schemas")
private List<String> schemas = null;
@JsonProperty("detail")
private String detail;
@JsonProperty("status")
private Integer status;
@JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
@JsonProperty("schemas")
public List<String> getSchemas() {
return schemas;
}
@JsonProperty("schemas")
public void setSchemas(List<String> schemas) {
this.schemas = schemas;
}
@JsonProperty("detail")
public String getDetail() {
return detail;
}
@JsonProperty("detail")
public void setDetail(String detail) {
this.detail = detail;
}
@JsonProperty("status")
public Integer getStatus() {
return status;
}
@JsonProperty("status")
public void setStatus(Integer status) {
this.status = status;
}
@JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
@JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
}

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.ipatuura_user_spi.schemas;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Generated;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({ "schemas", "filter" })
@Generated("jsonschema2pojo")
public class SCIMSearchRequest {
@JsonProperty("schemas")
private List<String> schemas = null;
@JsonProperty("filter")
private String filter;
@JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
@JsonProperty("schemas")
public List<String> getSchemas() {
return schemas;
}
@JsonProperty("schemas")
public void setSchemas(List<String> schemas) {
this.schemas = schemas;
}
@JsonProperty("filter")
public String getFilter() {
return filter;
}
@JsonProperty("filter")
public void setFilter(String filter) {
this.filter = filter;
}
@JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
@JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
}

View File

@@ -0,0 +1,434 @@
/*
* Copyright 2024 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.ipatuura_user_spi.schemas;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.annotation.Generated;
import com.fasterxml.jackson.annotation.JsonAnyGetter;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({ "Resources", "itemsPerPage", "schemas", "startIndex", "totalResults" })
@Generated("jsonschema2pojo")
public class SCIMUser {
@JsonProperty("Resources")
private List<Resource> resources = null;
@JsonProperty("itemsPerPage")
private Integer itemsPerPage;
@JsonProperty("schemas")
private List<String> schemas = null;
@JsonProperty("startIndex")
private Integer startIndex;
@JsonProperty("totalResults")
private Integer totalResults;
@JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
@JsonProperty("Resources")
public List<Resource> getResources() {
return resources;
}
@JsonProperty("Resources")
public void setResources(List<Resource> resources) {
this.resources = resources;
}
@JsonProperty("itemsPerPage")
public Integer getItemsPerPage() {
return itemsPerPage;
}
@JsonProperty("itemsPerPage")
public void setItemsPerPage(Integer itemsPerPage) {
this.itemsPerPage = itemsPerPage;
}
@JsonProperty("schemas")
public List<String> getSchemas() {
return schemas;
}
@JsonProperty("schemas")
public void setSchemas(List<String> schemas) {
this.schemas = schemas;
}
@JsonProperty("startIndex")
public Integer getStartIndex() {
return startIndex;
}
@JsonProperty("startIndex")
public void setStartIndex(Integer startIndex) {
this.startIndex = startIndex;
}
@JsonProperty("totalResults")
public Integer getTotalResults() {
return totalResults;
}
@JsonProperty("totalResults")
public void setTotalResults(Integer totalResults) {
this.totalResults = totalResults;
}
@JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
@JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({ "active", "emails", "groups", "id", "meta", "name", "schemas", "userName" })
@Generated("jsonschema2pojo")
public static class Resource {
@JsonProperty("active")
private Boolean active;
@JsonProperty("emails")
private List<Email> emails = null;
@JsonProperty("groups")
private List<Group> groups = null;
@JsonProperty("id")
private String id;
@JsonProperty("meta")
private Meta meta;
@JsonProperty("name")
private Name name;
@JsonProperty("schemas")
private List<String> schemas = null;
@JsonProperty("userName")
private String userName;
@JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
@JsonProperty("active")
public Boolean getActive() {
return active;
}
@JsonProperty("active")
public void setActive(boolean b) {
this.active = b;
}
@JsonProperty("emails")
public List<Email> getEmails() {
return emails;
}
@JsonProperty("emails")
public void setEmails(List<Email> emails) {
this.emails = emails;
}
@JsonProperty("groups")
public List<Group> getGroups() {
return groups;
}
@JsonProperty("groups")
public void setGroups(List<Group> groups) {
this.groups = groups;
}
@JsonProperty("id")
public String getId() {
return id;
}
@JsonProperty("id")
public void setId(String id) {
this.id = id;
}
@JsonProperty("meta")
public Meta getMeta() {
return meta;
}
@JsonProperty("meta")
public void setMeta(Meta meta) {
this.meta = meta;
}
@JsonProperty("name")
public Name getName() {
return name;
}
@JsonProperty("name")
public void setName(Name name) {
this.name = name;
}
@JsonProperty("schemas")
public List<String> getSchemas() {
return schemas;
}
@JsonProperty("schemas")
public void setSchemas(List<String> schemas) {
this.schemas = schemas;
}
@JsonProperty("userName")
public String getUserName() {
return userName;
}
@JsonProperty("userName")
public void setUserName(String userName) {
this.userName = userName;
}
@JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
@JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({ "familyName", "givenName", "middleName" })
@Generated("jsonschema2pojo")
public static class Name {
@JsonProperty("familyName")
private String familyName;
@JsonProperty("givenName")
private String givenName;
@JsonProperty("middleName")
private String middleName;
@JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
@JsonProperty("familyName")
public String getFamilyName() {
return familyName;
}
@JsonProperty("familyName")
public void setFamilyName(String familyName) {
this.familyName = familyName;
}
@JsonProperty("givenName")
public String getGivenName() {
return givenName;
}
@JsonProperty("givenName")
public void setGivenName(String givenName) {
this.givenName = givenName;
}
@JsonProperty("middleName")
public String getMiddleName() {
return middleName;
}
@JsonProperty("middleName")
public void setMiddleName(String middleName) {
this.middleName = middleName;
}
@JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
@JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({ "location", "resourceType" })
@Generated("jsonschema2pojo")
public static class Meta {
@JsonProperty("location")
private String location;
@JsonProperty("resourceType")
private String resourceType;
@JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
@JsonProperty("location")
public String getLocation() {
return location;
}
@JsonProperty("location")
public void setLocation(String location) {
this.location = location;
}
@JsonProperty("resourceType")
public String getResourceType() {
return resourceType;
}
@JsonProperty("resourceType")
public void setResourceType(String resourceType) {
this.resourceType = resourceType;
}
@JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
@JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({ "primary", "type", "value" })
@Generated("jsonschema2pojo")
public static class Email {
@JsonProperty("primary")
private Boolean primary;
@JsonProperty("type")
private String type;
@JsonProperty("value")
private String value;
@JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
@JsonProperty("primary")
public Boolean getPrimary() {
return primary;
}
@JsonProperty("primary")
public void setPrimary(Boolean primary) {
this.primary = primary;
}
@JsonProperty("type")
public String getType() {
return type;
}
@JsonProperty("type")
public void setType(String type) {
this.type = type;
}
@JsonProperty("value")
public String getValue() {
return value;
}
@JsonProperty("value")
public void setValue(String value) {
this.value = value;
}
@JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
@JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
}
@JsonInclude(JsonInclude.Include.NON_NULL)
@JsonPropertyOrder({ "$ref", "display", "value" })
@Generated("jsonschema2pojo")
public static class Group {
@JsonProperty("$ref")
private String $ref;
@JsonProperty("display")
private String display;
@JsonProperty("value")
private String value;
@JsonIgnore
private Map<String, Object> additionalProperties = new HashMap<String, Object>();
@JsonProperty("$ref")
public String get$ref() {
return $ref;
}
@JsonProperty("$ref")
public void set$ref(String $ref) {
this.$ref = $ref;
}
@JsonProperty("display")
public String getDisplay() {
return display;
}
@JsonProperty("display")
public void setDisplay(String display) {
this.display = display;
}
@JsonProperty("value")
public String getValue() {
return value;
}
@JsonProperty("value")
public void setValue(String value) {
this.value = value;
}
@JsonAnyGetter
public Map<String, Object> getAdditionalProperties() {
return this.additionalProperties;
}
@JsonAnySetter
public void setAdditionalProperty(String name, Object value) {
this.additionalProperties.put(name, value);
}
}
}
}

View File

@@ -0,0 +1,18 @@
#
# Copyright 2024 Red Hat, Inc. and/or its affiliates
# and other contributors as indicated by the @author tags.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
org.keycloak.ipatuura_user_spi.IpatuuraUserStorageProviderFactory

View File

@@ -36,6 +36,7 @@
<module>kerberos</module>
<module>ldap</module>
<module>sssd</module>
<module>ipatuura</module>
</modules>
</project>

View File

@@ -917,6 +917,11 @@
<artifactId>keycloak-ldap-federation</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-ipatuura-federation</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-dependencies-server-min</artifactId>

View File

@@ -334,6 +334,16 @@
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-ipatuura-federation</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-config-api</artifactId>