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;
020
021import org.apache.log4j.Logger;
022import org.apache.wiki.api.core.Acl;
023import org.apache.wiki.api.core.AclEntry;
024import org.apache.wiki.api.core.Context;
025import org.apache.wiki.api.core.ContextEnum;
026import org.apache.wiki.api.core.Engine;
027import org.apache.wiki.api.core.Page;
028import org.apache.wiki.api.core.Session;
029import org.apache.wiki.api.exceptions.NoRequiredPropertyException;
030import org.apache.wiki.api.exceptions.WikiException;
031import org.apache.wiki.auth.acl.AclManager;
032import org.apache.wiki.auth.acl.UnresolvedPrincipal;
033import org.apache.wiki.auth.authorize.GroupManager;
034import org.apache.wiki.auth.authorize.Role;
035import org.apache.wiki.auth.permissions.AllPermission;
036import org.apache.wiki.auth.permissions.PagePermission;
037import org.apache.wiki.auth.user.UserDatabase;
038import org.apache.wiki.auth.user.UserProfile;
039import org.apache.wiki.event.WikiEventListener;
040import org.apache.wiki.event.WikiEventManager;
041import org.apache.wiki.event.WikiSecurityEvent;
042import org.apache.wiki.i18n.InternationalizationManager;
043import org.apache.wiki.pages.PageManager;
044import org.apache.wiki.preferences.Preferences;
045import org.apache.wiki.util.ClassUtil;
046import org.freshcookies.security.policy.LocalPolicy;
047
048import javax.servlet.http.HttpServletResponse;
049import java.io.File;
050import java.io.IOException;
051import java.net.URL;
052import java.security.AccessControlException;
053import java.security.AccessController;
054import java.security.CodeSource;
055import java.security.Permission;
056import java.security.Principal;
057import java.security.PrivilegedAction;
058import java.security.ProtectionDomain;
059import java.security.cert.Certificate;
060import java.text.MessageFormat;
061import java.util.Arrays;
062import java.util.Map;
063import java.util.Properties;
064import java.util.ResourceBundle;
065import java.util.WeakHashMap;
066
067
068/**
069 * <p>Default implementation for {@link AuthorizationManager}</p>
070 * {@inheritDoc}
071 *
072 * <p>See the {@link #checkPermission(Session, Permission)} and {@link #hasRoleOrPrincipal(Session, Principal)} methods for more
073 * information on the authorization logic.</p>
074 * @since 2.3
075 * @see AuthenticationManager
076 */
077public class DefaultAuthorizationManager implements AuthorizationManager {
078
079    private static final Logger log = Logger.getLogger( DefaultAuthorizationManager.class );
080
081    private Authorizer m_authorizer = null;
082
083    /** Cache for storing ProtectionDomains used to evaluate the local policy. */
084    private Map< Principal, ProtectionDomain > m_cachedPds = new WeakHashMap<>();
085
086    private Engine m_engine = null;
087
088    private LocalPolicy m_localPolicy = null;
089
090    /**
091     * Constructs a new DefaultAuthorizationManager instance.
092     */
093    public DefaultAuthorizationManager() {
094    }
095
096    /** {@inheritDoc} */
097    @Override
098    public boolean checkPermission( final Session session, final Permission permission ) {
099        // A slight sanity check.
100        if( session == null || permission == null ) {
101            fireEvent( WikiSecurityEvent.ACCESS_DENIED, null, permission );
102            return false;
103        }
104
105        final Principal user = session.getLoginPrincipal();
106
107        // Always allow the action if user has AllPermission
108        final Permission allPermission = new AllPermission( m_engine.getApplicationName() );
109        final boolean hasAllPermission = checkStaticPermission( session, allPermission );
110        if( hasAllPermission ) {
111            fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission );
112            return true;
113        }
114
115        // If the user doesn't have *at least* the permission granted by policy, return false.
116        final boolean hasPolicyPermission = checkStaticPermission( session, permission );
117        if( !hasPolicyPermission ) {
118            fireEvent( WikiSecurityEvent.ACCESS_DENIED, user, permission );
119            return false;
120        }
121
122        // If this isn't a PagePermission, it's allowed
123        if( !( permission instanceof PagePermission ) ) {
124            fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission );
125            return true;
126        }
127
128        // If the page or ACL is null, it's allowed.
129        final String pageName = ((PagePermission)permission).getPage();
130        final Page page = m_engine.getManager( PageManager.class ).getPage( pageName );
131        final Acl acl = ( page == null) ? null : m_engine.getManager( AclManager.class ).getPermissions( page );
132        if( page == null ||  acl == null || acl.isEmpty() ) {
133            fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission );
134            return true;
135        }
136
137        // Next, iterate through the Principal objects assigned this permission. If the context's subject possesses
138        // any of these, the action is allowed.
139        final Principal[] aclPrincipals = acl.findPrincipals( permission );
140
141        log.debug( "Checking ACL entries..." );
142        log.debug( "Acl for this page is: " + acl );
143        log.debug( "Checking for principal: " + Arrays.toString( aclPrincipals ) );
144        log.debug( "Permission: " + permission );
145
146        for( Principal aclPrincipal : aclPrincipals ) {
147            // If the ACL principal we're looking at is unresolved, try to resolve it here & correct the Acl
148            if ( aclPrincipal instanceof UnresolvedPrincipal ) {
149                final AclEntry aclEntry = acl.getAclEntry( aclPrincipal );
150                aclPrincipal = resolvePrincipal( aclPrincipal.getName() );
151                if ( aclEntry != null && !( aclPrincipal instanceof UnresolvedPrincipal ) ) {
152                    aclEntry.setPrincipal( aclPrincipal );
153                }
154            }
155
156            if ( hasRoleOrPrincipal( session, aclPrincipal ) ) {
157                fireEvent( WikiSecurityEvent.ACCESS_ALLOWED, user, permission );
158                return true;
159            }
160        }
161        fireEvent( WikiSecurityEvent.ACCESS_DENIED, user, permission );
162        return false;
163    }
164
165    /** {@inheritDoc} */
166    @Override
167    public Authorizer getAuthorizer() throws WikiSecurityException {
168        if ( m_authorizer != null ) {
169            return m_authorizer;
170        }
171        throw new WikiSecurityException( "Authorizer did not initialize properly. Check the logs." );
172    }
173
174    /** {@inheritDoc} */
175    @Override
176    public boolean hasRoleOrPrincipal( final Session session, final Principal principal ) {
177        // If either parameter is null, always deny
178        if( session == null || principal == null ) {
179            return false;
180        }
181
182        // If principal is role, delegate to isUserInRole
183        if( AuthenticationManager.isRolePrincipal( principal ) ) {
184            return isUserInRole( session, principal );
185        }
186
187        // We must be looking for a user principal, assuming that the user has been properly logged in. So just look for a name match.
188        if( session.isAuthenticated() && AuthenticationManager.isUserPrincipal( principal ) ) {
189            final String principalName = principal.getName();
190            final Principal[] userPrincipals = session.getPrincipals();
191            for( final Principal userPrincipal : userPrincipals ) {
192                if( userPrincipal.getName().equals( principalName ) ) {
193                    return true;
194                }
195            }
196        }
197        return false;
198    }
199
200    /** {@inheritDoc} */
201    @Override
202    public boolean hasAccess( final Context context, final HttpServletResponse response, final boolean redirect ) throws IOException {
203        final boolean allowed = checkPermission( context.getWikiSession(), context.requiredPermission() );
204        final ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE );
205
206        // Stash the wiki context
207        if ( context.getHttpRequest() != null && context.getHttpRequest().getAttribute( Context.ATTR_CONTEXT ) == null ) {
208            context.getHttpRequest().setAttribute( Context.ATTR_CONTEXT, context );
209        }
210
211        // If access not allowed, redirect
212        if( !allowed && redirect ) {
213            final Principal currentUser  = context.getWikiSession().getUserPrincipal();
214            final String pageurl = context.getPage().getName();
215            if( context.getWikiSession().isAuthenticated() ) {
216                log.info( "User " + currentUser.getName() + " has no access - forbidden (permission=" + context.requiredPermission() + ")" );
217                context.getWikiSession().addMessage( MessageFormat.format( rb.getString( "security.error.noaccess.logged" ),
218                                                     context.getName()) );
219            } else {
220                log.info( "User " + currentUser.getName() + " has no access - redirecting (permission=" + context.requiredPermission() + ")" );
221                context.getWikiSession().addMessage( MessageFormat.format( rb.getString("security.error.noaccess"), context.getName() ) );
222            }
223            response.sendRedirect( m_engine.getURL( ContextEnum.WIKI_LOGIN.getRequestContext(), pageurl, null ) );
224        }
225        return allowed;
226    }
227
228    /**
229     * {@inheritDoc}
230     *
231     * Expects to find property 'jspwiki.authorizer' with a valid Authorizer implementation name to take care of role lookup operations.
232     */
233    @Override
234    public void initialize( final Engine engine, final Properties properties ) throws WikiException {
235        m_engine = engine;
236
237        //  JAAS authorization continues
238        m_authorizer = getAuthorizerImplementation( properties );
239        m_authorizer.initialize( engine, properties );
240
241        // Initialize local security policy
242        try {
243            final String policyFileName = properties.getProperty( POLICY, DEFAULT_POLICY );
244            final URL policyURL = engine.findConfigFile( policyFileName );
245
246            if (policyURL != null) {
247                final File policyFile = new File( policyURL.toURI().getPath() );
248                log.info("We found security policy URL: " + policyURL + " and transformed it to file " + policyFile.getAbsolutePath());
249                m_localPolicy = new LocalPolicy( policyFile, engine.getContentEncoding().displayName() );
250                m_localPolicy.refresh();
251                log.info( "Initialized default security policy: " + policyFile.getAbsolutePath() );
252            } else {
253                final String sb = "JSPWiki was unable to initialize the default security policy (WEB-INF/jspwiki.policy) file. " +
254                                  "Please ensure that the jspwiki.policy file exists in the default location. " +
255                                  "This file should exist regardless of the existance of a global policy file. " +
256                                  "The global policy file is identified by the java.security.policy variable. ";
257                final WikiSecurityException wse = new WikiSecurityException( sb );
258                log.fatal( sb, wse );
259                throw wse;
260            }
261        } catch ( final Exception e) {
262            log.error("Could not initialize local security policy: " + e.getMessage() );
263            throw new WikiException( "Could not initialize local security policy: " + e.getMessage(), e );
264        }
265    }
266
267    /**
268     * Attempts to locate and initialize a Authorizer to use with this manager. Throws a WikiException if no entry is found, or if one
269     * fails to initialize.
270     *
271     * @param props jspwiki.properties, containing a 'jspwiki.authorization.provider' class name.
272     * @return a Authorizer used to get page authorization information.
273     * @throws WikiException if there are problems finding the authorizer implementation.
274     */
275    private Authorizer getAuthorizerImplementation( final Properties props ) throws WikiException {
276        final String authClassName = props.getProperty( PROP_AUTHORIZER, DEFAULT_AUTHORIZER );
277        return ( Authorizer )locateImplementation( authClassName );
278    }
279
280    private Object locateImplementation( final String clazz ) throws WikiException {
281        if ( clazz != null ) {
282            try {
283                final Class< ? > authClass = ClassUtil.findClass( "org.apache.wiki.auth.authorize", clazz );
284                return authClass.newInstance();
285            } catch( final ClassNotFoundException e ) {
286                log.fatal( "Authorizer " + clazz + " cannot be found", e );
287                throw new WikiException( "Authorizer " + clazz + " cannot be found", e );
288            } catch( final InstantiationException e ) {
289                log.fatal( "Authorizer " + clazz + " cannot be created", e );
290                throw new WikiException( "Authorizer " + clazz + " cannot be created", e );
291            } catch( final IllegalAccessException e ) {
292                log.fatal( "You are not allowed to access this authorizer class", e );
293                throw new WikiException( "You are not allowed to access this authorizer class", e );
294            }
295        }
296
297        throw new NoRequiredPropertyException( "Unable to find a " + PROP_AUTHORIZER + " entry in the properties.", PROP_AUTHORIZER );
298    }
299
300    /** {@inheritDoc} */
301    @Override
302    public boolean allowedByLocalPolicy( final Principal[] principals, final Permission permission ) {
303        for ( final Principal principal : principals ) {
304            // Get ProtectionDomain for this Principal from cache, or create new one
305            ProtectionDomain pd = m_cachedPds.get( principal );
306            if ( pd == null ) {
307                final ClassLoader cl = this.getClass().getClassLoader();
308                final CodeSource cs = new CodeSource( null, (Certificate[])null );
309                pd = new ProtectionDomain( cs, null, cl, new Principal[]{ principal } );
310                m_cachedPds.put( principal, pd );
311            }
312
313            // Consult the local policy and get the answer
314            if ( m_localPolicy.implies( pd, permission ) ) {
315                return true;
316            }
317        }
318        return false;
319    }
320
321    /** {@inheritDoc} */
322    @Override
323    public boolean checkStaticPermission( final Session session, final Permission permission ) {
324        return ( Boolean )Session.doPrivileged( session, ( PrivilegedAction< Boolean > )() -> {
325            try {
326                // Check the JVM-wide security policy first
327                AccessController.checkPermission( permission );
328                return Boolean.TRUE;
329            } catch( final AccessControlException e ) {
330                // Global policy denied the permission
331            }
332
333            // Try the local policy - check each Role/Group and User Principal
334            if ( allowedByLocalPolicy( session.getRoles(), permission ) || allowedByLocalPolicy( session.getPrincipals(), permission ) ) {
335                return Boolean.TRUE;
336            }
337            return Boolean.FALSE;
338        } );
339    }
340
341    /** {@inheritDoc} */
342    @Override
343    public Principal resolvePrincipal( final String name ) {
344        // Check built-in Roles first
345        final Role role = new Role(name);
346        if ( Role.isBuiltInRole( role ) ) {
347            return role;
348        }
349
350        // Check Authorizer Roles
351        Principal principal = m_authorizer.findRole( name );
352        if ( principal != null ) {
353            return principal;
354        }
355
356        // Check Groups
357        principal = m_engine.getManager( GroupManager.class ).findRole( name );
358        if ( principal != null ) {
359            return principal;
360        }
361
362        // Ok, no luck---this must be a user principal
363        final Principal[] principals;
364        final UserProfile profile;
365        final UserDatabase db = m_engine.getManager( UserManager.class ).getUserDatabase();
366        try {
367            profile = db.find( name );
368            principals = db.getPrincipals( profile.getLoginName() );
369            for( final Principal value : principals ) {
370                principal = value;
371                if( principal.getName().equals( name ) ) {
372                    return principal;
373                }
374            }
375        } catch( final NoSuchPrincipalException e ) {
376            // We couldn't find the user...
377        }
378        // Ok, no luck---mark this as unresolved and move on
379        return new UnresolvedPrincipal( name );
380    }
381
382
383    // events processing .......................................................
384
385    /** {@inheritDoc} */
386    @Override
387    public synchronized void addWikiEventListener( final WikiEventListener listener ) {
388        WikiEventManager.addWikiEventListener( this, listener );
389    }
390
391    /** {@inheritDoc} */
392    @Override
393    public synchronized void removeWikiEventListener( final WikiEventListener listener ) {
394        WikiEventManager.removeWikiEventListener( this, listener );
395    }
396
397}