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.DelegateHeaderPrincipalProvider.DelegatedHeaderPrincipal;
021import static org.fcrepo.auth.common.HttpHeaderPrincipalProvider.HttpHeaderPrincipal;
022import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_ADMIN_ROLE;
023import static org.fcrepo.auth.common.ServletContainerAuthFilter.FEDORA_USER_ROLE;
024import static org.fcrepo.auth.webac.URIConstants.FOAF_AGENT_VALUE;
025import static org.fcrepo.auth.webac.URIConstants.WEBAC_AUTHENTICATED_AGENT_VALUE;
026import static org.fcrepo.auth.webac.WebACFilter.getBaseUri;
027import static org.fcrepo.auth.webac.WebACFilter.identifierConverter;
028import static org.fcrepo.http.commons.session.TransactionConstants.ATOMIC_ID_HEADER;
029import static org.slf4j.LoggerFactory.getLogger;
030
031import java.net.URI;
032import java.security.Principal;
033import java.util.Collection;
034import java.util.HashMap;
035import java.util.HashSet;
036import java.util.Map;
037import java.util.Set;
038
039import javax.inject.Inject;
040import javax.servlet.http.HttpServletRequest;
041
042import org.fcrepo.auth.common.ContainerRolesPrincipalProvider.ContainerRolesPrincipal;
043import org.fcrepo.config.FedoraPropsConfig;
044import org.fcrepo.http.commons.session.TransactionProvider;
045import org.fcrepo.kernel.api.ContainmentIndex;
046import org.fcrepo.kernel.api.ReadOnlyTransaction;
047import org.fcrepo.kernel.api.Transaction;
048import org.fcrepo.kernel.api.TransactionManager;
049import org.fcrepo.kernel.api.exception.PathNotFoundException;
050import org.fcrepo.kernel.api.exception.RepositoryConfigurationException;
051import org.fcrepo.kernel.api.identifiers.FedoraId;
052import org.fcrepo.kernel.api.models.FedoraResource;
053import org.fcrepo.kernel.api.models.ResourceFactory;
054
055import org.apache.http.auth.BasicUserPrincipal;
056import org.apache.shiro.authc.AuthenticationException;
057import org.apache.shiro.authc.AuthenticationInfo;
058import org.apache.shiro.authc.AuthenticationToken;
059import org.apache.shiro.authz.AuthorizationInfo;
060import org.apache.shiro.authz.SimpleAuthorizationInfo;
061import org.apache.shiro.realm.AuthorizingRealm;
062import org.apache.shiro.subject.PrincipalCollection;
063import org.slf4j.Logger;
064import org.springframework.beans.factory.annotation.Autowired;
065import org.springframework.beans.factory.annotation.Qualifier;
066/**
067 * Authorization-only realm that performs authorization checks using WebAC ACLs stored in a Fedora repository. It
068 * locates the ACL for the currently requested resource and parses the ACL RDF into a set of {@link WebACPermission}
069 * instances.
070 *
071 * @author peichman
072 */
073public class WebACAuthorizingRealm extends AuthorizingRealm {
074
075    private static final Logger log = getLogger(WebACAuthorizingRealm.class);
076
077    private static final ContainerRolesPrincipal adminPrincipal = new ContainerRolesPrincipal(FEDORA_ADMIN_ROLE);
078
079    private static final ContainerRolesPrincipal userPrincipal = new ContainerRolesPrincipal(FEDORA_USER_ROLE);
080
081    public static final String URIS_TO_AUTHORIZE = "URIS_TO_AUTHORIZE";
082
083    @Inject
084    private FedoraPropsConfig fedoraPropsConfig;
085
086    @Inject
087    private HttpServletRequest request;
088
089    @Inject
090    private WebACRolesProvider rolesProvider;
091
092    @Inject
093    private TransactionManager transactionManager;
094
095    @Inject
096    private ResourceFactory resourceFactory;
097
098    @Autowired
099    @Qualifier("containmentIndex")
100    private ContainmentIndex containmentIndex;
101
102    private Transaction transaction() {
103        final String txId = request.getHeader(ATOMIC_ID_HEADER);
104        if (txId == null) {
105            return ReadOnlyTransaction.INSTANCE;
106        }
107        final var txProvider = new TransactionProvider(transactionManager, request,
108                getBaseUri(request), fedoraPropsConfig.getJmsBaseUrl());
109        return txProvider.provide();
110    }
111
112    @Override
113    protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
114        final SimpleAuthorizationInfo authzInfo = new SimpleAuthorizationInfo();
115        boolean isAdmin = false;
116
117        final Collection<DelegatedHeaderPrincipal> delegatePrincipals =
118                principals.byType(DelegatedHeaderPrincipal.class);
119
120        // if the user was assigned the "fedoraAdmin" container role, they get the
121        // "fedoraAdmin" application role
122        if (principals.byType(ContainerRolesPrincipal.class).contains(adminPrincipal)) {
123            if (delegatePrincipals.size() > 1) {
124                throw new RepositoryConfigurationException("Too many delegates! " + delegatePrincipals);
125            } else if (delegatePrincipals.size() < 1) {
126                authzInfo.addRole(FEDORA_ADMIN_ROLE);
127                return authzInfo;
128            }
129            isAdmin = true;
130            // if Admin is delegating, they are a normal user
131            authzInfo.addRole(FEDORA_USER_ROLE);
132        } else if (principals.byType(ContainerRolesPrincipal.class).contains(userPrincipal)) {
133            authzInfo.addRole(FEDORA_USER_ROLE);
134        }
135
136        // for non-admins, we must check the ACL for the requested resource
137        @SuppressWarnings("unchecked")
138        Set<URI> targetURIs = (Set<URI>) request.getAttribute(URIS_TO_AUTHORIZE);
139        if (targetURIs == null) {
140            targetURIs = new HashSet<>();
141        }
142        final Map<URI, Map<String, Collection<String>>> rolesForURI =
143                new HashMap<URI, Map<String, Collection<String>>>();
144        final String contextPath = request.getContextPath() + request.getServletPath();
145        for (final URI uri : targetURIs) {
146            if (identifierConverter(request).inInternalDomain(uri.toString())) {
147                final FedoraId id = FedoraId.create(uri.toString());
148                log.debug("Getting roles for id {}", id.getFullId());
149                rolesForURI.put(uri, getRolesForId(id));
150            } else {
151                String path = uri.getPath();
152                if (path.startsWith(contextPath)) {
153                    path = path.replaceFirst(contextPath, "");
154                }
155                log.debug("Getting roles for path {}", path);
156                rolesForURI.put(uri, getRolesForPath(path));
157            }
158        }
159
160        for (final Object o : principals.asList()) {
161            log.debug("User has principal with name: {}", ((Principal) o).getName());
162        }
163        final Principal userPrincipal = principals.oneByType(BasicUserPrincipal.class);
164        final Collection<HttpHeaderPrincipal> headerPrincipals = principals.byType(HttpHeaderPrincipal.class);
165        // Add permissions for user or delegated user principal
166        if (isAdmin && delegatePrincipals.size() == 1) {
167            final DelegatedHeaderPrincipal delegatedPrincipal = delegatePrincipals.iterator().next();
168            log.debug("Admin user is delegating to {}", delegatedPrincipal);
169            addPermissions(authzInfo, rolesForURI, delegatedPrincipal.getName());
170            addPermissions(authzInfo, rolesForURI, WEBAC_AUTHENTICATED_AGENT_VALUE);
171        } else if (userPrincipal != null) {
172            log.debug("Basic user principal username: {}", userPrincipal.getName());
173            addPermissions(authzInfo, rolesForURI, userPrincipal.getName());
174            addPermissions(authzInfo, rolesForURI, WEBAC_AUTHENTICATED_AGENT_VALUE);
175        } else {
176            log.debug("No basic user principal found");
177        }
178        // Add permissions for header principals
179        if (headerPrincipals.isEmpty()) {
180            log.debug("No header principals found!");
181        }
182        headerPrincipals.forEach((headerPrincipal) -> {
183            addPermissions(authzInfo, rolesForURI, headerPrincipal.getName());
184        });
185
186        // Added FOAF_AGENT permissions for both authenticated and unauthenticated users
187        addPermissions(authzInfo, rolesForURI, FOAF_AGENT_VALUE);
188
189        return authzInfo;
190
191    }
192
193    private Map<String, Collection<String>> getRolesForPath(final String path) {
194        final FedoraId id = identifierConverter(request).pathToInternalId(path);
195        return getRolesForId(id);
196    }
197
198    private Map<String, Collection<String>> getRolesForId(final FedoraId id) {
199        Map<String, Collection<String>> roles = null;
200        final FedoraResource fedoraResource = getResourceOrParentFromPath(id);
201        if (fedoraResource != null) {
202            // check ACL for the request URI and get a mapping of agent => modes
203            roles = rolesProvider.getRoles(fedoraResource, transaction());
204        }
205        return roles;
206    }
207
208    private void addPermissions(final SimpleAuthorizationInfo authzInfo,
209            final Map<URI, Map<String, Collection<String>>> rolesForURI, final String agentName) {
210        if (rolesForURI != null) {
211            for (final URI uri : rolesForURI.keySet()) {
212                log.debug("Adding permissions gathered for URI {}", uri);
213                final Map<String, Collection<String>> roles = rolesForURI.get(uri);
214                if (roles != null) {
215                    final Collection<String> modesForUser = roles.get(agentName);
216                    if (modesForUser != null) {
217                        // add WebACPermission instance for each mode in the Authorization
218                        for (final String mode : modesForUser) {
219                            final WebACPermission perm = new WebACPermission(URI.create(mode), uri);
220                            authzInfo.addObjectPermission(perm);
221                            log.debug("Added permission {}", perm);
222                        }
223                    }
224                }
225            }
226        }
227    }
228
229    /**
230     * This realm is authorization-only.
231     */
232    @Override
233    protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token)
234            throws AuthenticationException {
235        return null;
236    }
237
238    /**
239     * This realm is authorization-only.
240     */
241    @Override
242    public boolean supports(final AuthenticationToken token) {
243        return false;
244    }
245
246    private FedoraResource getResourceOrParentFromPath(final FedoraId fedoraId) {
247        try {
248            log.debug("Testing FedoraResource for {}", fedoraId.getFullIdPath());
249            return this.resourceFactory.getResource(transaction(), fedoraId);
250        } catch (final PathNotFoundException exc) {
251            log.debug("Resource {} not found getting container", fedoraId.getFullIdPath());
252            final FedoraId containerId =
253                    containmentIndex.getContainerIdByPath(transaction(), fedoraId, false);
254            log.debug("Attempting to get FedoraResource for {}", fedoraId.getFullIdPath());
255            try {
256                log.debug("Got FedoraResource for {}", containerId.getFullIdPath());
257                return this.resourceFactory.getResource(transaction(), containerId);
258            } catch (final PathNotFoundException exc2) {
259                log.debug("Path {} does not exist, but we should never end up here.", containerId.getFullIdPath());
260                return null;
261            }
262        }
263    }
264
265}