001/* 
002    Licensed to the Apache Software Foundation (ASF) under one
003    or more contributor license agreements.  See the NOTICE file
004    distributed with this work for additional information
005    regarding copyright ownership.  The ASF licenses this file
006    to you under the Apache License, Version 2.0 (the
007    "License"); you may not use this file except in compliance
008    with the License.  You may obtain a copy of the License at
009
010       http://www.apache.org/licenses/LICENSE-2.0
011
012    Unless required by applicable law or agreed to in writing,
013    software distributed under the License is distributed on an
014    "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015    KIND, either express or implied.  See the License for the
016    specific language governing permissions and limitations
017    under the License.  
018 */
019package org.apache.wiki.auth.acl;
020
021import java.security.Permission;
022import java.security.Principal;
023import java.util.ArrayList;
024import java.util.Collections;
025import java.util.Enumeration;
026import java.util.List;
027import java.util.Map;
028import java.util.NoSuchElementException;
029import java.util.Properties;
030import java.util.StringTokenizer;
031import java.util.TreeMap;
032import java.util.regex.Matcher;
033import java.util.regex.Pattern;
034
035import org.apache.log4j.Logger;
036import org.apache.wiki.PageLock;
037import org.apache.wiki.PageManager;
038import org.apache.wiki.WikiContext;
039import org.apache.wiki.WikiEngine;
040import org.apache.wiki.WikiPage;
041import org.apache.wiki.api.exceptions.ProviderException;
042import org.apache.wiki.attachment.Attachment;
043import org.apache.wiki.auth.AuthorizationManager;
044import org.apache.wiki.auth.PrincipalComparator;
045import org.apache.wiki.auth.WikiSecurityException;
046import org.apache.wiki.auth.permissions.PagePermission;
047import org.apache.wiki.auth.permissions.PermissionFactory;
048import org.apache.wiki.render.RenderingManager;
049
050/**
051 * Default implementation that parses Acls from wiki page markup.
052 *
053 * @since 2.3
054 */
055public class DefaultAclManager implements AclManager {
056
057    private static final Logger log = Logger.getLogger(DefaultAclManager.class);
058
059    private AuthorizationManager m_auth = null;
060    private WikiEngine m_engine = null;
061    private static final String PERM_REGEX = "(" +
062            PagePermission.COMMENT_ACTION + "|" +
063            PagePermission.DELETE_ACTION + "|" +
064            PagePermission.EDIT_ACTION + "|" +
065            PagePermission.MODIFY_ACTION + "|" +
066            PagePermission.RENAME_ACTION + "|" +
067            PagePermission.UPLOAD_ACTION + "|" +
068            PagePermission.VIEW_ACTION + ")";
069    private static final String ACL_REGEX = "\\[\\{\\s*ALLOW\\s+" + PERM_REGEX + "\\s*(.*?)\\s*\\}\\]";
070
071    /**
072     * Identifies ACL strings in wiki text; the first group is the action (view, edit) and
073     * the second is the list of Principals separated by commas. The overall match is
074     * the ACL string from [{ to }].
075     */
076    public static final Pattern ACL_PATTERN = Pattern.compile(ACL_REGEX);
077
078    /**
079     * Initializes the AclManager with a supplied wiki engine and properties.
080     *
081     * @param engine the wiki engine
082     * @param props  the initialization properties
083     * @see org.apache.wiki.auth.acl.AclManager#initialize(org.apache.wiki.WikiEngine,
084     *      java.util.Properties)
085     */
086    public void initialize(WikiEngine engine, Properties props) {
087        m_auth = engine.getAuthorizationManager();
088        m_engine = engine;
089    }
090
091    /**
092     * A helper method for parsing textual AccessControlLists. The line is in
093     * form "ALLOW <permission> <principal>, <principal>, <principal>". This
094     * method was moved from Authorizer.
095     *
096     * @param page     The current wiki page. If the page already has an ACL, it
097     *                 will be used as a basis for this ACL in order to avoid the
098     *                 creation of a new one.
099     * @param ruleLine The rule line, as described above.
100     * @return A valid Access Control List. May be empty.
101     * @throws WikiSecurityException if the ruleLine was faulty somehow.
102     * @since 2.1.121
103     */
104    public Acl parseAcl(WikiPage page, String ruleLine) throws WikiSecurityException {
105        Acl acl = page.getAcl();
106        if (acl == null) {
107            acl = new AclImpl();
108        }
109
110        try {
111            StringTokenizer fieldToks = new StringTokenizer(ruleLine);
112            fieldToks.nextToken();
113            String actions = fieldToks.nextToken();
114            page.getName();
115
116            while (fieldToks.hasMoreTokens()) {
117                String principalName = fieldToks.nextToken(",").trim();
118                Principal principal = m_auth.resolvePrincipal(principalName);
119                AclEntry oldEntry = acl.getEntry(principal);
120
121                if (oldEntry != null) {
122                    log.debug("Adding to old acl list: " + principal + ", " + actions);
123                    oldEntry.addPermission(PermissionFactory.getPagePermission(page, actions));
124                } else {
125                    log.debug("Adding new acl entry for " + actions);
126                    AclEntry entry = new AclEntryImpl();
127
128                    entry.setPrincipal(principal);
129                    entry.addPermission(PermissionFactory.getPagePermission(page, actions));
130
131                    acl.addEntry(entry);
132                }
133            }
134
135            page.setAcl(acl);
136
137            log.debug(acl.toString());
138        } catch (NoSuchElementException nsee) {
139            log.warn("Invalid access rule: " + ruleLine + " - defaults will be used.");
140            throw new WikiSecurityException("Invalid access rule: " + ruleLine, nsee);
141        } catch (IllegalArgumentException iae) {
142            throw new WikiSecurityException("Invalid permission type: " + ruleLine, iae);
143        }
144
145        return acl;
146    }
147
148
149    /**
150     * Returns the access control list for the page.
151     * If the ACL has not been parsed yet, it is done
152     * on-the-fly. If the page has a parent page, then that is tried also.
153     * This method was moved from Authorizer;
154     * it was consolidated with some code from AuthorizationManager.
155     * This method is guaranteed to return a non-<code>null</code> Acl.
156     *
157     * @param page the page
158     * @return the Acl representing permissions for the page
159     * @since 2.2.121
160     */
161    public Acl getPermissions(WikiPage page) {
162        //
163        //  Does the page already have cached ACLs?
164        //
165        Acl acl = page.getAcl();
166        log.debug("page=" + page.getName() + "\n" + acl);
167
168        if (acl == null) {
169            //
170            //  If null, try the parent.
171            //
172            if (page instanceof Attachment) {
173                WikiPage parent = m_engine.getPage(((Attachment) page).getParentName());
174
175                acl = getPermissions(parent);
176            } else {
177                //
178                //  Or, try parsing the page
179                //
180                WikiContext ctx = new WikiContext(m_engine, page);
181
182                ctx.setVariable(RenderingManager.VAR_EXECUTE_PLUGINS, Boolean.FALSE);
183
184                m_engine.getHTML(ctx, page);
185
186                page = m_engine.getPage(page.getName(), page.getVersion());
187                acl = page.getAcl();
188
189                if (acl == null) {
190                    acl = new AclImpl();
191                    page.setAcl(acl);
192                }
193            }
194        }
195
196        return acl;
197    }
198
199    /**
200     * Sets the access control list for the page and persists it by prepending
201     * it to the wiki page markup and saving the page. When this method is
202     * called, all other ACL markup in the page is removed. This method will forcibly
203     * expire locks on the wiki page if they exist. Any ProviderExceptions will be
204     * re-thrown as WikiSecurityExceptions.
205     *
206     * @param page the wiki page
207     * @param acl  the access control list
208     * @throws WikiSecurityException of the Acl cannot be set
209     * @since 2.5
210     */
211    public void setPermissions(WikiPage page, Acl acl) throws WikiSecurityException {
212        PageManager pageManager = m_engine.getPageManager();
213
214        // Forcibly expire any page locks
215        PageLock lock = pageManager.getCurrentLock(page);
216        if (lock != null) {
217            pageManager.unlockPage(lock);
218        }
219
220        // Remove all of the existing ACLs.
221        String pageText = m_engine.getPureText(page);
222        Matcher matcher = DefaultAclManager.ACL_PATTERN.matcher(pageText);
223        String cleansedText = matcher.replaceAll("");
224        String newText = DefaultAclManager.printAcl(page.getAcl()) + cleansedText;
225        try {
226            pageManager.putPageText(page, newText);
227        } catch (ProviderException e) {
228            throw new WikiSecurityException("Could not set Acl. Reason: ProviderExcpetion " + e.getMessage(), e);
229        }
230    }
231
232    /**
233     * Generates an ACL string for inclusion in a wiki page, based on a supplied Acl object.
234     * All of the permissions in this Acl are assumed to apply to the same page scope.
235     * The names of the pages are ignored; only the actions and principals matter.
236     *
237     * @param acl the ACL
238     * @return the ACL string
239     */
240    protected static String printAcl(Acl acl) {
241        // Extract the ACL entries into a Map with keys == permissions, values == principals
242        Map<String, List<Principal>> permissionPrincipals = new TreeMap<String, List<Principal>>();
243        Enumeration<AclEntry> entries = acl.entries();
244        while (entries.hasMoreElements()) {
245            AclEntry entry = entries.nextElement();
246            Principal principal = entry.getPrincipal();
247            Enumeration<Permission> permissions = entry.permissions();
248            while (permissions.hasMoreElements()) {
249                Permission permission = permissions.nextElement();
250                List<Principal> principals = permissionPrincipals.get(permission.getActions());
251                if (principals == null) {
252                    principals = new ArrayList<Principal>();
253                    String action = permission.getActions();
254                    if (action.indexOf(',') != -1) {
255                        throw new IllegalStateException("AclEntry permission cannot have multiple targets.");
256                    }
257                    permissionPrincipals.put(action, principals);
258                }
259                principals.add(principal);
260            }
261        }
262
263        // Now, iterate through each permission in the map and generate an ACL string
264
265        StringBuilder s = new StringBuilder();
266        for (Map.Entry<String, List<Principal>> entry : permissionPrincipals.entrySet()) {
267            String action = entry.getKey();
268            List<Principal> principals = entry.getValue();
269            Collections.sort(principals, new PrincipalComparator());
270            s.append("[{ALLOW ");
271            s.append(action);
272            s.append(" ");
273            for (int i = 0; i < principals.size(); i++) {
274                Principal principal = principals.get(i);
275                s.append(principal.getName());
276                if (i < (principals.size() - 1)) {
277                    s.append(",");
278                }
279            }
280            s.append("}]\n");
281        }
282        return s.toString();
283    }
284
285}