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 org.apache.http.auth.BasicUserPrincipal;
021import org.apache.shiro.authc.AuthenticationException;
022import org.apache.shiro.authc.AuthenticationInfo;
023import org.apache.shiro.authc.AuthenticationToken;
024import org.apache.shiro.authz.AuthorizationInfo;
025import org.apache.shiro.authz.SimpleAuthorizationInfo;
026import org.apache.shiro.realm.AuthorizingRealm;
027import org.apache.shiro.subject.PrincipalCollection;
028import org.fcrepo.auth.common.ContainerRolesPrincipalProvider.ContainerRolesPrincipal;
029import org.fcrepo.config.FedoraPropsConfig;
030import org.fcrepo.http.commons.session.TransactionProvider;
031import org.fcrepo.kernel.api.ContainmentIndex;
032import org.fcrepo.kernel.api.Transaction;
033import org.fcrepo.kernel.api.TransactionManager;
034import org.fcrepo.kernel.api.TransactionUtils;
035import org.fcrepo.kernel.api.exception.PathNotFoundException;
036import org.fcrepo.kernel.api.exception.RepositoryConfigurationException;
037import org.fcrepo.kernel.api.identifiers.FedoraId;
038import org.fcrepo.kernel.api.models.FedoraResource;
039import org.fcrepo.kernel.api.models.ResourceFactory;
040import org.slf4j.Logger;
041import org.springframework.beans.factory.annotation.Autowired;
042import org.springframework.beans.factory.annotation.Qualifier;
043
044import javax.inject.Inject;
045import javax.servlet.http.HttpServletRequest;
046import java.net.URI;
047import java.security.Principal;
048import java.util.Collection;
049import java.util.HashMap;
050import java.util.HashSet;
051import java.util.Map;
052import java.util.Set;
053
054import static org.fcrepo.auth.common.DelegateHeaderPrincipalProvider.DelegatedHeaderPrincipal;
055import static org.fcrepo.auth.common.HttpHeaderPrincipalProvider.HttpHeaderPrincipal;
056import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_ADMIN_ROLE;
057import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_USER_ROLE;
058import static org.fcrepo.auth.webac.URIConstants.FOAF_AGENT_VALUE;
059import static org.fcrepo.auth.webac.URIConstants.WEBAC_AUTHENTICATED_AGENT_VALUE;
060import static org.fcrepo.auth.webac.WebACFilter.getBaseUri;
061import static org.fcrepo.auth.webac.WebACFilter.identifierConverter;
062import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_ID_HEADER;
063import static org.slf4j.LoggerFactory.getLogger;
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 FedoraPropsConfig fedoraPropsConfig;
083
084    @Inject
085    private HttpServletRequest request;
086
087    @Inject
088    private WebACRolesProvider rolesProvider;
089
090    @Inject
091    private TransactionManager transactionManager;
092
093    @Inject
094    private ResourceFactory resourceFactory;
095
096    @Autowired
097    @Qualifier("containmentIndex")
098    private ContainmentIndex containmentIndex;
099
100    private Transaction transaction() {
101        final String txId = request.getHeader(ATOMIC_ID_HEADER);
102        if (txId == null) {
103            return null;
104        }
105        final var txProvider = new TransactionProvider(transactionManager, request,
106                getBaseUri(request), fedoraPropsConfig.getJmsBaseUrl());
107        return txProvider.provide();
108    }
109
110    @Override
111    protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
112        final SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo();
113        boolean isAdmin = false;
114
115        final Collection<DelegatedHeaderPrincipal> delegatePrincipals =
116                principals.byType(DelegatedHeaderPrincipal.class);
117
118        // if the user was assigned the "fedoraAdmin" container role, they get the
119        // "fedoraAdmin" application role
120        if (principals.byType(ContainerRolesPrincipal.class).contains(adminPrincipal)) {
121            if (delegatePrincipals.size() > 1) {
122                throw new RepositoryConfigurationException("Too many delegates! " + delegatePrincipals);
123            } else if (delegatePrincipals.size() < 1) {
124                authzInfo.addRole(FEDORA_ADMIN_ROLE);
125                return authzInfo;
126            }
127            isAdmin = true;
128            // if Admin is delegating, they are a normal user
129            authzInfo.addRole(FEDORA_USER_ROLE);
130        } else if (principals.byType(ContainerRolesPrincipal.class).contains(userPrincipal)) {
131            authzInfo.addRole(FEDORA_USER_ROLE);
132        }
133
134        // for non-admins, we must check the ACL for the requested resource
135        @SuppressWarnings("unchecked")
136        Set<URI> targetURIs = (Set<URI>) request.getAttribute(URIS_TO_AUTHORIZE);
137        if (targetURIs == null) {
138            targetURIs = new HashSet<>();
139        }
140        final Map<URI, Map<String, Collection<String>>> rolesForURI =
141                new HashMap<URI, Map<String, Collection<String>>>();
142        final String contextPath = request.getContextPath() + request.getServletPath();
143        for (final URI uri : targetURIs) {
144            if (identifierConverter(request).inInternalDomain(uri.toString())) {
145                final FedoraId id = FedoraId.create(uri.toString());
146                log.debug("Getting roles for id {}", id.getFullId());
147                rolesForURI.put(uri, getRolesForId(id));
148            } else {
149                String path = uri.getPath();
150                if (path.startsWith(contextPath)) {
151                    path = path.replaceFirst(contextPath, "");
152                }
153                log.debug("Getting roles for path {}", path);
154                rolesForURI.put(uri, getRolesForPath(path));
155            }
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        final FedoraId id = identifierConverter(request).pathToInternalId(path);
193        return getRolesForId(id);
194    }
195
196    private Map<String, Collection<String>> getRolesForId(final FedoraId id) {
197        Map<String, Collection<String>> roles = null;
198        final FedoraResource fedoraResource = getResourceOrParentFromPath(id);
199        if (fedoraResource != null) {
200            // check ACL for the request URI and get a mapping of agent => modes
201            roles = rolesProvider.getRoles(fedoraResource, transaction());
202        }
203        return roles;
204    }
205
206    private void addPermissions(final SimpleAuthorizationInfo authzInfo,
207            final Map<URI, Map<String, Collection<String>>> rolesForURI, final String agentName) {
208        if (rolesForURI != null) {
209            for (final URI uri : rolesForURI.keySet()) {
210                log.debug("Adding permissions gathered for URI {}", uri);
211                final Map<String, Collection<String>> roles = rolesForURI.get(uri);
212                if (roles != null) {
213                    final Collection<String> modesForUser = roles.get(agentName);
214                    if (modesForUser != null) {
215                        // add WebACPermission instance for each mode in the Authorization
216                        for (final String mode : modesForUser) {
217                            final WebACPermission perm = new WebACPermission(URI.create(mode), uri);
218                            authzInfo.addObjectPermission(perm);
219                            log.debug("Added permission {}", perm);
220                        }
221                    }
222                }
223            }
224        }
225    }
226
227    /**
228     * This realm is authorization-only.
229     */
230    @Override
231    protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token)
232            throws AuthenticationException {
233        return null;
234    }
235
236    /**
237     * This realm is authorization-only.
238     */
239    @Override
240    public boolean supports(final AuthenticationToken token) {
241        return false;
242    }
243
244    private FedoraResource getResourceOrParentFromPath(final FedoraId fedoraId) {
245        try {
246            log.debug("Testing FedoraResource for {}", fedoraId.getFullIdPath());
247            return this.resourceFactory.getResource(transaction(), fedoraId);
248        } catch (final PathNotFoundException exc) {
249            log.debug("Resource {} not found getting container", fedoraId.getFullIdPath());
250            final FedoraId containerId =
251                    containmentIndex.getContainerIdByPath(TransactionUtils.openTxId(transaction()), fedoraId, false);
252            log.debug("Attempting to get FedoraResource for {}", fedoraId.getFullIdPath());
253            try {
254                log.debug("Got FedoraResource for {}", containerId.getFullIdPath());
255                return this.resourceFactory.getResource(transaction(), containerId);
256            } catch (final PathNotFoundException exc2) {
257                log.debug("Path {} does not exist, but we should never end up here.", containerId.getFullIdPath());
258                return null;
259            }
260        }
261    }
262
263}