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            return Arrays.stream(userPrincipals).anyMatch(userPrincipal -> userPrincipal.getName().equals(principalName));
193        }
194        return false;
195    }
196
197    /** {@inheritDoc} */
198    @Override
199    public boolean hasAccess( final Context context, final HttpServletResponse response, final boolean redirect ) throws IOException {
200        final boolean allowed = checkPermission( context.getWikiSession(), context.requiredPermission() );
201
202        // Stash the wiki context
203        if ( context.getHttpRequest() != null && context.getHttpRequest().getAttribute( Context.ATTR_CONTEXT ) == null ) {
204            context.getHttpRequest().setAttribute( Context.ATTR_CONTEXT, context );
205        }
206
207        // If access not allowed, redirect
208        if( !allowed && redirect ) {
209            final ResourceBundle rb = Preferences.getBundle( context, InternationalizationManager.CORE_BUNDLE );
210            final Principal currentUser  = context.getWikiSession().getUserPrincipal();
211            final String pageurl = context.getPage().getName();
212            if( context.getWikiSession().isAuthenticated() ) {
213                LOG.info( "User {} has no access - forbidden (permission={})", currentUser.getName(), context.requiredPermission() );
214                context.getWikiSession().addMessage( MessageFormat.format( rb.getString( "security.error.noaccess.logged" ), context.getName()) );
215            } else {
216                LOG.info( "User {} has no access - redirecting (permission={})", currentUser.getName(), context.requiredPermission() );
217                context.getWikiSession().addMessage( MessageFormat.format( rb.getString( "security.error.noaccess" ), context.getName() ) );
218            }
219            response.sendRedirect( m_engine.getURL( ContextEnum.WIKI_LOGIN.getRequestContext(), pageurl, null ) );
220        }
221        return allowed;
222    }
223
224    /**
225     * {@inheritDoc}
226     *
227     * Expects to find property 'jspwiki.authorizer' with a valid Authorizer implementation name to take care of role lookup operations.
228     */
229    @Override
230    public void initialize( final Engine engine, final Properties properties ) throws WikiException {
231        m_engine = engine;
232
233        //  JAAS authorization continues
234        m_authorizer = getAuthorizerImplementation( properties );
235        m_authorizer.initialize( engine, properties );
236
237        // Initialize local security policy
238        try {
239            final String policyFileName = properties.getProperty( POLICY, DEFAULT_POLICY );
240            final URL policyURL = engine.findConfigFile( policyFileName );
241
242            if (policyURL != null) {
243                final File policyFile = new File( policyURL.toURI().getPath() );
244                LOG.info("We found security policy URL: {} and transformed it to file {}",policyURL, policyFile.getAbsolutePath());
245                m_localPolicy = new LocalPolicy( policyFile, engine.getContentEncoding().displayName() );
246                m_localPolicy.refresh();
247                LOG.info( "Initialized default security policy: {}", policyFile.getAbsolutePath() );
248            } else {
249                final String sb = "JSPWiki was unable to initialize the default security policy (WEB-INF/jspwiki.policy) file. " +
250                                  "Please ensure that the jspwiki.policy file exists in the default location. " +
251                                  "This file should exist regardless of the existance of a global policy file. " +
252                                  "The global policy file is identified by the java.security.policy variable. ";
253                final WikiSecurityException wse = new WikiSecurityException( sb );
254                LOG.fatal( sb, wse );
255                throw wse;
256            }
257        } catch ( final Exception e) {
258            LOG.error("Could not initialize local security policy: {}", e.getMessage() );
259            throw new WikiException( "Could not initialize local security policy: " + e.getMessage(), e );
260        }
261    }
262
263    /**
264     * Attempts to locate and initialize an Authorizer to use with this manager. Throws a WikiException if no entry is found, or if one
265     * fails to initialize.
266     *
267     * @param props jspwiki.properties, containing a 'jspwiki.authorization.provider' class name.
268     * @return an Authorizer used to get page authorization information.
269     * @throws WikiException if there are problems finding the authorizer implementation.
270     */
271    private Authorizer getAuthorizerImplementation( final Properties props ) throws WikiException {
272        final String authClassName = props.getProperty( PROP_AUTHORIZER, DEFAULT_AUTHORIZER );
273        return locateImplementation( authClassName );
274    }
275
276    private Authorizer locateImplementation( final String clazz ) throws WikiException {
277        if ( clazz != null ) {
278            try {
279                return ClassUtil.buildInstance( "org.apache.wiki.auth.authorize", clazz );
280            } catch( final ReflectiveOperationException e ) {
281                LOG.fatal( "Authorizer {} cannot be instantiated", clazz, e );
282                throw new WikiException( "Authorizer " + clazz + " cannot be instantiated", e );
283            }
284        }
285
286        throw new NoRequiredPropertyException( "Unable to find a " + PROP_AUTHORIZER + " entry in the properties.", PROP_AUTHORIZER );
287    }
288
289    /** {@inheritDoc} */
290    @Override
291    public boolean allowedByLocalPolicy( final Principal[] principals, final Permission permission ) {
292        for ( final Principal principal : principals ) {
293            // Get ProtectionDomain for this Principal from cache, or create new one
294            ProtectionDomain pd = m_cachedPds.get( principal );
295            if ( pd == null ) {
296                final ClassLoader cl = this.getClass().getClassLoader();
297                final CodeSource cs = new CodeSource( null, (Certificate[])null );
298                pd = new ProtectionDomain( cs, null, cl, new Principal[]{ principal } );
299                m_cachedPds.put( principal, pd );
300            }
301
302            // Consult the local policy and get the answer
303            if ( m_localPolicy.implies( pd, permission ) ) {
304                return true;
305            }
306        }
307        return false;
308    }
309
310    /** {@inheritDoc} */
311    @Override
312    public boolean checkStaticPermission( final Session session, final Permission permission ) {
313        return ( Boolean )Session.doPrivileged( session, ( PrivilegedAction< Boolean > )() -> {
314            try {
315                // Check the JVM-wide security policy first
316                AccessController.checkPermission( permission );
317                return Boolean.TRUE;
318            } catch( final AccessControlException e ) {
319                // Global policy denied the permission
320            }
321
322            // Try the local policy - check each Role/Group and User Principal
323            if ( allowedByLocalPolicy( session.getRoles(), permission ) || allowedByLocalPolicy( session.getPrincipals(), permission ) ) {
324                return Boolean.TRUE;
325            }
326            return Boolean.FALSE;
327        } );
328    }
329
330    /** {@inheritDoc} */
331    @Override
332    public Principal resolvePrincipal( final String name ) {
333        // Check built-in Roles first
334        final Role role = new Role(name);
335        if ( Role.isBuiltInRole( role ) ) {
336            return role;
337        }
338
339        // Check Authorizer Roles
340        Principal principal = m_authorizer.findRole( name );
341        if ( principal != null ) {
342            return principal;
343        }
344
345        // Check Groups
346        principal = m_engine.getManager( GroupManager.class ).findRole( name );
347        if ( principal != null ) {
348            return principal;
349        }
350
351        // Ok, no luck---this must be a user principal
352        final Principal[] principals;
353        final UserProfile profile;
354        final UserDatabase db = m_engine.getManager( UserManager.class ).getUserDatabase();
355        try {
356            profile = db.find( name );
357            principals = db.getPrincipals( profile.getLoginName() );
358            for( final Principal value : principals ) {
359                principal = value;
360                if( principal.getName().equals( name ) ) {
361                    return principal;
362                }
363            }
364        } catch( final NoSuchPrincipalException e ) {
365            // We couldn't find the user...
366        }
367        // Ok, no luck---mark this as unresolved and move on
368        return new UnresolvedPrincipal( name );
369    }
370
371
372    // events processing .......................................................
373
374    /** {@inheritDoc} */
375    @Override
376    public synchronized void addWikiEventListener( final WikiEventListener listener ) {
377        WikiEventManager.addWikiEventListener( this, listener );
378    }
379
380    /** {@inheritDoc} */
381    @Override
382    public synchronized void removeWikiEventListener( final WikiEventListener listener ) {
383        WikiEventManager.removeWikiEventListener( this, listener );
384    }
385
386}