From 7792b695e378c30d301ee2df453647ee6ab071b2 Mon Sep 17 00:00:00 2001 From: Alex O'Ree Date: Mon, 15 Dec 2025 16:14:53 -0500 Subject: [PATCH] JSPWIKI-1210 Advanced ACL rules, now supports AND/OR and NOT operators, as well as parenthesis --- .../java/org/apache/wiki/api/core/Engine.java | 8 ++ .../main/java/org/apache/wiki/WikiEngine.java | 3 +- .../auth/DefaultAuthorizationManager.java | 34 ++++- .../wiki/auth/acl/DefaultAclManager.java | 52 ++++++- .../apache/wiki/auth/acl/adv/AdvancedAcl.java | 116 ++++++++++++++++ .../org/apache/wiki/auth/acl/adv/NotNode.java | 49 +++++++ .../wiki/auth/acl/adv/OperatorNode.java | 69 +++++++++ .../apache/wiki/auth/acl/adv/RoleNode.java | 63 +++++++++ .../apache/wiki/auth/acl/adv/RuleNode.java | 45 ++++++ .../apache/wiki/auth/acl/adv/RuleParser.java | 109 +++++++++++++++ .../wiki/auth/acl/DefaultAclManagerTest.java | 84 +++++++++++ .../wiki/auth/acl/adv/RuleParserTest.java | 131 ++++++++++++++++++ 12 files changed, 759 insertions(+), 4 deletions(-) create mode 100644 jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/AdvancedAcl.java create mode 100644 jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/NotNode.java create mode 100644 jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/OperatorNode.java create mode 100644 jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RoleNode.java create mode 100644 jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RuleNode.java create mode 100644 jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RuleParser.java create mode 100644 jspwiki-main/src/test/java/org/apache/wiki/auth/acl/adv/RuleParserTest.java diff --git a/jspwiki-api/src/main/java/org/apache/wiki/api/core/Engine.java b/jspwiki-api/src/main/java/org/apache/wiki/api/core/Engine.java index 0dd704a22b..f1cc2897b0 100644 --- a/jspwiki-api/src/main/java/org/apache/wiki/api/core/Engine.java +++ b/jspwiki-api/src/main/java/org/apache/wiki/api/core/Engine.java @@ -110,6 +110,14 @@ public interface Engine { /** The name of the property containing the ACLManager implementing class. The value is {@value}. */ String PROP_ACL_MANAGER_IMPL = "jspwiki.aclManager"; + + /** + * The name of the property containing the AuthorizationManager implementing + * class. The value is {@value}. + * + * @since 3.0.0 + */ + String PROP_AUTHZ_MANAGER_IMPL = "jspwiki.authZManager"; /** The name of the property containing the ReferenceManager implementing class. The value is {@value}. */ String PROP_REF_MANAGER_IMPL = "jspwiki.refManager"; diff --git a/jspwiki-main/src/main/java/org/apache/wiki/WikiEngine.java b/jspwiki-main/src/main/java/org/apache/wiki/WikiEngine.java index db9ba64ccc..4a26a2e8d4 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/WikiEngine.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/WikiEngine.java @@ -276,6 +276,7 @@ public void initialize( final Properties props ) throws WikiException { // try { final String aclClassName = m_properties.getProperty( PROP_ACL_MANAGER_IMPL, ClassUtil.getMappedClass( AclManager.class.getName() ).getName() ); + final String authClassName = m_properties.getProperty( PROP_AUTHZ_MANAGER_IMPL, ClassUtil.getMappedClass( AuthorizationManager.class.getName() ).getName() ); final String urlConstructorClassName = TextUtil.getStringProperty( props, PROP_URLCONSTRUCTOR, "DefaultURLConstructor" ); final Class< URLConstructor > urlclass = ClassUtil.findClass( "org.apache.wiki.url", urlConstructorClassName ); @@ -289,7 +290,7 @@ public void initialize( final Properties props ) throws WikiException { initComponent( VariableManager.class, props ); initComponent( SearchManager.class, this, props ); initComponent( AuthenticationManager.class ); - initComponent( AuthorizationManager.class ); + initComponent( authClassName, AuthorizationManager.class ); initComponent( UserManager.class ); initComponent( GroupManager.class ); initComponent( EditorManager.class, this ); diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultAuthorizationManager.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultAuthorizationManager.java index 8080de4217..74109c7914 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultAuthorizationManager.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/DefaultAuthorizationManager.java @@ -61,10 +61,14 @@ Licensed to the Apache Software Foundation (ASF) under one import java.security.cert.Certificate; import java.text.MessageFormat; import java.util.Arrays; +import java.util.HashSet; import java.util.Map; import java.util.Properties; import java.util.ResourceBundle; +import java.util.Set; import java.util.WeakHashMap; +import org.apache.wiki.auth.acl.adv.AdvancedAcl; +import org.apache.wiki.auth.acl.adv.RuleNode; /** @@ -85,7 +89,7 @@ public class DefaultAuthorizationManager implements AuthorizationManager { /** Cache for storing ProtectionDomains used to evaluate the local policy. */ private final Map< Principal, ProtectionDomain > m_cachedPds = new WeakHashMap<>(); - private Engine m_engine; + protected Engine m_engine; private LocalPolicy m_localPolicy; @@ -135,7 +139,35 @@ public boolean checkPermission( final Session session, final Permission permissi fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission ); return true; } + if (acl instanceof AdvancedAcl) { + AdvancedAcl a2 = (AdvancedAcl) acl; + Set roles = new HashSet<>(); + roles.add(session.getLoginPrincipal().getName()); + if (session.getRoles() != null) { + for (Principal p : session.getRoles()) { + roles.add(p.getName()); + } + } + RuleNode node = a2.getNode(permission); + if (node == null) { + fireEvent(WikiSecurityEvent.ACCESS_ALLOWED, user, permission); + return true; + } + Set potentialRoles = node.getAllRoles(); + for (String s : potentialRoles) { + if (hasRoleOrPrincipal(session, new WikiPrincipal(s))) { + roles.add(s); + } + } + if (node.evaluate(roles)) { + //granted.. + fireEvent(WikiSecurityEvent.ACCESS_ALLOWED, user, permission); + return true; + } + fireEvent( WikiSecurityEvent.ACCESS_DENIED, user, permission ); + return false; + } // Next, iterate through the Principal objects assigned this permission. If the context's subject possesses // any of these, the action is allowed. final Principal[] aclPrincipals = acl.findPrincipals( permission ); diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/DefaultAclManager.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/DefaultAclManager.java index b044058dc8..32ff9946ff 100644 --- a/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/DefaultAclManager.java +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/DefaultAclManager.java @@ -49,6 +49,12 @@ Licensed to the Apache Software Foundation (ASF) under one import java.util.TreeMap; import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.apache.wiki.auth.acl.adv.AdvancedAcl; +import org.apache.wiki.auth.acl.adv.NotNode; +import org.apache.wiki.auth.acl.adv.OperatorNode; +import org.apache.wiki.auth.acl.adv.RoleNode; +import org.apache.wiki.auth.acl.adv.RuleNode; +import org.apache.wiki.auth.acl.adv.RuleParser; /** * Default implementation that parses Acls from wiki page markup. @@ -91,10 +97,40 @@ public void initialize( final Engine engine, final Properties props ) { public Acl parseAcl( final Page page, final String ruleLine ) throws WikiSecurityException { Acl acl = page.getAcl(); if (acl == null) { - acl = Wiki.acls().acl(); + acl = Wiki.acls().acl(); } try { + if (ruleLine.contains(" AND ") + || ruleLine.contains(" NOT ") + || ruleLine.contains(" OR ")) { + try { + if (acl == null || !(acl instanceof AdvancedAcl)) { + acl = new AdvancedAcl(); + } + final StringTokenizer fieldToks = new StringTokenizer(ruleLine); + //burn off the allow tag + fieldToks.nextToken(); + //get the permission flag. i.e. edit, view, etc + final String actions = fieldToks.nextToken(); + StringBuilder sb = new StringBuilder(); + while (fieldToks.hasMoreTokens()) { + sb.append(fieldToks.nextToken() + " "); + } + RuleParser parser = new RuleParser(sb.toString()); + RuleNode node = parser.parse(); + ((AdvancedAcl) acl).addRuleNode(node, actions); + recursiveResolve(node); + page.setAcl(acl); + LOG.debug(acl.toString()); + return acl; + } catch (final NoSuchElementException nsee) { + LOG.warn("Invalid access rule: " + ruleLine + " - defaults will be used."); + throw new WikiSecurityException("Invalid access rule: " + ruleLine, nsee); + } catch (final IllegalArgumentException iae) { + throw new WikiSecurityException("Invalid permission type: " + ruleLine, iae); + } + } final StringTokenizer fieldToks = new StringTokenizer(ruleLine); fieldToks.nextToken(); final String actions = fieldToks.nextToken(); @@ -128,7 +164,19 @@ public Acl parseAcl( final Page page, final String ruleLine ) throws WikiSecurit return acl; } - + private void recursiveResolve(RuleNode node) { + if (node == null) { + return; + } + if (node instanceof OperatorNode) { + recursiveResolve(((OperatorNode) node).getLeft()); + recursiveResolve(((OperatorNode) node).getRight()); + } else if (node instanceof NotNode) { + recursiveResolve(((NotNode) node).getChild()); + } else if (node instanceof RoleNode) { + ((RoleNode) node).setPrincipal(m_auth.resolvePrincipal(((RoleNode) node).getRole())); + } + } /** {@inheritDoc} */ @Override diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/AdvancedAcl.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/AdvancedAcl.java new file mode 100644 index 0000000000..4d10ccb49d --- /dev/null +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/AdvancedAcl.java @@ -0,0 +1,116 @@ +/* + * Copyright 2025 The Apache Software Foundation. + * + * 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.apache.wiki.auth.acl.adv; + +import java.security.Permission; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import org.apache.wiki.api.core.AclEntry; +import org.apache.wiki.auth.acl.Acl; + +/** + * an extension to the base ACL classes that provides boolean logic, tyical use + * case is for role/group membership + * + * @since 3.0.0 + * @see AclEntry + * @see AclImpl + * @see DefaultAclManager + */ +public class AdvancedAcl implements org.apache.wiki.api.core.Acl, Acl { + + private Map nodes = new HashMap<>(); + + @Override + public boolean addEntry(AclEntry entry) { + if (entry instanceof AdvancedAcl e) { + this.nodes.putAll(e.nodes); + return true; + } + return false; + } + + @Override + public Enumeration aclEntries() { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean isEmpty() { + return nodes.isEmpty(); + } + + @Override + public Principal[] findPrincipals(Permission permission) { + final List< Principal> principals = new ArrayList<>(); + final Enumeration< AclEntry> entries = aclEntries(); + while (entries.hasMoreElements()) { + final AclEntry entry = entries.nextElement(); + final Enumeration< Permission> permissions = entry.permissions(); + while (permissions.hasMoreElements()) { + final Permission perm = permissions.nextElement(); + if (perm.implies(permission)) { + principals.add(entry.getPrincipal()); + } + } + } + return principals.toArray(new Principal[0]); + } + + @Override + public AclEntry getAclEntry(Principal principal) { + throw new UnsupportedOperationException("Not supported yet."); + } + + @Override + public boolean removeEntry(AclEntry entry) { + boolean success = false; + if (entry instanceof AdvancedAcl e) { + for (String s : e.nodes.keySet()) { + RuleNode remove = nodes.remove(s); + if (remove != null) { + success = true; + } + } + + } + return success; + } + + public void addRuleNode(RuleNode node, String actions) { + nodes.put(actions, node); + } + + public RuleNode getNode(Permission permission) { + return nodes.get(permission.getActions()); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + + for (Entry e : nodes.entrySet()) { + sb.append("[{ALLOW ").append(e.getKey()).append(" ").append(e.toString()).append("}]\n"); + } + return sb.toString(); + } + +} diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/NotNode.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/NotNode.java new file mode 100644 index 0000000000..512070f7ce --- /dev/null +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/NotNode.java @@ -0,0 +1,49 @@ +/* + * Copyright 2025 The Apache Software Foundation. + * + * 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.apache.wiki.auth.acl.adv; + +import java.util.Set; + +/** + * + * @author AO + */ +public class NotNode extends RuleNode { + private final RuleNode child; + + public RuleNode getChild() { + return child; + } + + public NotNode(RuleNode child) { + this.child = child; + } + + @Override + public boolean evaluate(java.util.Set userRoles) { + return !child.evaluate(userRoles); + } + + @Override + public String toString() { + return "(NOT " + child + ")"; + } + + @Override + public Set getAllRoles() { + return child.getAllRoles(); + } +} diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/OperatorNode.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/OperatorNode.java new file mode 100644 index 0000000000..d8501c076a --- /dev/null +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/OperatorNode.java @@ -0,0 +1,69 @@ +/* + * Copyright 2025 The Apache Software Foundation. + * + * 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.apache.wiki.auth.acl.adv; + +import java.util.HashSet; +import java.util.Set; + +/** + * + * @author AO + */ +public class OperatorNode extends RuleNode { + + @Override + public Set getAllRoles() { + Set set = new HashSet<>(); + set.addAll(left.getAllRoles()); + set.addAll(right.getAllRoles()); + return set; + } + public enum Operator { AND, OR } + + private final Operator operator; + private final RuleNode left, right; + + public Operator getOperator() { + return operator; + } + + public RuleNode getLeft() { + return left; + } + + public RuleNode getRight() { + return right; + } + + public OperatorNode(Operator operator, RuleNode left, RuleNode right) { + this.operator = operator; + this.left = left; + this.right = right; + } + + @Override + public boolean evaluate(java.util.Set userRoles) { + boolean l = left.evaluate(userRoles); + boolean r = right.evaluate(userRoles); + return operator == Operator.AND ? (l && r) : (l || r); + } + + @Override + public String toString() { + return "(" + left + " " + operator + " " + right + ")"; + } +} + diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RoleNode.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RoleNode.java new file mode 100644 index 0000000000..d42053c290 --- /dev/null +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RoleNode.java @@ -0,0 +1,63 @@ +/* + * Copyright 2025 The Apache Software Foundation. + * + * 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.apache.wiki.auth.acl.adv; + +import java.security.Principal; +import java.util.HashSet; +import java.util.Set; + +/** + * + * @author AO + */ +public class RoleNode extends RuleNode { + + private final String role; + + public String getRole() { + return role; + } + + public RoleNode(String role) { + this.role = role.trim(); + } + + @Override + public boolean evaluate(java.util.Set userRoles) { + return userRoles.contains(role); + } + + @Override + public String toString() { + return role; + } + protected Principal principal; + + public void setPrincipal(Principal principal) { + this.principal = principal; + } + + public Principal getPrincipal() { + return this.principal; + } + + @Override + public Set getAllRoles() { + Set set = new HashSet<>(); + set.add(role); + return set; + } +} diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RuleNode.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RuleNode.java new file mode 100644 index 0000000000..c3243bea28 --- /dev/null +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RuleNode.java @@ -0,0 +1,45 @@ +/* + * Copyright 2025 The Apache Software Foundation. + * + * 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.apache.wiki.auth.acl.adv; + +import java.util.Set; + +/** + * A rule node is a tree like representation of a boolean logic statement used + * for access controls on a wiki page. + * + * @author AO + * @since 3.0.0 + */ +public abstract class RuleNode { + + /** + * evaluates if a given user/request is allowed to a specific page given + * their roles/attributes etc + * + * @param userRoles + * @return true if allowed, false otherwise + */ + public abstract boolean evaluate(java.util.Set userRoles); + + /** + * gets the complete list of all roles defined within the ACL statement. + * + * @return set of roles/usernames/attributes etc + */ + public abstract Set getAllRoles(); + +} diff --git a/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RuleParser.java b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RuleParser.java new file mode 100644 index 0000000000..6f82a8b6c4 --- /dev/null +++ b/jspwiki-main/src/main/java/org/apache/wiki/auth/acl/adv/RuleParser.java @@ -0,0 +1,109 @@ +/* + * Copyright 2025 The Apache Software Foundation. + * + * 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.apache.wiki.auth.acl.adv; + +import java.util.*; + +/** + * + * @author AO + */ + +public class RuleParser { + private final List tokens; + private int pos = 0; + + public RuleParser(String expr) { + this.tokens = tokenize(expr); + } + + public RuleNode parse() { + RuleNode node = parseOr(); + if (pos < tokens.size()) { + throw new IllegalArgumentException("Unexpected token: " + tokens.get(pos)); + } + return node; + } + + // Lowest precedence + private RuleNode parseOr() { + RuleNode left = parseAnd(); + while (match("OR")) { + RuleNode right = parseAnd(); + left = new OperatorNode(OperatorNode.Operator.OR, left, right); + } + return left; + } + + // Next precedence + private RuleNode parseAnd() { + RuleNode left = parseNot(); + while (match("AND")) { + RuleNode right = parseNot(); + left = new OperatorNode(OperatorNode.Operator.AND, left, right); + } + return left; + } + + // Handles unary NOT + private RuleNode parseNot() { + if (match("NOT")) { + return new NotNode(parseNot()); + } + return parsePrimary(); + } + + // Handles parentheses and role identifiers + private RuleNode parsePrimary() { + if (match("(")) { + RuleNode expr = parseOr(); + expect(")"); + return expr; + } + return new RoleNode(expectIdentifier()); + } + + // --- Helpers --- + private boolean match(String token) { + if (pos < tokens.size() && tokens.get(pos).equalsIgnoreCase(token)) { + pos++; + return true; + } + return false; + } + + private void expect(String token) { + if (!match(token)) { + throw new IllegalArgumentException("Expected '" + token + "'"); + } + } + + private String expectIdentifier() { + if (pos < tokens.size()) { + String tok = tokens.get(pos++); + if (!tok.equalsIgnoreCase("AND") && !tok.equalsIgnoreCase("OR") + && !tok.equalsIgnoreCase("NOT") + && !tok.equals("(") && !tok.equals(")")) { + return tok; + } + } + throw new IllegalArgumentException("Expected identifier at position " + pos); + } + + private static List tokenize(String expr) { + return Arrays.asList(expr.replaceAll("([()])", " $1 ").trim().split("\\s+")); + } +} diff --git a/jspwiki-main/src/test/java/org/apache/wiki/auth/acl/DefaultAclManagerTest.java b/jspwiki-main/src/test/java/org/apache/wiki/auth/acl/DefaultAclManagerTest.java index a6ce0348cb..b41a52f4fb 100644 --- a/jspwiki-main/src/test/java/org/apache/wiki/auth/acl/DefaultAclManagerTest.java +++ b/jspwiki-main/src/test/java/org/apache/wiki/auth/acl/DefaultAclManagerTest.java @@ -35,6 +35,9 @@ Licensed to the Apache Software Foundation (ASF) under one import java.security.Principal; import java.util.regex.Matcher; +import org.apache.wiki.auth.acl.adv.AdvancedAcl; +import org.apache.wiki.auth.acl.adv.RuleNode; +import org.mockito.internal.util.collections.Sets; public class DefaultAclManagerTest { @@ -44,6 +47,12 @@ public class DefaultAclManagerTest public void setUp() throws Exception { m_engine.saveText( "TestDefaultPage", "Foo" ); m_engine.saveText( "TestAclPage", "Bar. [{ALLOW edit Charlie, Herman}] " ); + + m_engine.saveText("TestAdvAclPage", "Bar. [{ALLOW edit Charlie OR Herman}] [{ALLOW view Charlie OR Herman}] "); + + m_engine.saveText("TestAdvAclPage2", "Bar. [{ALLOW edit Charlie OR Admin OR NOT Accounting}] "); + m_engine.saveText("TestAdvAclPage3", "Bar. [{ALLOW edit Group1 AND (Group2 OR Group3)}] "); + } @AfterEach @@ -51,6 +60,11 @@ public void tearDown() { try { m_engine.getManager( PageManager.class ).deletePage( "TestDefaultPage" ); m_engine.getManager( PageManager.class ).deletePage( "TestAclPage" ); + m_engine.getManager( PageManager.class ).deletePage( "TestAdvAclPage" ); + m_engine.getManager( PageManager.class ).deletePage( "TestAdvAclPage2" ); + m_engine.getManager( PageManager.class ).deletePage( "TestAdvAclPage3" ); + + } catch ( final ProviderException e ) { } } @@ -98,7 +112,77 @@ public void testGetPermissions() p = acl.findPrincipals( PermissionFactory.getPagePermission(page, "delete") ); Assertions.assertEquals( 0, p.length ); } + + @Test + public void testGetPermissionsAdvancedAcl() { + Page page = m_engine.getManager(PageManager.class).getPage("TestDefaultPage"); + AclManager mgr = m_engine.getManager(AclManager.class); + + page = m_engine.getManager(PageManager.class).getPage("TestAdvAclPage"); + AdvancedAcl aacl = (AdvancedAcl) mgr.getPermissions(page); + Assertions.assertNotNull(page.getAcl()); + Assertions.assertFalse(page.getAcl().isEmpty()); + + // Charlie is an editor; reading is therefore implied + RuleNode node = aacl.getNode(PermissionFactory.getPagePermission(page, "view")); + Assertions.assertTrue(node.evaluate(Sets.newSet("Charlie"))); + Assertions.assertTrue(node.evaluate(Sets.newSet("Herman"))); + + // Charlie should not be able to delete this page + Assertions.assertTrue(node.evaluate(Sets.newSet("Herman"))); + + // Herman should be in the ACL as an editor + node = aacl.getNode(PermissionFactory.getPagePermission(page, "edit")); + Assertions.assertTrue(node.evaluate(Sets.newSet("Herman"))); + + } + + @Test + public void testGetPermissionsAdvancedAcl2() { + Page page = m_engine.getManager(PageManager.class).getPage("TestAdvAclPage2"); + Assertions.assertNotNull(page.getAcl()); + Assertions.assertFalse(page.getAcl().isEmpty()); + AclManager mgr = m_engine.getManager(AclManager.class); + + AdvancedAcl acl = (AdvancedAcl) mgr.getPermissions(page); + Assertions.assertNotNull(page.getAcl()); + Assertions.assertFalse(page.getAcl().isEmpty()); + + // Charlie is an editor; reading is therefore implied + RuleNode node = acl.getNode(PermissionFactory.getPagePermission(page, "edit")); + Assertions.assertTrue(node.evaluate(Sets.newSet("Charlie"))); + Assertions.assertTrue(node.evaluate(Sets.newSet("Admin"))); + Assertions.assertFalse(node.evaluate(Sets.newSet("Accounting"))); + + + } + + @Test + public void testGetPermissionsAdvancedAcl3() { + Page page = m_engine.getManager(PageManager.class).getPage("TestAdvAclPage3"); + Assertions.assertNotNull(page.getAcl()); + Assertions.assertFalse(page.getAcl().isEmpty()); + AclManager mgr = m_engine.getManager(AclManager.class); + + AdvancedAcl acl = (AdvancedAcl) mgr.getPermissions(page); + Assertions.assertNotNull(page.getAcl()); + Assertions.assertFalse(page.getAcl().isEmpty()); + + // Charlie is an editor; reading is therefore implied + RuleNode node = acl.getNode(PermissionFactory.getPagePermission(page, "edit")); + Assertions.assertFalse(node.evaluate(Sets.newSet("Group1"))); + Assertions.assertFalse(node.evaluate(Sets.newSet("Group2"))); + Assertions.assertFalse(node.evaluate(Sets.newSet("Group3"))); + Assertions.assertTrue(node.evaluate(Sets.newSet("Group1", "Group2"))); + Assertions.assertTrue(node.evaluate(Sets.newSet("Group1", "Group3"))); + Assertions.assertTrue(node.evaluate(Sets.newSet("Group1", "Group2", "Group3"))); + Assertions.assertFalse(node.evaluate(Sets.newSet("Admin"))); + Assertions.assertFalse(node.evaluate(Sets.newSet("Accounting"))); + + + } + @Test public void testAclRegex() { diff --git a/jspwiki-main/src/test/java/org/apache/wiki/auth/acl/adv/RuleParserTest.java b/jspwiki-main/src/test/java/org/apache/wiki/auth/acl/adv/RuleParserTest.java new file mode 100644 index 0000000000..065e3a53a9 --- /dev/null +++ b/jspwiki-main/src/test/java/org/apache/wiki/auth/acl/adv/RuleParserTest.java @@ -0,0 +1,131 @@ +/* + * Copyright 2025 The Apache Software Foundation. + * + * 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.apache.wiki.auth.acl.adv; + +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +/** + * tests the rule parser for the advanced acl tool chain + * + * @see AdvancedAcl + * @since 3.0.0 + + */ +public class RuleParserTest { + + public RuleParserTest() { + } + + @Test + public void simpleOrStatement() { + RuleParser instance = new RuleParser("bob or mary"); + RuleNode result = instance.parse(); + Assertions.assertEquals(2, result.getAllRoles().size()); + Assertions.assertTrue(result.evaluate(Set.of("bob"))); + Assertions.assertTrue(result.evaluate(Set.of("mary"))); + Assertions.assertFalse(result.evaluate(Set.of("john"))); + } + + @Test + public void simpleAndStatement() { + RuleParser instance = new RuleParser("accounting AND finance"); + RuleNode result = instance.parse(); + Assertions.assertEquals(2, result.getAllRoles().size()); + Assertions.assertTrue(result.evaluate(Set.of("accounting", "finance"))); + Assertions.assertTrue(result.evaluate(Set.of("accounting", "finance", "otherDepartment"))); + Assertions.assertFalse(result.evaluate(Set.of("accounting"))); + Assertions.assertFalse(result.evaluate(Set.of("finance"))); + Assertions.assertFalse(result.evaluate(Set.of("john"))); + } + + @Test + public void complexSetup() { + RuleParser instance = new RuleParser("accounting AND (finance OR admin)"); + RuleNode result = instance.parse(); + Assertions.assertEquals(3, result.getAllRoles().size()); + Assertions.assertTrue(result.evaluate(Set.of("accounting", "finance"))); + Assertions.assertTrue(result.evaluate(Set.of("accounting", "finance", "otherDepartment"))); + Assertions.assertTrue(result.evaluate(Set.of("accounting", "finance", "admin"))); + Assertions.assertTrue(result.evaluate(Set.of("accounting", "admin"))); + Assertions.assertFalse(result.evaluate(Set.of("accounting"))); + Assertions.assertFalse(result.evaluate(Set.of("finance"))); + Assertions.assertFalse(result.evaluate(Set.of("john"))); + } + + @Test + public void complexSetupNot() { + RuleParser instance = new RuleParser("accounting AND finance AND NOT(bob)"); + instance = new RuleParser("(accounting AND finance AND NOT(bob))"); + RuleNode result = instance.parse(); + Assertions.assertEquals(3, result.getAllRoles().size()); + Assertions.assertTrue(result.evaluate(Set.of("accounting", "finance"))); + Assertions.assertTrue(result.evaluate(Set.of("accounting", "finance", "otherDepartment"))); + Assertions.assertFalse(result.evaluate(Set.of("accounting", "finance", "bob"))); + Assertions.assertFalse(result.evaluate(Set.of("accounting", "admin"))); + Assertions.assertFalse(result.evaluate(Set.of("accounting"))); + Assertions.assertFalse(result.evaluate(Set.of("finance"))); + Assertions.assertFalse(result.evaluate(Set.of("john"))); + } + + @Test + public void mismatchedParan() { + Assertions.assertThrows(IllegalArgumentException.class, () -> { + RuleParser instance = new RuleParser("(accounting AND finance AND NOT(bob)"); + RuleNode parse = instance.parse(); + System.out.println(parse); + }); + + RuleParser instance = new RuleParser("(accounting AND finance AND NOT(bob))"); + instance.parse(); + + List invalidExprs = List.of( + "role1 AND", + "AND role1", + "role1 OR OR role2", + "role1 (role2 OR role3)", + "(role1 AND role2", + "role1 AND role2)", + "role1 AND NOT", + "NOT", + "role1 OR AND role2", + "()", + "( )", + "role1 AND (OR role2)", + "role1 AND (role2 OR)", + "role1 role2", + "role1 AND (NOT)", + "(role1 AND) OR role2", + "role1 AND ((role2)", + "role1 AND )role2(", + "role1 AND NOT ( )", + "NOT (role1 AND)" + ); + + for (String expr : invalidExprs) { + try { + RuleParser parser = new RuleParser(expr); + parser.parse(); + System.out.println("❌ Should have failed but parsed: " + expr); + } catch (Exception e) { + System.out.println("✅ Correctly failed: " + expr + " → " + e.getMessage()); + } + } + } + +}