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}