Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 64 additions & 35 deletions model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java
Original file line number Diff line number Diff line change
Expand Up @@ -231,30 +231,79 @@ public void setRememberMe(boolean rememberMe) {

@Override
public void setAttribute(String name, String value) {
updateAttribute(name, value);
}

/**
* Sets the attribute and publishes a {@link RealmModel.RealmAttributeUpdateEvent} if the value changed.
*
* @return {@code true} if the value changed and the event was published, {@code false} for a no-op write
*/
private boolean updateAttribute(String name, String value) {
RealmAttributeEntity existing = null;
for (RealmAttributeEntity attr : realm.getAttributes()) {
if (attr.getName().equals(name)) {
attr.setValue(value);
return;
existing = attr;
break;
}
}
RealmAttributeEntity attr = new RealmAttributeEntity();
attr.setName(name);
attr.setValue(value);
attr.setRealm(realm);
em.persist(attr);
realm.getAttributes().add(attr);
if (existing != null) {
if (Objects.equals(existing.getValue(), value)) {
return false;
}
existing.setValue(value);
} else {
RealmAttributeEntity attr = new RealmAttributeEntity();
attr.setName(name);
attr.setValue(value);
attr.setRealm(realm);
em.persist(attr);
realm.getAttributes().add(attr);
}
publishRealmAttributeUpdateEvent(name, value);
Comment thread
ssilvert marked this conversation as resolved.
return true;
}

private void publishRealmAttributeUpdateEvent(String name, String value) {
session.getKeycloakSessionFactory().publish(new RealmModel.RealmAttributeUpdateEvent() {

@Override
public RealmModel getRealm() {
return RealmAdapter.this;
}

@Override
public String getAttributeName() {
return name;
}

@Override
public String getAttributeValue() {
return value;
}

@Override
public KeycloakSession getKeycloakSession() {
return session;
}
});
}

@Override
public void removeAttribute(String name) {
boolean removed = false;
Iterator<RealmAttributeEntity> it = realm.getAttributes().iterator();
while (it.hasNext()) {
RealmAttributeEntity attr = it.next();
if (attr.getName().equals(name)) {
it.remove();
em.remove(attr);
removed = true;
}
}
if (removed) {
publishRealmAttributeUpdateEvent(name, null);
}
}

@Override
Expand Down Expand Up @@ -1303,33 +1352,13 @@ public boolean isAdminPermissionsEnabled() {

@Override
public void setAdminPermissionsEnabled(boolean adminPermissionsEnabled) {
boolean isAdminPermissionsAlreadyEnabled = getAdminPermissionsClient() != null;
setAttribute(RealmAttributes.ADMIN_PERMISSIONS_ENABLED, adminPermissionsEnabled);

// sending an event if we are enabling the permissions and it was not enabled already
if (adminPermissionsEnabled && !isAdminPermissionsAlreadyEnabled) {
session.getKeycloakSessionFactory().publish(new RealmModel.RealmAttributeUpdateEvent() {

@Override
public RealmModel getRealm() {
return RealmAdapter.this;
}

@Override
public String getAttributeName() {
return RealmAttributes.ADMIN_PERMISSIONS_ENABLED;
}

@Override
public String getAttributeValue() {
return String.valueOf(adminPermissionsEnabled);
}

@Override
public KeycloakSession getKeycloakSession() {
return session;
}
});
boolean published = updateAttribute(RealmAttributes.ADMIN_PERMISSIONS_ENABLED, String.valueOf(adminPermissionsEnabled));

// updateAttribute already published the update if the value changed. This explicit publish
// covers only the repair case where the attribute was already "true" but the admin
// permissions client is missing, keeping one event per change.
if (!published && adminPermissionsEnabled && getAdminPermissionsClient() == null) {
publishRealmAttributeUpdateEvent(RealmAttributes.ADMIN_PERMISSIONS_ENABLED, String.valueOf(adminPermissionsEnabled));
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.keycloak.tests.ssf.transmitter;

import java.util.List;

import org.keycloak.common.Profile;
import org.keycloak.representations.idm.ClientScopeRepresentation;
import org.keycloak.ssf.Ssf;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.server.DefaultKeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

/**
* Regression test for <a href="https://github.com/keycloak/keycloak/issues/49236">keycloak#49236</a>:
* enabling the SSF transmitter on an existing realm must create the
* {@code ssf.read} and {@code ssf.manage} client scopes if they are missing.
*/
@KeycloakIntegrationTest(config = SsfTransmitterEnableCreatesClientScopesTests.SsfServerConfig.class)
public class SsfTransmitterEnableCreatesClientScopesTests {

@InjectRealm(config = SsfDisabledRealm.class)
ManagedRealm realm;

@Test
public void testEnablingTransmitterCreatesMissingClientScopes() {

// The realm was created while the SSF feature was already enabled, so the
// scopes exist from realm creation. Delete them to simulate a realm that
// was created before the SSF feature was turned on.
deleteClientScopeIfPresent(Ssf.SCOPE_SSF_READ);
deleteClientScopeIfPresent(Ssf.SCOPE_SSF_MANAGE);

Assertions.assertNull(findClientScope(Ssf.SCOPE_SSF_READ), "precondition: ssf.read should be absent");
Assertions.assertNull(findClientScope(Ssf.SCOPE_SSF_MANAGE), "precondition: ssf.manage should be absent");

// Enable the SSF transmitter through a regular realm update, like the admin console does
realm.updateWithCleanup(r -> r.attribute(Ssf.SSF_TRANSMITTER_ENABLED_KEY, "true"));

ClientScopeRepresentation readScope = findClientScope(Ssf.SCOPE_SSF_READ);
ClientScopeRepresentation manageScope = findClientScope(Ssf.SCOPE_SSF_MANAGE);

Assertions.assertNotNull(readScope, "ssf.read client scope should be created when the transmitter is enabled");
Assertions.assertNotNull(manageScope, "ssf.manage client scope should be created when the transmitter is enabled");

// The scopes must be registered as optional realm client scopes so receivers have to request them explicitly
List<String> optionalScopeNames = realm.admin().getDefaultOptionalClientScopes().stream()
.map(ClientScopeRepresentation::getName)
.toList();
Assertions.assertTrue(optionalScopeNames.contains(Ssf.SCOPE_SSF_READ), "ssf.read should be an optional client scope");
Assertions.assertTrue(optionalScopeNames.contains(Ssf.SCOPE_SSF_MANAGE), "ssf.manage should be an optional client scope");
}

@Test
public void testReenablingTransmitterRestoresDeletedClientScopes() {

// Start with the transmitter enabled and the scopes present
realm.updateWithCleanup(r -> r.attribute(Ssf.SSF_TRANSMITTER_ENABLED_KEY, "true"));

// Admin disables the transmitter and removes the scopes
realm.updateWithCleanup(r -> r.attribute(Ssf.SSF_TRANSMITTER_ENABLED_KEY, "false"));
deleteClientScopeIfPresent(Ssf.SCOPE_SSF_READ);
deleteClientScopeIfPresent(Ssf.SCOPE_SSF_MANAGE);

// Re-enabling must bring the scopes back
realm.updateWithCleanup(r -> r.attribute(Ssf.SSF_TRANSMITTER_ENABLED_KEY, "true"));

Assertions.assertNotNull(findClientScope(Ssf.SCOPE_SSF_READ), "ssf.read client scope should be re-created");
Assertions.assertNotNull(findClientScope(Ssf.SCOPE_SSF_MANAGE), "ssf.manage client scope should be re-created");
}

private ClientScopeRepresentation findClientScope(String name) {
return realm.admin().clientScopes().findAll().stream()
.filter(scope -> name.equals(scope.getName()))
.findFirst()
.orElse(null);
}

private void deleteClientScopeIfPresent(String name) {
ClientScopeRepresentation scope = findClientScope(name);
if (scope != null) {
realm.admin().clientScopes().get(scope.getId()).remove();
}
}

public static class SsfServerConfig extends DefaultKeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
KeycloakServerConfigBuilder configured = super.configure(config);
config.features(Profile.Feature.SSF);
return configured;
}
}

public static class SsfDisabledRealm implements RealmConfig {
@Override
public RealmBuilder configure(RealmBuilder realm) {
// Intentionally no ssf.transmitterEnabled attribute — the realm starts
// with the transmitter disabled, like a realm created before SSF was adopted
return realm.name("ssf-enable-scopes");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
import static org.hamcrest.Matchers.hasEntry;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;

@RequireProvider(RealmProvider.class)
public class RealmModelTest extends KeycloakModelTest {
Expand Down Expand Up @@ -153,6 +154,52 @@ public void testRealmPreRemoveDoesntRemoveEntitiesFromOtherRealms() {
assertThat(resourceServer, notNullValue());
}

@Test
public void testRealmAttributeUpdateEvents() {
ProviderEventListener providerEventListener = null;
try {
List<RealmModel.RealmAttributeUpdateEvent> attributeUpdateEvents = new ArrayList<>();
providerEventListener = event -> {
if (event instanceof RealmModel.RealmAttributeUpdateEvent attributeUpdateEvent
&& "attr-a".equals(attributeUpdateEvent.getAttributeName())) {
attributeUpdateEvents.add(attributeUpdateEvent);
}
};
getFactory().register(providerEventListener);

withRealm(realmId, (session, realm) -> {
// creating an attribute publishes an event
realm.setAttribute("attr-a", "value-1");
assertThat(attributeUpdateEvents, hasSize(1));
assertThat(attributeUpdateEvents.get(0).getAttributeValue(), equalTo("value-1"));

// setting the same value again is a no-op and publishes nothing
realm.setAttribute("attr-a", "value-1");
assertThat(attributeUpdateEvents, hasSize(1));

// changing the value publishes an event
realm.setAttribute("attr-a", "value-2");
assertThat(attributeUpdateEvents, hasSize(2));
assertThat(attributeUpdateEvents.get(1).getAttributeValue(), equalTo("value-2"));

// removing the attribute publishes an event with a null value
realm.removeAttribute("attr-a");
assertThat(attributeUpdateEvents, hasSize(3));
assertThat(attributeUpdateEvents.get(2).getAttributeValue(), nullValue());

// removing an absent attribute publishes nothing
realm.removeAttribute("attr-a");
assertThat(attributeUpdateEvents, hasSize(3));

return null;
});
} finally {
if (providerEventListener != null) {
getFactory().unregister(providerEventListener);
}
}
}

@Test
public void testMoveGroup() {
ProviderEventListener providerEventListener = null;
Expand Down
Loading