diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/Node.java b/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/Node.java index c16e96464b..8d97b6bc75 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/Node.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/Node.java @@ -304,6 +304,17 @@ public NodeAffiliate addNoneAffiliation(JID jid) { return addAffiliation(jid, NodeAffiliate.Affiliation.none); } + /** + * Adds a new affiliation or updates an existing affiliation of the specified entity JID + * to become a member affiliate. + * + * @param jid the JID of the member. + * @return the newly created or modified affiliation to the node. + */ + public NodeAffiliate addMember(JID jid) { + return addAffiliation(jid, NodeAffiliate.Affiliation.member); + } + /** * Sets that the specified entity is an outcast of the node. Outcast entities are not * able to publish or subscribe to the node. Existing subscriptions will be deleted. diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/NodeAffiliate.java b/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/NodeAffiliate.java index f172159b0c..e948dab55b 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/NodeAffiliate.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/NodeAffiliate.java @@ -364,6 +364,10 @@ public static enum Affiliation { * A publisher can subscribe and publish items to the node. */ publisher, + /** + * A member can subscribe to and retrieve items from whitelist nodes. + */ + member, /** * A user with no affiliation can susbcribe to the node. */ diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/PubSubEngine.java b/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/PubSubEngine.java index 1a6a7012ae..f2786a1743 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/PubSubEngine.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/PubSubEngine.java @@ -1761,6 +1761,9 @@ private void modifyNodeAffiliations(PubSubService service, IQ iq, Element entiti else if ("publisher".equals(newAffiliation)) { node.addPublisher(owner); } + else if ("member".equals(newAffiliation)) { + node.addMember(owner); + } else if ("none".equals(newAffiliation)) { node.addNoneAffiliation(owner); } diff --git a/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/models/WhitelistAccess.java b/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/models/WhitelistAccess.java index 43560d64c3..b4133dd8db 100644 --- a/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/models/WhitelistAccess.java +++ b/xmppserver/src/main/java/org/jivesoftware/openfire/pubsub/models/WhitelistAccess.java @@ -40,10 +40,13 @@ public boolean canSubscribe(Node node, JID owner, JID subscriber) { if (node.isAdmin(owner)) { return true; } - // User is in the whitelist if he has an affiliation and it is not of type outcast + // Whitelist access requires an explicit privileged affiliation. A "none" affiliation can + // linger after a subscription is removed and must not keep granting access. NodeAffiliate nodeAffiliate = node.getAffiliate(owner); - return nodeAffiliate != null && - nodeAffiliate.getAffiliation() != NodeAffiliate.Affiliation.outcast; + return nodeAffiliate != null && ( + nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.member || + nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.publisher || + nodeAffiliate.getAffiliation() == NodeAffiliate.Affiliation.owner); } @Override diff --git a/xmppserver/src/test/java/org/jivesoftware/openfire/pubsub/models/WhitelistAccessTest.java b/xmppserver/src/test/java/org/jivesoftware/openfire/pubsub/models/WhitelistAccessTest.java new file mode 100644 index 0000000000..1b936eeabf --- /dev/null +++ b/xmppserver/src/test/java/org/jivesoftware/openfire/pubsub/models/WhitelistAccessTest.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2026 Ignite Realtime Foundation. All rights reserved. + * + * 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.jivesoftware.openfire.pubsub.models; + +import org.jivesoftware.openfire.pubsub.Node; +import org.jivesoftware.openfire.pubsub.NodeAffiliate; +import org.junit.jupiter.api.Test; +import org.xmpp.packet.JID; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class WhitelistAccessTest { + + private final WhitelistAccess access = new WhitelistAccess(); + private final Node node = mock(Node.class); + private final NodeAffiliate affiliate = mock(NodeAffiliate.class); + private final JID user = new JID("user@example.org"); + + @Test + void memberCanSubscribe() { + when(node.getAffiliate(user)).thenReturn(affiliate); + when(affiliate.getAffiliation()).thenReturn(NodeAffiliate.Affiliation.member); + + assertTrue(access.canSubscribe(node, user, user)); + } + + @Test + void noneAffiliationIsNotWhitelisted() { + when(node.getAffiliate(user)).thenReturn(affiliate); + when(affiliate.getAffiliation()).thenReturn(NodeAffiliate.Affiliation.none); + + assertFalse(access.canSubscribe(node, user, user)); + } +}