From 3c33a7180efbb65ccfb46f402f3d5c81aaf89c66 Mon Sep 17 00:00:00 2001 From: Stefan Guilhen Date: Mon, 2 Dec 2024 14:59:21 -0300 Subject: [PATCH] Add initial IPA-Tuura federation (#35467) * Add initial federation ipatuura plugin Closes #35325 Signed-off-by: Justin Stephenson Signed-off-by: Stefan Guilhen Co-authored-by: Stefan Guilhen --- .../java/org/keycloak/common/Profile.java | 2 + dependencies/server-all/pom.xml | 4 + federation/ipatuura/pom.xml | 64 +++ .../keycloak/ipatuura_user_spi/Ipatuura.java | 414 +++++++++++++++++ .../IpatuuraUserModelDelegate.java | 91 ++++ .../IpatuuraUserStorageProvider.java | 329 +++++++++++++ .../IpatuuraUserStorageProviderFactory.java | 104 +++++ .../authenticator/IpatuuraAuthenticator.java | 62 +++ .../ipatuura_user_spi/schemas/SCIMError.java | 86 ++++ .../schemas/SCIMSearchRequest.java | 75 +++ .../ipatuura_user_spi/schemas/SCIMUser.java | 434 ++++++++++++++++++ ...eycloak.storage.UserStorageProviderFactory | 18 + federation/pom.xml | 1 + pom.xml | 5 + quarkus/runtime/pom.xml | 10 + 15 files changed, 1699 insertions(+) create mode 100644 federation/ipatuura/pom.xml create mode 100644 federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/Ipatuura.java create mode 100644 federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserModelDelegate.java create mode 100755 federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserStorageProvider.java create mode 100755 federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserStorageProviderFactory.java create mode 100755 federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/authenticator/IpatuuraAuthenticator.java create mode 100644 federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMError.java create mode 100644 federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMSearchRequest.java create mode 100644 federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMUser.java create mode 100644 federation/ipatuura/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory diff --git a/common/src/main/java/org/keycloak/common/Profile.java b/common/src/main/java/org/keycloak/common/Profile.java index a7bbcea726f..577f8552236 100755 --- a/common/src/main/java/org/keycloak/common/Profile.java +++ b/common/src/main/java/org/keycloak/common/Profile.java @@ -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; diff --git a/dependencies/server-all/pom.xml b/dependencies/server-all/pom.xml index c25827c2a37..40825859ef6 100755 --- a/dependencies/server-all/pom.xml +++ b/dependencies/server-all/pom.xml @@ -86,6 +86,10 @@ org.keycloak keycloak-sssd-federation + + org.keycloak + keycloak-ipatuura-federation + diff --git a/federation/ipatuura/pom.xml b/federation/ipatuura/pom.xml new file mode 100644 index 00000000000..5153d42c3cb --- /dev/null +++ b/federation/ipatuura/pom.xml @@ -0,0 +1,64 @@ + + + + + keycloak-parent + org.keycloak + 999.0.0-SNAPSHOT + ../../pom.xml + + 4.0.0 + + keycloak-ipatuura-federation + Keycloak Ipatuura Federation + + + + + org.keycloak + keycloak-core + provided + + + org.keycloak + keycloak-server-spi + provided + + + org.keycloak + keycloak-server-spi-private + provided + + + org.keycloak + keycloak-model-storage-private + provided + + + org.jboss.logging + jboss-logging + provided + + + org.keycloak + keycloak-services + + + + diff --git a/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/Ipatuura.java b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/Ipatuura.java new file mode 100644 index 00000000000..00c148d3be3 --- /dev/null +++ b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/Ipatuura.java @@ -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 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 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 schemas = new ArrayList(); + 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 schemas = new ArrayList(); + List emails = new ArrayList(); + List groups = new ArrayList(); + + 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 emails = new ArrayList(); + + 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 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 getGroupsList(SCIMUser user) { + List groups = user.getResources().get(0).getGroups(); + List groupnames = new ArrayList(); + + for (SCIMUser.Resource.Group group : groups) { + logger.debug("Retrieving group: " + group.getDisplay()); + groupnames.add(group.getDisplay()); + } + + return groupnames; + } +} diff --git a/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserModelDelegate.java b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserModelDelegate.java new file mode 100644 index 00000000000..1c5ac232bc1 --- /dev/null +++ b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserModelDelegate.java @@ -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 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); + } +} diff --git a/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserStorageProvider.java b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserStorageProvider.java new file mode 100755 index 00000000000..9b24f0e99ff --- /dev/null +++ b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserStorageProvider.java @@ -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 Justin Stephenson + * @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 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 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 getSupportedCredentialTypes() { + return new HashSet(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 performSearch(RealmModel realm, String search) { + List 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 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 searchForUserByUserAttributeStream(RealmModel realm, String attrName, String attrValue) { + return Stream.empty(); + } + + @Override + public Stream searchForUserStream(RealmModel realm, Map 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 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); + } +} diff --git a/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserStorageProviderFactory.java b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserStorageProviderFactory.java new file mode 100755 index 00000000000..411677d2fd1 --- /dev/null +++ b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/IpatuuraUserStorageProviderFactory.java @@ -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 Justin Stephenson + * @version $Revision: 1 $ + */ +public class IpatuuraUserStorageProviderFactory implements UserStorageProviderFactory, EnvironmentDependentProviderFactory { + + private static final Logger logger = Logger.getLogger(IpatuuraUserStorageProviderFactory.class); + public static final String PROVIDER_NAME = "ipatuura"; + protected static final List PROVIDERS = new LinkedList<>(); + protected static final List 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 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); + } +} diff --git a/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/authenticator/IpatuuraAuthenticator.java b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/authenticator/IpatuuraAuthenticator.java new file mode 100755 index 00000000000..1ca6a15c14d --- /dev/null +++ b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/authenticator/IpatuuraAuthenticator.java @@ -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 Justin Stephenson + * @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; + } +} diff --git a/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMError.java b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMError.java new file mode 100644 index 00000000000..fcc78d52a18 --- /dev/null +++ b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMError.java @@ -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 schemas = null; + @JsonProperty("detail") + private String detail; + @JsonProperty("status") + private Integer status; + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("schemas") + public List getSchemas() { + return schemas; + } + + @JsonProperty("schemas") + public void setSchemas(List 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 getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMSearchRequest.java b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMSearchRequest.java new file mode 100644 index 00000000000..52da04559fc --- /dev/null +++ b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMSearchRequest.java @@ -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 schemas = null; + @JsonProperty("filter") + private String filter; + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("schemas") + public List getSchemas() { + return schemas; + } + + @JsonProperty("schemas") + public void setSchemas(List schemas) { + this.schemas = schemas; + } + + @JsonProperty("filter") + public String getFilter() { + return filter; + } + + @JsonProperty("filter") + public void setFilter(String filter) { + this.filter = filter; + } + + @JsonAnyGetter + public Map getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + +} diff --git a/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMUser.java b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMUser.java new file mode 100644 index 00000000000..f8c3ba8aec6 --- /dev/null +++ b/federation/ipatuura/src/main/java/org/keycloak/ipatuura_user_spi/schemas/SCIMUser.java @@ -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 resources = null; + @JsonProperty("itemsPerPage") + private Integer itemsPerPage; + @JsonProperty("schemas") + private List schemas = null; + @JsonProperty("startIndex") + private Integer startIndex; + @JsonProperty("totalResults") + private Integer totalResults; + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("Resources") + public List getResources() { + return resources; + } + + @JsonProperty("Resources") + public void setResources(List 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 getSchemas() { + return schemas; + } + + @JsonProperty("schemas") + public void setSchemas(List 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 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 emails = null; + @JsonProperty("groups") + private List groups = null; + @JsonProperty("id") + private String id; + @JsonProperty("meta") + private Meta meta; + @JsonProperty("name") + private Name name; + @JsonProperty("schemas") + private List schemas = null; + @JsonProperty("userName") + private String userName; + @JsonIgnore + private Map additionalProperties = new HashMap(); + + @JsonProperty("active") + public Boolean getActive() { + return active; + } + + @JsonProperty("active") + public void setActive(boolean b) { + this.active = b; + } + + @JsonProperty("emails") + public List getEmails() { + return emails; + } + + @JsonProperty("emails") + public void setEmails(List emails) { + this.emails = emails; + } + + @JsonProperty("groups") + public List getGroups() { + return groups; + } + + @JsonProperty("groups") + public void setGroups(List 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 getSchemas() { + return schemas; + } + + @JsonProperty("schemas") + public void setSchemas(List schemas) { + this.schemas = schemas; + } + + @JsonProperty("userName") + public String getUserName() { + return userName; + } + + @JsonProperty("userName") + public void setUserName(String userName) { + this.userName = userName; + } + + @JsonAnyGetter + public Map 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 additionalProperties = new HashMap(); + + @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 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 additionalProperties = new HashMap(); + + @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 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 additionalProperties = new HashMap(); + + @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 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 additionalProperties = new HashMap(); + + @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 getAdditionalProperties() { + return this.additionalProperties; + } + + @JsonAnySetter + public void setAdditionalProperty(String name, Object value) { + this.additionalProperties.put(name, value); + } + + } + } +} diff --git a/federation/ipatuura/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory b/federation/ipatuura/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory new file mode 100644 index 00000000000..0907504e0d2 --- /dev/null +++ b/federation/ipatuura/src/main/resources/META-INF/services/org.keycloak.storage.UserStorageProviderFactory @@ -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 diff --git a/federation/pom.xml b/federation/pom.xml index 314e274a73b..662c1f5a77e 100755 --- a/federation/pom.xml +++ b/federation/pom.xml @@ -36,6 +36,7 @@ kerberos ldap sssd + ipatuura diff --git a/pom.xml b/pom.xml index 9c96c60d06a..291bf1c6aba 100644 --- a/pom.xml +++ b/pom.xml @@ -917,6 +917,11 @@ keycloak-ldap-federation ${project.version} + + org.keycloak + keycloak-ipatuura-federation + ${project.version} + org.keycloak keycloak-dependencies-server-min diff --git a/quarkus/runtime/pom.xml b/quarkus/runtime/pom.xml index f9ab9049c28..f75047d8de6 100644 --- a/quarkus/runtime/pom.xml +++ b/quarkus/runtime/pom.xml @@ -334,6 +334,16 @@ + + org.keycloak + keycloak-ipatuura-federation + + + * + * + + + org.keycloak keycloak-config-api