001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance 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, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.auth.webac;
019
020import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_ADMIN_ROLE;
021import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_USER_ROLE;
022import static org.fcrepo.auth.webac.URIConstants.FOAF_AGENT_VALUE;
023import static org.fcrepo.auth.webac.URIConstants.WEBAC_AUTHENTICATED_AGENT_VALUE;
024import static org.fcrepo.auth.common.HttpHeaderPrincipalProvider.HttpHeaderPrincipal;
025import static org.fcrepo.auth.common.DelegateHeaderPrincipalProvider.DelegatedHeaderPrincipal;
026import static org.slf4j.LoggerFactory.getLogger;
027
028import java.net.URI;
029import java.security.Principal;
030import java.util.Collection;
031import java.util.HashMap;
032import java.util.HashSet;
033import java.util.Map;
034import java.util.Set;
035
036import javax.inject.Inject;
037import javax.jcr.Node;
038import javax.jcr.PathNotFoundException;
039import javax.servlet.http.HttpServletRequest;
040import javax.ws.rs.core.Context;
041import javax.ws.rs.core.UriBuilder;
042import javax.ws.rs.core.UriInfo;
043
044import org.apache.http.auth.BasicUserPrincipal;
045import org.apache.jena.rdf.model.Resource;
046import org.apache.shiro.authc.AuthenticationException;
047import org.apache.shiro.authc.AuthenticationInfo;
048import org.apache.shiro.authc.AuthenticationToken;
049import org.apache.shiro.authz.AuthorizationInfo;
050import org.apache.shiro.authz.SimpleAuthorizationInfo;
051import org.apache.shiro.realm.AuthorizingRealm;
052import org.apache.shiro.subject.PrincipalCollection;
053import org.fcrepo.auth.common.ContainerRolesPrincipalProvider.ContainerRolesPrincipal;
054import org.fcrepo.http.api.FedoraLdp;
055import org.fcrepo.http.commons.api.rdf.HttpResourceConverter;
056import org.fcrepo.http.commons.session.HttpSession;
057import org.fcrepo.http.commons.session.SessionFactory;
058import org.fcrepo.kernel.api.exception.RepositoryConfigurationException;
059import org.fcrepo.kernel.api.exception.RepositoryRuntimeException;
060import org.fcrepo.kernel.api.identifiers.IdentifierConverter;
061import org.fcrepo.kernel.api.models.FedoraResource;
062import org.fcrepo.kernel.modeshape.FedoraResourceImpl;
063import org.slf4j.Logger;
064/**
065 * Authorization-only realm that performs authorization checks using WebAC ACLs stored in a Fedora repository. It
066 * locates the ACL for the currently requested resource and parses the ACL RDF into a set of {@link WebACPermission}
067 * instances.
068 *
069 * @author peichman
070 */
071public class WebACAuthorizingRealm extends AuthorizingRealm {
072
073    private static final Logger log = getLogger(WebACAuthorizingRealm.class);
074
075    private static final ContainerRolesPrincipal adminPrincipal = new ContainerRolesPrincipal(FEDORA_ADMIN_ROLE);
076
077    private static final ContainerRolesPrincipal userPrincipal = new ContainerRolesPrincipal(FEDORA_USER_ROLE);
078
079    public static final String URIS_TO_AUTHORIZE = "URIS_TO_AUTHORIZE";
080
081    @Inject
082    private SessionFactory sessionFactory;
083
084    @Inject
085    private HttpServletRequest request;
086
087    @Inject
088    private WebACRolesProvider rolesProvider;
089
090    private HttpSession session;
091
092    private HttpSession session() {
093        if (session == null) {
094            session = sessionFactory.getSession(request);
095        }
096        return session;
097    }
098
099    private IdentifierConverter<Resource, FedoraResource> idTranslator;
100
101    private IdentifierConverter<Resource, FedoraResource> translator() {
102        if (idTranslator == null) {
103            idTranslator = new HttpResourceConverter(session(), UriBuilder.fromResource(FedoraLdp.class));
104        }
105
106        return idTranslator;
107    }
108
109    /**
110     * Useful for constructing URLs
111     */
112    @Context
113    private UriInfo uriInfo;
114
115
116    @Override
117    protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
118        final SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo();
119        boolean isAdmin = false;
120
121        final Collection<DelegatedHeaderPrincipal> delegatePrincipals =
122                principals.byType(DelegatedHeaderPrincipal.class);
123
124        // if the user was assigned the "fedoraAdmin" container role, they get the
125        // "fedoraAdmin" application role
126        if (principals.byType(ContainerRolesPrincipal.class).contains(adminPrincipal)) {
127            if (delegatePrincipals.size() > 1) {
128                throw new RepositoryConfigurationException("Too many delegates! " + delegatePrincipals);
129            } else if (delegatePrincipals.size() < 1) {
130                authzInfo.addRole(FEDORA_ADMIN_ROLE);
131                return authzInfo;
132            }
133            isAdmin = true;
134            // if Admin is delegating, they are a normal user
135            authzInfo.addRole(FEDORA_USER_ROLE);
136        } else if (principals.byType(ContainerRolesPrincipal.class).contains(userPrincipal)) {
137            authzInfo.addRole(FEDORA_USER_ROLE);
138        }
139
140        // for non-admins, we must check the ACL for the requested resource
141        @SuppressWarnings("unchecked")
142        Set<URI> targetURIs = (Set<URI>) request.getAttribute(URIS_TO_AUTHORIZE);
143        if (targetURIs == null) {
144            targetURIs = new HashSet<>();
145        }
146        final Map<URI, Map<String, Collection<String>>> rolesForURI =
147                new HashMap<URI, Map<String, Collection<String>>>();
148        final String contextPath = request.getContextPath() + request.getServletPath();
149        for (final URI uri : targetURIs) {
150            String path = uri.getPath();
151            if (path.startsWith(contextPath)) {
152                path = path.replaceFirst(contextPath, "");
153            }
154            log.debug("Getting roles for path {}", path);
155            rolesForURI.put(uri, getRolesForPath(path));
156        }
157
158        for (final Object o : principals.asList()) {
159            log.debug("User has principal with name: {}", ((Principal) o).getName());
160        }
161        final Principal userPrincipal = principals.oneByType(BasicUserPrincipal.class);
162        final Collection<HttpHeaderPrincipal> headerPrincipals = principals.byType(HttpHeaderPrincipal.class);
163        // Add permissions for user or delegated user principal
164        if (isAdmin && delegatePrincipals.size() == 1) {
165            final DelegatedHeaderPrincipal delegatedPrincipal = delegatePrincipals.iterator().next();
166            log.debug("Admin user is delegating to {}", delegatedPrincipal);
167            addPermissions(authzInfo, rolesForURI, delegatedPrincipal.getName());
168            addPermissions(authzInfo, rolesForURI, WEBAC_AUTHENTICATED_AGENT_VALUE);
169        } else if (userPrincipal != null) {
170            log.debug("Basic user principal username: {}", userPrincipal.getName());
171            addPermissions(authzInfo, rolesForURI, userPrincipal.getName());
172            addPermissions(authzInfo, rolesForURI, WEBAC_AUTHENTICATED_AGENT_VALUE);
173        } else {
174            log.debug("No basic user principal found");
175        }
176        // Add permissions for header principals
177        if (headerPrincipals.isEmpty()) {
178            log.debug("No header principals found!");
179        }
180        headerPrincipals.forEach((headerPrincipal) -> {
181            addPermissions(authzInfo, rolesForURI, headerPrincipal.getName());
182        });
183
184        // Added FOAF_AGENT permissions for both authenticated and unauthenticated users
185        addPermissions(authzInfo, rolesForURI, FOAF_AGENT_VALUE);
186
187        return authzInfo;
188
189    }
190
191    private Map<String, Collection<String>> getRolesForPath(final String path) {
192
193        Map<String, Collection<String>> roles = null;
194        final FedoraResource fedoraResource = getResourceOrParentFromPath(path);
195
196        if (fedoraResource != null) {
197            final Node node = ((FedoraResourceImpl) fedoraResource).getNode();
198
199            // check ACL for the request URI and get a mapping of agent => modes
200            roles = rolesProvider.getRoles(node);
201        }
202        return roles;
203    }
204
205    private void addPermissions(final SimpleAuthorizationInfo authzInfo,
206            final Map<URI, Map<String, Collection<String>>> rolesForURI, final String agentName) {
207        if (rolesForURI != null) {
208            for (final URI uri : rolesForURI.keySet()) {
209                log.debug("Adding permissions gathered for URI {}", uri);
210                final Map<String, Collection<String>> roles = rolesForURI.get(uri);
211                final Collection<String> modesForUser = roles.get(agentName);
212                if (modesForUser != null) {
213                    // add WebACPermission instance for each mode in the Authorization
214                    for (final String mode : modesForUser) {
215                        final WebACPermission perm = new WebACPermission(URI.create(mode), uri);
216                        authzInfo.addObjectPermission(perm);
217                        log.debug("Added permission {}", perm);
218                    }
219                }
220            }
221        }
222    }
223
224    /**
225     * This realm is authorization-only.
226     */
227    @Override
228    protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token)
229            throws AuthenticationException {
230        return null;
231    }
232
233    /**
234     * This realm is authorization-only.
235     */
236    @Override
237    public boolean supports(final AuthenticationToken token) {
238        return false;
239    }
240
241    private FedoraResource getResourceOrParentFromPath(final String path) {
242        FedoraResource resource = null;
243        log.debug("Attempting to get FedoraResource for {}", path);
244        try {
245            resource = translator().convert(translator().toDomain(path));
246            log.debug("Got FedoraResource for {}", path);
247        } catch (final RepositoryRuntimeException e) {
248            if (e.getCause() instanceof PathNotFoundException) {
249                log.debug("Path {} does not exist", path);
250                // go up the path looking for a node that exists
251                if (path.length() > 1) {
252                    final int lastSlash = path.lastIndexOf("/");
253                    final int end = lastSlash > 0 ? lastSlash : lastSlash + 1;
254                    resource = getResourceOrParentFromPath(path.substring(0, end));
255                }
256            }
257        }
258        return resource;
259    }
260
261}