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.WikiContext;
037import org.apache.wiki.WikiEngine;
038import org.apache.wiki.WikiPage;
039import org.apache.wiki.api.exceptions.ProviderException;
040import org.apache.wiki.attachment.Attachment;
041import org.apache.wiki.auth.AuthorizationManager;
042import org.apache.wiki.auth.PrincipalComparator;
043import org.apache.wiki.auth.WikiSecurityException;
044import org.apache.wiki.auth.permissions.PagePermission;
045import org.apache.wiki.auth.permissions.PermissionFactory;
046import org.apache.wiki.pages.PageLock;
047import org.apache.wiki.pages.PageManager;
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                if (page.getAcl() == null) {
187                    page.setAcl( new AclImpl() );
188                }
189                acl = page.getAcl();
190            }
191        }
192
193        return acl;
194    }
195
196    /**
197     * Sets the access control list for the page and persists it by prepending
198     * it to the wiki page markup and saving the page. When this method is
199     * called, all other ACL markup in the page is removed. This method will forcibly
200     * expire locks on the wiki page if they exist. Any ProviderExceptions will be
201     * re-thrown as WikiSecurityExceptions.
202     *
203     * @param page the wiki page
204     * @param acl  the access control list
205     * @throws WikiSecurityException of the Acl cannot be set
206     * @since 2.5
207     */
208    public void setPermissions(WikiPage page, Acl acl) throws WikiSecurityException {
209        PageManager pageManager = m_engine.getPageManager();
210
211        // Forcibly expire any page locks
212        PageLock lock = pageManager.getCurrentLock(page);
213        if (lock != null) {
214            pageManager.unlockPage(lock);
215        }
216
217        // Remove all of the existing ACLs.
218        String pageText = m_engine.getPureText(page);
219        Matcher matcher = DefaultAclManager.ACL_PATTERN.matcher(pageText);
220        String cleansedText = matcher.replaceAll("");
221        String newText = DefaultAclManager.printAcl(page.getAcl()) + cleansedText;
222        try {
223            pageManager.putPageText(page, newText);
224        } catch (ProviderException e) {
225            throw new WikiSecurityException("Could not set Acl. Reason: ProviderExcpetion " + e.getMessage(), e);
226        }
227    }
228
229    /**
230     * Generates an ACL string for inclusion in a wiki page, based on a supplied Acl object.
231     * All of the permissions in this Acl are assumed to apply to the same page scope.
232     * The names of the pages are ignored; only the actions and principals matter.
233     *
234     * @param acl the ACL
235     * @return the ACL string
236     */
237    protected static String printAcl(Acl acl) {
238        // Extract the ACL entries into a Map with keys == permissions, values == principals
239        Map<String, List<Principal>> permissionPrincipals = new TreeMap<String, List<Principal>>();
240        Enumeration<AclEntry> entries = acl.entries();
241        while (entries.hasMoreElements()) {
242            AclEntry entry = entries.nextElement();
243            Principal principal = entry.getPrincipal();
244            Enumeration<Permission> permissions = entry.permissions();
245            while (permissions.hasMoreElements()) {
246                Permission permission = permissions.nextElement();
247                List<Principal> principals = permissionPrincipals.get(permission.getActions());
248                if (principals == null) {
249                    principals = new ArrayList<Principal>();
250                    String action = permission.getActions();
251                    if (action.indexOf(',') != -1) {
252                        throw new IllegalStateException("AclEntry permission cannot have multiple targets.");
253                    }
254                    permissionPrincipals.put(action, principals);
255                }
256                principals.add(principal);
257            }
258        }
259
260        // Now, iterate through each permission in the map and generate an ACL string
261
262        StringBuilder s = new StringBuilder();
263        for (Map.Entry<String, List<Principal>> entry : permissionPrincipals.entrySet()) {
264            String action = entry.getKey();
265            List<Principal> principals = entry.getValue();
266            Collections.sort(principals, new PrincipalComparator());
267            s.append("[{ALLOW ");
268            s.append(action);
269            s.append(" ");
270            for (int i = 0; i < principals.size(); i++) {
271                Principal principal = principals.get(i);
272                s.append(principal.getName());
273                if (i < (principals.size() - 1)) {
274                    s.append(",");
275                }
276            }
277            s.append("}]\n");
278        }
279        return s.toString();
280    }
281
282}