1 package org.apache.maven.archiva.webdav;
4 * Licensed to the Apache Software Foundation (ASF) under one
5 * or more contributor license agreements. See the NOTICE file
6 * distributed with this work for additional information
7 * regarding copyright ownership. The ASF licenses this file
8 * to you under the Apache License, Version 2.0 (the
9 * "License"); you may not use this file except in compliance
10 * with the License. You may obtain a copy of the License at
12 * http://www.apache.org/licenses/LICENSE-2.0
14 * Unless required by applicable law or agreed to in writing,
15 * software distributed under the License is distributed on an
16 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17 * KIND, either express or implied. See the License for the
18 * specific language governing permissions and limitations
22 import com.opensymphony.xwork.ActionContext;
23 import org.apache.jackrabbit.webdav.*;
24 import org.apache.maven.archiva.repository.ManagedRepositoryContent;
25 import org.apache.maven.archiva.repository.RepositoryNotFoundException;
26 import org.apache.maven.archiva.repository.RepositoryException;
27 import org.apache.maven.archiva.repository.RepositoryContentFactory;
28 import org.apache.maven.archiva.repository.layout.LayoutException;
29 import org.apache.maven.archiva.repository.content.RepositoryRequest;
30 import org.apache.maven.archiva.repository.audit.AuditListener;
31 import org.apache.maven.archiva.repository.audit.Auditable;
32 import org.apache.maven.archiva.repository.audit.AuditEvent;
33 import org.apache.maven.archiva.repository.metadata.MetadataTools;
34 import org.apache.maven.archiva.repository.metadata.RepositoryMetadataException;
35 import org.apache.maven.archiva.webdav.util.WebdavMethodUtil;
36 import org.apache.maven.archiva.webdav.util.MimeTypes;
37 import org.apache.maven.archiva.webdav.util.RepositoryPathUtil;
38 import org.apache.maven.archiva.proxy.RepositoryProxyConnectors;
39 import org.apache.maven.archiva.common.utils.PathUtil;
40 import org.apache.maven.archiva.configuration.ArchivaConfiguration;
41 import org.apache.maven.archiva.configuration.RepositoryGroupConfiguration;
42 import org.apache.maven.archiva.model.ArtifactReference;
43 import org.apache.maven.archiva.model.ProjectReference;
44 import org.apache.maven.archiva.model.VersionedReference;
45 import org.apache.maven.archiva.policies.ProxyDownloadException;
46 import org.apache.maven.archiva.security.ArchivaXworkUser;
47 import org.apache.maven.archiva.security.ServletAuthenticator;
48 import org.apache.maven.model.DistributionManagement;
49 import org.apache.maven.model.Model;
50 import org.apache.maven.model.Relocation;
51 import org.apache.maven.model.io.xpp3.MavenXpp3Reader;
52 import org.codehaus.plexus.redback.authentication.AuthenticationException;
53 import org.codehaus.plexus.redback.authentication.AuthenticationResult;
54 import org.codehaus.plexus.redback.authorization.AuthorizationException;
55 import org.codehaus.plexus.redback.authorization.UnauthorizedException;
56 import org.codehaus.plexus.redback.policy.AccountLockedException;
57 import org.codehaus.plexus.redback.policy.MustChangePasswordException;
58 import org.codehaus.plexus.redback.system.SecuritySession;
59 import org.codehaus.plexus.redback.system.SecuritySystemConstants;
60 import org.codehaus.plexus.redback.xwork.filter.authentication.HttpAuthenticator;
61 import org.codehaus.plexus.util.xml.pull.XmlPullParserException;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
65 import javax.servlet.http.HttpServletResponse;
66 import java.util.ArrayList;
67 import java.util.HashMap;
68 import java.util.List;
71 import org.apache.commons.lang.StringUtils;
72 import org.apache.jackrabbit.webdav.lock.LockManager;
73 import org.apache.jackrabbit.webdav.lock.SimpleLockManager;
76 * @author <a href="mailto:james@atlassian.com">James William Dumay</a>
77 * @plexus.component role="org.apache.maven.archiva.webdav.ArchivaDavResourceFactory"
79 public class ArchivaDavResourceFactory
80 implements DavResourceFactory, Auditable
82 private static final String HTTP_PUT_METHOD = "PUT";
84 private Logger log = LoggerFactory.getLogger( ArchivaDavResourceFactory.class );
87 * @plexus.requirement role="org.apache.maven.archiva.repository.audit.AuditListener"
89 private List<AuditListener> auditListeners = new ArrayList<AuditListener>();
94 private RepositoryContentFactory repositoryFactory;
99 private RepositoryRequest repositoryRequest;
102 * @plexus.requirement role-hint="default"
104 private RepositoryProxyConnectors connectors;
107 * @plexus.requirement
109 private MetadataTools metadataTools;
112 * @plexus.requirement
114 private MimeTypes mimeTypes;
117 * @plexus.requirement
119 private ArchivaConfiguration archivaConfiguration;
122 * @plexus.requirement
124 private ServletAuthenticator servletAuth;
127 * @plexus.requirement role-hint="basic"
129 private HttpAuthenticator httpAuth;
133 * Lock Manager - use simple implementation from JackRabbit
135 private final LockManager lockManager = new SimpleLockManager();
137 public DavResource createResource( final DavResourceLocator locator, final DavServletRequest request,
138 final DavServletResponse response )
141 checkLocatorIsInstanceOfRepositoryLocator( locator );
142 ArchivaDavResourceLocator archivaLocator = (ArchivaDavResourceLocator) locator;
144 RepositoryGroupConfiguration repoGroupConfig =
145 archivaConfiguration.getConfiguration().getRepositoryGroupsAsMap().get(
146 ( (RepositoryLocator) locator ).getRepositoryId() );
147 List<String> repositories = new ArrayList<String>();
149 boolean isGet = WebdavMethodUtil.isReadMethod( request.getMethod() );
150 boolean isPut = WebdavMethodUtil.isWriteMethod( request.getMethod() );
152 if ( repoGroupConfig != null )
154 if( WebdavMethodUtil.isWriteMethod( request.getMethod() ) )
156 throw new DavException( HttpServletResponse.SC_METHOD_NOT_ALLOWED,
157 "Write method not allowed for repository groups." );
159 repositories.addAll( repoGroupConfig.getRepositories() );
161 // handle browse requests for virtual repos
162 if ( RepositoryPathUtil.getLogicalResource( locator.getResourcePath() ).endsWith( "/" ) )
164 return getResource( request, repositories, archivaLocator );
169 repositories.add( ( (RepositoryLocator) locator ).getRepositoryId() );
172 //MRM-419 - Windows Webdav support. Should not 404 if there is no content.
173 if (StringUtils.isEmpty(archivaLocator.getRepositoryId()))
175 throw new DavException(HttpServletResponse.SC_NO_CONTENT);
178 DavResource resource = null;
179 DavException e = null;
181 for ( String repositoryId : repositories )
183 ManagedRepositoryContent managedRepository = null;
187 managedRepository = getManagedRepository( repositoryId );
189 catch ( DavException de )
191 throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Invalid managed repository <" +
192 repositoryId + ">" );
195 if ( !locator.getResourcePath().startsWith( ArchivaDavResource.HIDDEN_PATH_PREFIX ) )
197 if ( managedRepository != null )
201 if( isAuthorized( request, repositoryId ) )
203 LogicalResource logicalResource =
204 new LogicalResource( RepositoryPathUtil.getLogicalResource( locator.getResourcePath() ) );
208 resource = doGet( managedRepository, request, archivaLocator, logicalResource );
213 resource = doPut( managedRepository, request, archivaLocator, logicalResource );
217 catch ( DavException de )
223 if( resource == null )
225 e = new DavException( HttpServletResponse.SC_NOT_FOUND, "Resource does not exist" );
229 setHeaders( locator, response );
231 // compatibility with MRM-440 to ensure browsing the repository works ok
232 if ( resource.isCollection() && !request.getRequestURI().endsWith("/" ) )
234 throw new BrowserRedirectException( resource.getHref() );
236 resource.addLockManager(lockManager);
242 e = new DavException( HttpServletResponse.SC_NOT_FOUND, "Repository does not exist" );
250 public DavResource createResource( final DavResourceLocator locator, final DavSession davSession )
253 checkLocatorIsInstanceOfRepositoryLocator( locator );
254 ArchivaDavResourceLocator archivaLocator = (ArchivaDavResourceLocator) locator;
256 DavResource resource = null;
257 if ( !locator.getResourcePath().startsWith( ArchivaDavResource.HIDDEN_PATH_PREFIX ) )
259 ManagedRepositoryContent managedRepository = getManagedRepository( archivaLocator.getRepositoryId() );
260 String logicalResource = RepositoryPathUtil.getLogicalResource( locator.getResourcePath() );
261 File resourceFile = new File( managedRepository.getRepoRoot(), logicalResource );
263 new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource, mimeTypes, davSession, archivaLocator,
266 resource.addLockManager(lockManager);
270 private DavResource doGet( ManagedRepositoryContent managedRepository, DavServletRequest request,
271 ArchivaDavResourceLocator locator, LogicalResource logicalResource )
274 File resourceFile = new File( managedRepository.getRepoRoot(), logicalResource.getPath() );
275 ArchivaDavResource resource =
276 new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource.getPath(), mimeTypes, request.getDavSession(), locator, this );
278 if ( !resource.isCollection() )
280 // At this point the incoming request can either be in default or
281 // legacy layout format.
282 boolean fromProxy = fetchContentFromProxies( managedRepository, request, logicalResource );
284 boolean previouslyExisted = resourceFile.exists();
288 // Perform an adjustment of the resource to the managed
289 // repository expected path.
290 String localResourcePath =
291 repositoryRequest.toNativePath( logicalResource.getPath(), managedRepository );
292 resourceFile = new File( managedRepository.getRepoRoot(), localResourcePath );
294 catch ( LayoutException e )
296 if ( previouslyExisted )
300 throw new DavException( HttpServletResponse.SC_NOT_FOUND, e );
303 // Attempt to fetch the resource from any defined proxy.
306 processAuditEvents( request, locator.getWorkspaceName(), logicalResource.getPath(), previouslyExisted,
307 resourceFile, " (proxied)" );
310 new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource.getPath(), mimeTypes, request.getDavSession(), locator,
313 if ( !resourceFile.exists() )
321 private DavResource doPut( ManagedRepositoryContent managedRepository, DavServletRequest request,
322 ArchivaDavResourceLocator locator, LogicalResource logicalResource )
326 * Create parent directories that don't exist when writing a file This actually makes this implementation not
327 * compliant to the WebDAV RFC - but we have enough knowledge about how the collection is being used to do this
328 * reasonably and some versions of Maven's WebDAV don't correctly create the collections themselves.
331 File rootDirectory = new File( managedRepository.getRepoRoot() );
332 File destDir = new File( rootDirectory, logicalResource.getPath() ).getParentFile();
333 if ( request.getMethod().equals(HTTP_PUT_METHOD) && !destDir.exists() )
336 String relPath = PathUtil.getRelative( rootDirectory.getAbsolutePath(), destDir );
337 triggerAuditEvent( request, logicalResource.getPath(), relPath, AuditEvent.CREATE_DIR );
340 File resourceFile = new File( managedRepository.getRepoRoot(), logicalResource.getPath() );
342 boolean previouslyExisted = resourceFile.exists();
344 processAuditEvents( request, locator.getRepositoryId(), logicalResource.getPath(), previouslyExisted,
345 resourceFile, null );
347 return new ArchivaDavResource( resourceFile.getAbsolutePath(), logicalResource.getPath(), mimeTypes, request.getDavSession(), locator,
351 private boolean fetchContentFromProxies( ManagedRepositoryContent managedRepository, DavServletRequest request,
352 LogicalResource resource )
355 if ( repositoryRequest.isSupportFile( resource.getPath() ) )
357 // Checksums are fetched with artifact / metadata.
359 // Need to adjust the path for the checksum resource.
363 // Is it a Metadata resource?
364 if ( repositoryRequest.isDefault( resource.getPath() ) && repositoryRequest.isMetadata( resource.getPath() ) )
366 return fetchMetadataFromProxies( managedRepository, request, resource );
369 // Not any of the above? Then it's gotta be an artifact reference.
372 // Get the artifact reference in a layout neutral way.
373 ArtifactReference artifact = repositoryRequest.toArtifactReference( resource.getPath() );
375 if ( artifact != null )
377 applyServerSideRelocation( managedRepository, artifact );
379 File proxiedFile = connectors.fetchFromProxies( managedRepository, artifact );
381 resource.setPath( managedRepository.toPath( artifact ) );
383 return ( proxiedFile != null );
386 catch ( LayoutException e )
390 catch ( ProxyDownloadException e )
392 log.error( e.getMessage(), e );
393 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR, "Unable to fetch artifact resource." );
398 private boolean fetchMetadataFromProxies( ManagedRepositoryContent managedRepository, DavServletRequest request,
399 LogicalResource resource )
402 ProjectReference project;
403 VersionedReference versioned;
408 versioned = metadataTools.toVersionedReference( resource.getPath() );
409 if ( versioned != null )
411 connectors.fetchFromProxies( managedRepository, versioned );
415 catch ( RepositoryMetadataException e )
422 project = metadataTools.toProjectReference( resource.getPath() );
423 if ( project != null )
425 connectors.fetchFromProxies( managedRepository, project );
429 catch ( RepositoryMetadataException e )
438 * A relocation capable client will request the POM prior to the artifact, and will then read meta-data and do
439 * client side relocation. A simplier client (like maven 1) will only request the artifact and not use the
442 * For such clients, archiva does server-side relocation by reading itself the <relocation> element in
443 * metadatas and serving the expected artifact.
445 protected void applyServerSideRelocation( ManagedRepositoryContent managedRepository, ArtifactReference artifact )
446 throws ProxyDownloadException
448 if ( "pom".equals( artifact.getType() ) )
453 // Build the artifact POM reference
454 ArtifactReference pomReference = new ArtifactReference();
455 pomReference.setGroupId( artifact.getGroupId() );
456 pomReference.setArtifactId( artifact.getArtifactId() );
457 pomReference.setVersion( artifact.getVersion() );
458 pomReference.setType( "pom" );
460 // Get the artifact POM from proxied repositories if needed
461 connectors.fetchFromProxies( managedRepository, pomReference );
463 // Open and read the POM from the managed repo
464 File pom = managedRepository.toFile( pomReference );
473 Model model = new MavenXpp3Reader().read( new FileReader( pom ) );
474 DistributionManagement dist = model.getDistributionManagement();
477 Relocation relocation = dist.getRelocation();
478 if ( relocation != null )
480 // artifact is relocated : update the repositoryPath
481 if ( relocation.getGroupId() != null )
483 artifact.setGroupId( relocation.getGroupId() );
485 if ( relocation.getArtifactId() != null )
487 artifact.setArtifactId( relocation.getArtifactId() );
489 if ( relocation.getVersion() != null )
491 artifact.setVersion( relocation.getVersion() );
496 catch ( FileNotFoundException e )
498 // Artifact has no POM in repo : ignore
500 catch ( IOException e )
502 // Unable to read POM : ignore.
504 catch ( XmlPullParserException e )
506 // Invalid POM : ignore
510 private void processAuditEvents( DavServletRequest request, String repositoryId, String resource,
511 boolean previouslyExisted, File resourceFile, String suffix )
513 if ( suffix == null )
518 // Process Create Audit Events.
519 if ( !previouslyExisted && resourceFile.exists() )
521 if ( resourceFile.isFile() )
523 triggerAuditEvent( request, repositoryId, resource, AuditEvent.CREATE_FILE + suffix );
525 else if ( resourceFile.isDirectory() )
527 triggerAuditEvent( request, repositoryId, resource, AuditEvent.CREATE_DIR + suffix );
530 // Process Remove Audit Events.
531 else if ( previouslyExisted && !resourceFile.exists() )
533 if ( resourceFile.isFile() )
535 triggerAuditEvent( request, repositoryId, resource, AuditEvent.REMOVE_FILE + suffix );
537 else if ( resourceFile.isDirectory() )
539 triggerAuditEvent( request, repositoryId, resource, AuditEvent.REMOVE_DIR + suffix );
542 // Process modify events.
545 if ( resourceFile.isFile() )
547 triggerAuditEvent( request, repositoryId, resource, AuditEvent.MODIFY_FILE + suffix );
552 private void triggerAuditEvent( String user, String remoteIP, String repositoryId, String resource, String action )
554 AuditEvent event = new AuditEvent( repositoryId, user, resource, action );
555 event.setRemoteIP( remoteIP );
557 for ( AuditListener listener : auditListeners )
559 listener.auditEvent( event );
563 private void triggerAuditEvent( DavServletRequest request, String repositoryId, String resource, String action )
565 triggerAuditEvent( ArchivaXworkUser.getActivePrincipal( ActionContext.getContext().getSession() ),
566 getRemoteIP( request ), repositoryId, resource, action );
569 private String getRemoteIP( DavServletRequest request )
571 return request.getRemoteAddr();
574 public void addAuditListener( AuditListener listener )
576 this.auditListeners.add( listener );
579 public void clearAuditListeners()
581 this.auditListeners.clear();
584 public void removeAuditListener( AuditListener listener )
586 this.auditListeners.remove( listener );
589 private void setHeaders( DavResourceLocator locator, DavServletResponse response )
591 // [MRM-503] - Metadata file need Pragma:no-cache response
593 if ( locator.getResourcePath().endsWith( "/maven-metadata.xml" ) )
595 response.addHeader( "Pragma", "no-cache" );
596 response.addHeader( "Cache-Control", "no-cache" );
599 // TODO: [MRM-524] determine http caching options for other types of files (artifacts, sha1, md5, snapshots)
602 private ManagedRepositoryContent getManagedRepository( String respositoryId )
605 if ( respositoryId != null )
609 return repositoryFactory.getManagedRepositoryContent( respositoryId );
611 catch ( RepositoryNotFoundException e )
613 throw new DavException( HttpServletResponse.SC_NOT_FOUND, e );
615 catch ( RepositoryException e )
617 throw new DavException( HttpServletResponse.SC_NOT_FOUND, e );
623 private void checkLocatorIsInstanceOfRepositoryLocator( DavResourceLocator locator )
626 if ( !( locator instanceof RepositoryLocator ) )
628 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
629 "Locator does not implement RepositoryLocator" );
633 class LogicalResource
637 public LogicalResource( String path )
642 public String getPath()
647 public void setPath( String path )
653 protected boolean isAuthorized( DavServletRequest request, String repositoryId )
658 AuthenticationResult result = httpAuth.getAuthenticationResult( request, null );
659 SecuritySession securitySession = httpAuth.getSecuritySession();
661 return servletAuth.isAuthenticated( request, result ) &&
662 servletAuth.isAuthorized( request, securitySession, repositoryId,
663 WebdavMethodUtil.isWriteMethod( request.getMethod() ) );
665 catch ( AuthenticationException e )
667 throw new UnauthorizedDavException( repositoryId, "You are not authenticated" );
669 catch ( MustChangePasswordException e )
671 throw new UnauthorizedDavException( repositoryId, "You must change your password." );
673 catch ( AccountLockedException e )
675 throw new UnauthorizedDavException( repositoryId, "User account is locked." );
677 catch ( AuthorizationException e )
679 throw new DavException( HttpServletResponse.SC_INTERNAL_SERVER_ERROR,
680 "Fatal Authorization Subsystem Error." );
682 catch ( UnauthorizedException e )
684 throw new UnauthorizedDavException( repositoryId, e.getMessage() );
688 private DavResource getResource( DavServletRequest request, List<String> repositories, ArchivaDavResourceLocator locator )
691 List<File> mergedRepositoryContents = new ArrayList<File>();
692 LogicalResource logicalResource =
693 new LogicalResource( RepositoryPathUtil.getLogicalResource( locator.getResourcePath() ) );
696 // if the current user logged in has permission to any of the repositories, allow user to
697 // browse the repo group but displaying only the repositories which the user has permission to access.
698 // otherwise, prompt for authentication.
700 // put the current session in the session map which will be passed to ArchivaXworkUser
701 Map<String, Object> sessionMap = new HashMap<String, Object>();
702 if( request.getSession().getAttribute( SecuritySystemConstants.SECURITY_SESSION_KEY ) != null )
704 sessionMap.put( SecuritySystemConstants.SECURITY_SESSION_KEY,
705 request.getSession().getAttribute( SecuritySystemConstants.SECURITY_SESSION_KEY ) );
708 String activePrincipal = ArchivaXworkUser.getActivePrincipal( sessionMap );
709 boolean allow = isAllowedToContinue( request, repositories, activePrincipal );
713 for( String repository : repositories )
715 // for prompted authentication
716 if( httpAuth.getSecuritySession() != null )
720 if( isAuthorized( request, repository ) )
722 getResource( locator, mergedRepositoryContents, logicalResource, repository );
725 catch ( DavException e )
732 // for the current user logged in
735 if( servletAuth.isAuthorizedToAccessVirtualRepository( activePrincipal, repository ) )
737 getResource( locator, mergedRepositoryContents, logicalResource, repository );
740 catch ( UnauthorizedException e )
749 throw new UnauthorizedDavException( locator.getRepositoryId(), "User not authorized." );
752 ArchivaVirtualDavResource resource =
753 new ArchivaVirtualDavResource( mergedRepositoryContents, logicalResource.getPath(), mimeTypes, locator, this );
755 // compatibility with MRM-440 to ensure browsing the repository group works ok
756 if ( resource.isCollection() && !request.getRequestURI().endsWith("/" ) )
758 throw new BrowserRedirectException( resource.getHref() );
764 private void getResource( ArchivaDavResourceLocator locator, List<File> mergedRepositoryContents,
765 LogicalResource logicalResource, String repository )
768 ManagedRepositoryContent managedRepository = null;
772 managedRepository = getManagedRepository( repository );
774 catch ( DavException de )
776 throw new DavException( HttpServletResponse.SC_NOT_FOUND, "Invalid managed repository <" +
780 if ( !locator.getResourcePath().startsWith( ArchivaVirtualDavResource.HIDDEN_PATH_PREFIX ) )
782 if( managedRepository != null )
784 File resourceFile = new File( managedRepository.getRepoRoot(), logicalResource.getPath() );
785 if( resourceFile.exists() )
787 mergedRepositoryContents.add( resourceFile );
794 * Check if the current user is authorized to access any of the repos
797 * @param repositories
798 * @param activePrincipal
801 private boolean isAllowedToContinue( DavServletRequest request, List<String> repositories, String activePrincipal )
803 boolean allow = false;
806 // if securitySession != null, it means that the user was prompted for authentication
807 if( httpAuth.getSecuritySession() != null )
809 for( String repository : repositories )
813 if( isAuthorized( request, repository ) )
819 catch( DavException e )
827 for( String repository : repositories )
831 if( servletAuth.isAuthorizedToAccessVirtualRepository( activePrincipal, repository ) )
837 catch ( UnauthorizedException e )