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