Skip to content

Commit

Permalink
Adding SAML protocol mapper to map organization membership
Browse files Browse the repository at this point in the history
Closes keycloak#28732

Signed-off-by: Pedro Igor <[email protected]>
  • Loading branch information
pedroigor committed May 6, 2024
1 parent 0e9b42a commit 5d76ca1
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
* limitations under the License.
*/

package org.keycloak.protocol.oidc.mappers;
package org.keycloak.organization.protocol.mappers.oidc;

import java.util.ArrayList;
import java.util.HashMap;
Expand All @@ -32,6 +32,12 @@
import org.keycloak.models.UserSessionModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.protocol.oidc.mappers.OIDCIDTokenMapper;
import org.keycloak.protocol.oidc.mappers.TokenIntrospectionTokenMapper;
import org.keycloak.protocol.oidc.mappers.UserInfoTokenMapper;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.IDToken;
Expand Down Expand Up @@ -69,15 +75,22 @@ public String getHelpText() {

@Override
protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) {
OrganizationProvider provider = keycloakSession.getProvider(OrganizationProvider.class);

if (!provider.isEnabled()) {
return;
}

UserModel user = userSession.getUser();
OrganizationProvider organizationProvider = keycloakSession.getProvider(OrganizationProvider.class);
OrganizationModel organization = organizationProvider.getByMember(user);
OrganizationModel organization = provider.getByMember(user);

if (organization != null) {
Map<String, Map<String, Object>> claim = new HashMap<>();
claim.put(organization.getName(), Map.of());
token.getOtherClaims().put(OAuth2Constants.ORGANIZATION, claim);
if (organization == null || !organization.isEnabled()) {
return;
}

Map<String, Map<String, Object>> claim = new HashMap<>();
claim.put(organization.getName(), Map.of());
token.getOtherClaims().put(OAuth2Constants.ORGANIZATION, claim);
}

public static ProtocolMapperModel create(String name, boolean accessToken, boolean idToken, boolean introspectionEndpoint) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* 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.organization.protocol.mappers.saml;

import java.util.List;

import org.keycloak.Config.Scope;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.OrganizationModel;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.organization.OrganizationProvider;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.protocol.saml.mappers.AbstractSAMLProtocolMapper;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.protocol.saml.mappers.SAMLAttributeStatementMapper;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;

public class OrganizationMembershipMapper extends AbstractSAMLProtocolMapper implements SAMLAttributeStatementMapper, EnvironmentDependentProviderFactory {

public static final String ID = "saml-organization-membership-mapper";
public static final String ORGANIZATION_ATTRIBUTE_NAME = "organization";

public static ProtocolMapperModel create() {
ProtocolMapperModel mapper = new ProtocolMapperModel();

mapper.setName("organization");
mapper.setProtocolMapper(ID);
mapper.setProtocol(SamlProtocol.LOGIN_PROTOCOL);

return mapper;

}

@Override
public void transformAttributeStatement(AttributeStatementType attributeStatement, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession) {
OrganizationProvider provider = session.getProvider(OrganizationProvider.class);

if (!provider.isEnabled()) {
return;
}

UserModel user = userSession.getUser();
OrganizationModel organization = provider.getByMember(user);

if (organization == null || !organization.isEnabled()) {
return;
}

AttributeType attribute = new AttributeType(ORGANIZATION_ATTRIBUTE_NAME);
attribute.setFriendlyName(ORGANIZATION_ATTRIBUTE_NAME);
attribute.setNameFormat(JBossSAMLURIConstants.ATTRIBUTE_FORMAT_BASIC.get());
attribute.addAttributeValue(organization.getName());
attributeStatement.addAttribute(new AttributeStatementType.ASTChoiceType(attribute));
}

@Override
public List<ProviderConfigProperty> getConfigProperties() {
return List.of();
}

@Override
public String getId() {
return ID;
}

@Override
public String getDisplayType() {
return "Organization Membership";
}

@Override
public String getDisplayCategory() {
return AttributeStatementHelper.ATTRIBUTE_STATEMENT_CATEGORY;
}

@Override
public String getHelpText() {
return "Add an attribute to the assertion with information about the organization membership.";
}

@Override
public boolean isSupported(Scope config) {
return Profile.isFeatureEnabled(Feature.ORGANIZATION);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
import org.keycloak.protocol.oidc.mappers.AllowedWebOriginsProtocolMapper;
import org.keycloak.protocol.oidc.mappers.AudienceResolveProtocolMapper;
import org.keycloak.protocol.oidc.mappers.FullNameMapper;
import org.keycloak.protocol.oidc.mappers.OrganizationMembershipMapper;
import org.keycloak.organization.protocol.mappers.oidc.OrganizationMembershipMapper;
import org.keycloak.protocol.oidc.mappers.UserAttributeMapper;
import org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper;
import org.keycloak.protocol.oidc.mappers.UserPropertyMapper;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
package org.keycloak.protocol.saml;

import org.keycloak.Config;
import org.keycloak.common.Profile;
import org.keycloak.common.Profile.Feature;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientScopeModel;
Expand All @@ -28,6 +30,7 @@
import org.keycloak.protocol.AbstractLoginProtocolFactory;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.saml.mappers.AttributeStatementHelper;
import org.keycloak.organization.protocol.mappers.saml.OrganizationMembershipMapper;
import org.keycloak.protocol.saml.mappers.RoleListMapper;
import org.keycloak.protocol.saml.mappers.UserPropertyAttributeStatementMapper;
import org.keycloak.representations.idm.CertificateRepresentation;
Expand Down Expand Up @@ -93,6 +96,11 @@ public void init(Config.Scope config) {
model = RoleListMapper.create("role list", "Role", AttributeStatementHelper.BASIC, null, false);
builtins.put("role list", model);
defaultBuiltins.add(model);
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
model = OrganizationMembershipMapper.create();
builtins.put("organization", model);
defaultBuiltins.add(model);
}
this.destinationValidator = DestinationValidator.forProtocolMap(config.getArray("knownProtocols"));
}

Expand All @@ -118,6 +126,14 @@ protected void createDefaultClientScopesImpl(RealmModel newRealm) {
roleListScope.setProtocol(getId());
roleListScope.addProtocolMapper(builtins.get("role list"));
newRealm.addDefaultClientScope(roleListScope, true);
if (Profile.isFeatureEnabled(Feature.ORGANIZATION)) {
ClientScopeModel organizationScope = newRealm.addClientScope("saml_organization");
organizationScope.setDescription("Organization Membership");
organizationScope.setDisplayOnConsentScreen(false);
organizationScope.setProtocol(getId());
organizationScope.addProtocolMapper(builtins.get("organization"));
newRealm.addDefaultClientScope(organizationScope, true);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ org.keycloak.protocol.oidc.mappers.RoleNameMapper
org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper
org.keycloak.protocol.oidc.mappers.GroupMembershipMapper
org.keycloak.protocol.oidc.mappers.AudienceProtocolMapper
org.keycloak.protocol.oidc.mappers.OrganizationMembershipMapper
org.keycloak.organization.protocol.mappers.oidc.OrganizationMembershipMapper
org.keycloak.protocol.oidc.mappers.AudienceResolveProtocolMapper
org.keycloak.protocol.oidc.mappers.AllowedWebOriginsProtocolMapper
org.keycloak.protocol.oidc.mappers.AcrProtocolMapper
Expand Down Expand Up @@ -56,3 +56,4 @@ org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCTypeMapper
org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCContextMapper
org.keycloak.protocol.oidc.mappers.SessionStateMapper
org.keycloak.protocol.oidc.mappers.SubMapper
org.keycloak.organization.protocol.mappers.saml.OrganizationMembershipMapper
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*
* 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.testsuite.organization.admin;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.keycloak.testsuite.util.SamlStreams.assertionsUnencrypted;
import static org.keycloak.testsuite.util.SamlStreams.attributeStatements;

import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;

import jakarta.ws.rs.core.UriBuilder;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.admin.client.resource.OrganizationResource;
import org.keycloak.common.Profile.Feature;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType;
import org.keycloak.dom.saml.v2.assertion.AttributeStatementType.ASTChoiceType;
import org.keycloak.dom.saml.v2.assertion.AttributeType;
import org.keycloak.protocol.saml.SamlConfigAttributes;
import org.keycloak.protocol.saml.SamlProtocol;
import org.keycloak.organization.protocol.mappers.saml.OrganizationMembershipMapper;
import org.keycloak.saml.common.constants.JBossSAMLURIConstants;
import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
import org.keycloak.testsuite.saml.RoleMapperTest;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.SamlClient;
import org.keycloak.testsuite.util.SamlClientBuilder;

@EnableFeature(Feature.ORGANIZATION)
public class OrganizationSAMLProtocolMapperTest extends AbstractOrganizationTest {

@Test
public void testAttribute() {
OrganizationResource organization = testRealm().organizations().get(createOrganization().getId());
addMember(organization);
String clientId = "saml-client";
testRealm().clients().create(ClientBuilder.create()
.protocol(SamlProtocol.LOGIN_PROTOCOL)
.clientId(clientId)
.redirectUris("*")
.attribute(SamlConfigAttributes.SAML_CLIENT_SIGNATURE_ATTRIBUTE, Boolean.FALSE.toString())
.build()).close();

SAMLDocumentHolder samlResponse = new SamlClientBuilder()
.authnRequest(RealmsResource
.protocolUrl(UriBuilder.fromUri(getAuthServerRoot()))
.build(TEST_REALM_NAME, SamlProtocol.LOGIN_PROTOCOL), clientId, RoleMapperTest.SAML_ASSERTION_CONSUMER_URL_EMPLOYEE_2, SamlClient.Binding.POST)
.build()
.login().user(memberEmail, memberPassword).build()
.login().user(memberEmail, memberPassword).build()
.getSamlResponse(SamlClient.Binding.POST);

assertThat(samlResponse.getSamlObject(), Matchers.isSamlResponse(JBossSAMLURIConstants.STATUS_SUCCESS));
AttributeType orgAttribute = attributeStatements(assertionsUnencrypted(samlResponse.getSamlObject()))
.flatMap((Function<AttributeStatementType, Stream<ASTChoiceType>>) attributeStatementType -> attributeStatementType.getAttributes().stream())
.map(ASTChoiceType::getAttribute)
.filter(attribute -> OrganizationMembershipMapper.ORGANIZATION_ATTRIBUTE_NAME.equals(attribute.getName()))
.findAny()
.orElse(null);
Assert.assertNotNull(orgAttribute);
List<Object> values = orgAttribute.getAttributeValue();
Assert.assertEquals(1, values.size());
Assert.assertEquals(organizationName, values.get(0));
}
}

0 comments on commit 5d76ca1

Please sign in to comment.