]> source.dussan.org Git - archiva.git/blob
aa3277e84a578e81ff3d4b062836b2e5f550c677
[archiva.git] /
1 package org.apache.archiva.web.servlet;
2
3 /*
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
11  *
12  *  http://www.apache.org/licenses/LICENSE-2.0
13  *
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
19  * under the License.
20  */
21
22 import java.io.File;
23 import java.io.IOException;
24 import java.io.InputStream;
25
26 import javax.servlet.http.HttpServletResponse;
27
28 import net.sf.ehcache.CacheManager;
29
30 import org.apache.commons.io.FileUtils;
31 import org.apache.maven.archiva.configuration.ArchivaConfiguration;
32 import org.apache.maven.archiva.configuration.Configuration;
33 import org.apache.maven.archiva.configuration.ManagedRepositoryConfiguration;
34 import org.apache.maven.archiva.security.ArchivaXworkUser;
35 import org.apache.maven.archiva.security.ServletAuthenticator;
36 import org.codehaus.plexus.redback.authentication.AuthenticationException;
37 import org.codehaus.plexus.redback.authentication.AuthenticationResult;
38 import org.codehaus.plexus.redback.authorization.UnauthorizedException;
39 import org.codehaus.plexus.redback.system.DefaultSecuritySession;
40 import org.codehaus.plexus.redback.system.SecuritySession;
41 import org.codehaus.plexus.spring.PlexusInSpringTestCase;
42 import org.codehaus.redback.integration.filter.authentication.HttpAuthenticator;
43 import org.codehaus.redback.integration.filter.authentication.basic.HttpBasicAuthentication;
44 import org.easymock.MockControl;
45 import org.easymock.classextension.MockClassControl;
46
47 import com.meterware.httpunit.GetMethodWebRequest;
48 import com.meterware.httpunit.HttpUnitOptions;
49 import com.meterware.httpunit.PutMethodWebRequest;
50 import com.meterware.httpunit.WebRequest;
51 import com.meterware.httpunit.WebResponse;
52 import com.meterware.servletunit.InvocationContext;
53 import com.meterware.servletunit.ServletRunner;
54 import com.meterware.servletunit.ServletUnitClient;
55
56 /**
57  * RepositoryServletSecurityTest
58  * 
59  * Test the flow of the authentication and authorization checks. This does not necessarily
60  * perform redback security checking.
61  * 
62  * @version $Id$
63  */
64 public class RepositoryServletSecurityTest
65     extends PlexusInSpringTestCase
66 {
67     protected static final String REPOID_INTERNAL = "internal";
68
69     protected ServletUnitClient sc;
70
71     protected File repoRootInternal;
72
73     private ServletRunner sr;
74
75     protected ArchivaConfiguration archivaConfiguration;
76
77     private DavSessionProvider davSessionProvider;
78
79     private MockControl servletAuthControl;
80
81     private ServletAuthenticator servletAuth;
82
83     private MockClassControl httpAuthControl;
84
85     private HttpAuthenticator httpAuth;
86
87     private ArchivaXworkUser archivaXworkUser;
88
89     private RepositoryServlet servlet;
90     
91     public void setUp()
92         throws Exception
93     {
94         super.setUp();
95
96         String appserverBase = getTestFile( "target/appserver-base" ).getAbsolutePath();
97         System.setProperty( "appserver.base", appserverBase );
98
99         File testConf = getTestFile( "src/test/resources/repository-archiva.xml" );
100         File testConfDest = new File( appserverBase, "conf/archiva.xml" );
101         FileUtils.copyFile( testConf, testConfDest );
102
103         archivaConfiguration = (ArchivaConfiguration) lookup( ArchivaConfiguration.class );
104         repoRootInternal = new File( appserverBase, "data/repositories/internal" );
105         Configuration config = archivaConfiguration.getConfiguration();
106
107         config.addManagedRepository( createManagedRepository( REPOID_INTERNAL, "Internal Test Repo", repoRootInternal ) );
108         saveConfiguration( archivaConfiguration );
109
110         CacheManager.getInstance().removeCache( "url-failures-cache" );
111
112         HttpUnitOptions.setExceptionsThrownOnErrorStatus( false );
113
114         sr = new ServletRunner( getTestFile( "src/test/resources/WEB-INF/repository-servlet-security-test/web.xml" ) );
115         sr.registerServlet( "/repository/*", RepositoryServlet.class.getName() );
116         sc = sr.newClient();
117
118         servletAuthControl = MockControl.createControl( ServletAuthenticator.class );
119         servletAuthControl.setDefaultMatcher( MockControl.ALWAYS_MATCHER );
120         servletAuth = (ServletAuthenticator) servletAuthControl.getMock();
121
122         httpAuthControl =
123             MockClassControl.createControl( HttpBasicAuthentication.class, HttpBasicAuthentication.class.getMethods() );
124         httpAuthControl.setDefaultMatcher( MockControl.ALWAYS_MATCHER );
125         httpAuth = (HttpAuthenticator) httpAuthControl.getMock();
126
127         archivaXworkUser = new ArchivaXworkUser();
128         archivaXworkUser.setGuest( "guest" );
129
130         davSessionProvider = new ArchivaDavSessionProvider( servletAuth, httpAuth, archivaXworkUser );      
131     }
132
133     protected ManagedRepositoryConfiguration createManagedRepository( String id, String name, File location )
134     {
135         ManagedRepositoryConfiguration repo = new ManagedRepositoryConfiguration();
136         repo.setId( id );
137         repo.setName( name );
138         repo.setLocation( location.getAbsolutePath() );
139         return repo;
140     }
141
142     protected void saveConfiguration()
143         throws Exception
144     {
145         saveConfiguration( archivaConfiguration );
146     }
147
148     protected void saveConfiguration( ArchivaConfiguration archivaConfiguration )
149         throws Exception
150     {
151         archivaConfiguration.save( archivaConfiguration.getConfiguration() );
152     }
153
154     protected void setupCleanRepo( File repoRootDir )
155         throws IOException
156     {
157         FileUtils.deleteDirectory( repoRootDir );
158         if ( !repoRootDir.exists() )
159         {
160             repoRootDir.mkdirs();
161         }
162     }
163
164     @Override
165     protected String getPlexusConfigLocation()
166     {
167         return "org/apache/maven/archiva/webdav/RepositoryServletSecurityTest.xml";
168     }
169
170     @Override
171     protected void tearDown()
172         throws Exception
173     {
174         if ( sc != null )
175         {
176             sc.clearContents();
177         }
178
179         if ( sr != null )
180         {
181             sr.shutDown();
182         }
183
184         if ( repoRootInternal.exists() )
185         {
186             FileUtils.deleteDirectory(repoRootInternal);
187         }
188
189         servlet = null;
190         
191         super.tearDown();
192     }
193
194     // test deploy with invalid user, and guest has no write access to repo
195     // 401 must be returned
196     public void testPutWithInvalidUserAndGuestHasNoWriteAccess()
197         throws Exception
198     {
199         setupCleanRepo( repoRootInternal );
200
201         String putUrl = "http://machine.com/repository/internal/path/to/artifact.jar";
202         InputStream is = getClass().getResourceAsStream( "/artifact.jar" );
203         assertNotNull( "artifact.jar inputstream", is );
204
205         WebRequest request = new PutMethodWebRequest( putUrl, is, "application/octet-stream" );
206         InvocationContext ic = sc.newInvocation( request );
207         servlet = (RepositoryServlet) ic.getServlet();
208         servlet.setDavSessionProvider( davSessionProvider );
209
210         AuthenticationResult result = new AuthenticationResult();
211         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
212         servletAuthControl.expectAndThrow( servletAuth.isAuthenticated( null, null ),
213                            new AuthenticationException( "Authentication error" ) );
214         
215         servletAuth.isAuthorized( "guest", "internal", true );        
216         servletAuthControl.setMatcher( MockControl.EQUALS_MATCHER );
217         servletAuthControl.setThrowable( new UnauthorizedException( "'guest' has no write access to repository" ) );
218
219         httpAuthControl.replay();
220         servletAuthControl.replay();
221         
222         servlet.service( ic.getRequest(), ic.getResponse() );
223         
224         httpAuthControl.verify();
225         servletAuthControl.verify();
226
227         //assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getResponseCode());
228     }
229
230     // test deploy with invalid user, but guest has write access to repo
231     public void testPutWithInvalidUserAndGuestHasWriteAccess()
232         throws Exception
233     {
234         setupCleanRepo( repoRootInternal );
235
236         String putUrl = "http://machine.com/repository/internal/path/to/artifact.jar";
237         InputStream is = getClass().getResourceAsStream( "/artifact.jar" );
238         assertNotNull( "artifact.jar inputstream", is );
239
240         WebRequest request = new PutMethodWebRequest( putUrl, is, "application/octet-stream" );
241
242         InvocationContext ic = sc.newInvocation( request );
243         servlet = (RepositoryServlet) ic.getServlet();
244         servlet.setDavSessionProvider( davSessionProvider );
245
246         ArchivaDavResourceFactory archivaDavResourceFactory = (ArchivaDavResourceFactory) servlet.getResourceFactory();
247         archivaDavResourceFactory.setHttpAuth( httpAuth );
248         archivaDavResourceFactory.setServletAuth( servletAuth );
249
250         servlet.setResourceFactory( archivaDavResourceFactory );
251         
252         AuthenticationResult result = new AuthenticationResult();
253         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
254         servletAuthControl.expectAndThrow( servletAuth.isAuthenticated( null, null ),
255                                            new AuthenticationException( "Authentication error" ) );
256         
257         servletAuth.isAuthorized( "guest", "internal", true );
258         servletAuthControl.setMatcher( MockControl.EQUALS_MATCHER );
259         servletAuthControl.setReturnValue( true );
260                 
261      // ArchivaDavResourceFactory#isAuthorized()
262         SecuritySession session = new DefaultSecuritySession();
263         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
264         httpAuthControl.expectAndReturn( httpAuth.getSecuritySession( ic.getRequest().getSession( true) ), session );
265         servletAuthControl.expectAndThrow( servletAuth.isAuthenticated( null, result ),
266                                            new AuthenticationException( "Authentication error" ) );
267         
268         // check if guest has write access
269         servletAuth.isAuthorized( "guest", "internal", true );
270         servletAuthControl.setMatcher( MockControl.EQUALS_MATCHER );
271         servletAuthControl.setReturnValue( true );
272         
273         httpAuthControl.replay();
274         servletAuthControl.replay();
275
276         servlet.service( ic.getRequest(), ic.getResponse() );
277
278         httpAuthControl.verify();
279         servletAuthControl.verify();
280
281         // assertEquals( HttpServletResponse.SC_CREATED, response.getResponseCode() );
282     }
283
284     // test deploy with a valid user with no write access
285     public void testPutWithValidUserWithNoWriteAccess()
286         throws Exception
287     {
288         setupCleanRepo( repoRootInternal );
289
290         String putUrl = "http://machine.com/repository/internal/path/to/artifact.jar";
291         InputStream is = getClass().getResourceAsStream( "/artifact.jar" );
292         assertNotNull( "artifact.jar inputstream", is );
293         
294         WebRequest request = new PutMethodWebRequest( putUrl, is, "application/octet-stream" );
295         
296         InvocationContext ic = sc.newInvocation( request ); 
297         servlet = (RepositoryServlet) ic.getServlet();
298         servlet.setDavSessionProvider( davSessionProvider );
299         
300         ArchivaDavResourceFactory archivaDavResourceFactory = (ArchivaDavResourceFactory) servlet.getResourceFactory();
301         archivaDavResourceFactory.setHttpAuth( httpAuth );
302         archivaDavResourceFactory.setServletAuth( servletAuth );
303         servlet.setResourceFactory( archivaDavResourceFactory );
304
305         AuthenticationResult result = new AuthenticationResult();
306         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
307         servletAuthControl.expectAndReturn( servletAuth.isAuthenticated( null, null ), true );
308         
309      // ArchivaDavResourceFactory#isAuthorized()
310         SecuritySession session = new DefaultSecuritySession();
311         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
312         httpAuthControl.expectAndReturn( httpAuth.getSecuritySession( ic.getRequest().getSession( true ) ), session );
313         servletAuthControl.expectAndReturn( servletAuth.isAuthenticated( null, result ), true );
314         servletAuthControl.expectAndThrow( servletAuth.isAuthorized( null, session, "internal", true ),
315                                            new UnauthorizedException( "User not authorized" ) );
316                 
317         httpAuthControl.replay();
318         servletAuthControl.replay();
319         
320         servlet.service( ic.getRequest(), ic.getResponse() );
321
322         httpAuthControl.verify();
323         servletAuthControl.verify();
324         
325         // assertEquals(HttpServletResponse.SC_UNAUTHORIZED, response.getResponseCode());
326     }
327
328     // test deploy with a valid user with write access
329     public void testPutWithValidUserWithWriteAccess()
330         throws Exception
331     {
332         setupCleanRepo( repoRootInternal );
333         assertTrue( repoRootInternal.exists() );
334
335         String putUrl = "http://machine.com/repository/internal/path/to/artifact.jar";
336         InputStream is = getClass().getResourceAsStream( "/artifact.jar" );
337         assertNotNull( "artifact.jar inputstream", is );
338
339         WebRequest request = new PutMethodWebRequest( putUrl, is, "application/octet-stream" );
340
341         InvocationContext ic = sc.newInvocation( request );
342         servlet = (RepositoryServlet) ic.getServlet();
343         servlet.setDavSessionProvider( davSessionProvider );
344
345         ArchivaDavResourceFactory archivaDavResourceFactory = (ArchivaDavResourceFactory) servlet.getResourceFactory();
346         archivaDavResourceFactory.setHttpAuth( httpAuth );
347         archivaDavResourceFactory.setServletAuth( servletAuth );
348
349         servlet.setResourceFactory( archivaDavResourceFactory );
350
351         AuthenticationResult result = new AuthenticationResult();
352         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
353         servletAuthControl.expectAndReturn( servletAuth.isAuthenticated( null, null ), true );
354
355         // ArchivaDavResourceFactory#isAuthorized()
356         SecuritySession session = new DefaultSecuritySession();
357         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
358         httpAuthControl.expectAndReturn( httpAuth.getSecuritySession( ic.getRequest().getSession( true ) ), session );
359         servletAuthControl.expectAndReturn( servletAuth.isAuthenticated( null, result ), true );
360         servletAuthControl.expectAndReturn( servletAuth.isAuthorized( null, session, "internal", true ), true );
361
362         httpAuthControl.replay();
363         servletAuthControl.replay();
364
365         servlet.service( ic.getRequest(), ic.getResponse() );
366
367         httpAuthControl.verify();
368         servletAuthControl.verify();
369
370         // assertEquals(HttpServletResponse.SC_CREATED, response.getResponseCode());
371     }
372
373     // test get with invalid user, and guest has read access to repo
374     public void testGetWithInvalidUserAndGuestHasReadAccess()
375         throws Exception
376     {
377         String commonsLangJar = "commons-lang/commons-lang/2.1/commons-lang-2.1.jar";
378         String expectedArtifactContents = "dummy-commons-lang-artifact";
379
380         File artifactFile = new File( repoRootInternal, commonsLangJar );
381         artifactFile.getParentFile().mkdirs();
382
383         FileUtils.writeStringToFile( artifactFile, expectedArtifactContents, null );
384
385         WebRequest request = new GetMethodWebRequest( "http://machine.com/repository/internal/" + commonsLangJar );
386         InvocationContext ic = sc.newInvocation( request );
387         servlet = (RepositoryServlet) ic.getServlet();
388         servlet.setDavSessionProvider( davSessionProvider );
389         
390         ArchivaDavResourceFactory archivaDavResourceFactory = (ArchivaDavResourceFactory) servlet.getResourceFactory();
391         archivaDavResourceFactory.setHttpAuth( httpAuth );
392         archivaDavResourceFactory.setServletAuth( servletAuth );
393
394         servlet.setResourceFactory( archivaDavResourceFactory );
395
396         AuthenticationResult result = new AuthenticationResult();
397         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
398         servletAuthControl.expectAndThrow( servletAuth.isAuthenticated( null, null ),
399                                            new AuthenticationException( "Authentication error" ) );
400         servletAuthControl.expectAndReturn( servletAuth.isAuthorized( "guest", "internal", false ), true );
401         
402      // ArchivaDavResourceFactory#isAuthorized()
403         SecuritySession session = new DefaultSecuritySession();
404         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
405         httpAuthControl.expectAndReturn( httpAuth.getSecuritySession( ic.getRequest().getSession( true ) ), session );
406         servletAuthControl.expectAndReturn( servletAuth.isAuthenticated( null, result ), true );
407         servletAuthControl.expectAndReturn( servletAuth.isAuthorized( null, session, "internal", true ), true );
408
409         httpAuthControl.replay();
410         servletAuthControl.replay();
411
412         WebResponse response = sc.getResponse( request );
413
414         httpAuthControl.verify();
415         servletAuthControl.verify();
416
417         assertEquals( HttpServletResponse.SC_OK, response.getResponseCode() );
418         assertEquals( "Expected file contents", expectedArtifactContents, response.getText() );
419     }
420
421     // test get with invalid user, and guest has no read access to repo
422     public void testGetWithInvalidUserAndGuestHasNoReadAccess()
423         throws Exception
424     {
425         String commonsLangJar = "commons-lang/commons-lang/2.1/commons-lang-2.1.jar";
426         String expectedArtifactContents = "dummy-commons-lang-artifact";
427
428         File artifactFile = new File( repoRootInternal, commonsLangJar );
429         artifactFile.getParentFile().mkdirs();
430
431         FileUtils.writeStringToFile( artifactFile, expectedArtifactContents, null );
432
433         WebRequest request = new GetMethodWebRequest( "http://machine.com/repository/internal/" + commonsLangJar );
434         InvocationContext ic = sc.newInvocation( request );
435         servlet = (RepositoryServlet) ic.getServlet();
436         servlet.setDavSessionProvider( davSessionProvider );
437
438         AuthenticationResult result = new AuthenticationResult();
439         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
440         servletAuthControl.expectAndThrow( servletAuth.isAuthenticated( null, null ),
441                                            new AuthenticationException( "Authentication error" ) );
442         servletAuthControl.expectAndReturn( servletAuth.isAuthorized( "guest", "internal", false ), false );
443
444         httpAuthControl.replay();
445         servletAuthControl.replay();
446
447         WebResponse response = sc.getResponse( request );
448
449         httpAuthControl.verify();
450         servletAuthControl.verify();
451
452         assertEquals( HttpServletResponse.SC_UNAUTHORIZED, response.getResponseCode() );
453     }
454
455     // test get with valid user with read access to repo
456     public void testGetWithAValidUserWithReadAccess()
457         throws Exception
458     {
459         String commonsLangJar = "commons-lang/commons-lang/2.1/commons-lang-2.1.jar";
460         String expectedArtifactContents = "dummy-commons-lang-artifact";
461
462         File artifactFile = new File( repoRootInternal, commonsLangJar );
463         artifactFile.getParentFile().mkdirs();
464
465         FileUtils.writeStringToFile( artifactFile, expectedArtifactContents, null );
466
467         WebRequest request = new GetMethodWebRequest( "http://machine.com/repository/internal/" + commonsLangJar );
468         InvocationContext ic = sc.newInvocation( request );
469         servlet = (RepositoryServlet) ic.getServlet();
470         servlet.setDavSessionProvider( davSessionProvider );
471
472         ArchivaDavResourceFactory archivaDavResourceFactory = (ArchivaDavResourceFactory) servlet.getResourceFactory();
473         archivaDavResourceFactory.setHttpAuth( httpAuth );
474         archivaDavResourceFactory.setServletAuth( servletAuth );
475
476         servlet.setResourceFactory( archivaDavResourceFactory );
477         
478         AuthenticationResult result = new AuthenticationResult();
479         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
480         servletAuthControl.expectAndReturn( servletAuth.isAuthenticated( null, null ), true );
481         
482      // ArchivaDavResourceFactory#isAuthorized()
483         SecuritySession session = new DefaultSecuritySession();
484         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
485         httpAuthControl.expectAndReturn( httpAuth.getSecuritySession( ic.getRequest().getSession( true ) ), session );
486         servletAuthControl.expectAndReturn( servletAuth.isAuthenticated( null, result ), true );
487         servletAuthControl.expectAndReturn( servletAuth.isAuthorized( null, session, "internal", true ), true );
488         
489         httpAuthControl.replay();
490         servletAuthControl.replay();
491
492         WebResponse response = sc.getResponse( request );
493         
494         httpAuthControl.verify();
495         servletAuthControl.verify();
496
497         assertEquals( HttpServletResponse.SC_OK, response.getResponseCode() );
498         assertEquals( "Expected file contents", expectedArtifactContents, response.getText() );
499     }
500
501     // test get with valid user with no read access to repo
502     public void testGetWithAValidUserWithNoReadAccess()
503         throws Exception
504     {
505         String commonsLangJar = "commons-lang/commons-lang/2.1/commons-lang-2.1.jar";
506         String expectedArtifactContents = "dummy-commons-lang-artifact";
507
508         File artifactFile = new File( repoRootInternal, commonsLangJar );
509         artifactFile.getParentFile().mkdirs();
510
511         FileUtils.writeStringToFile( artifactFile, expectedArtifactContents, null );
512
513         WebRequest request = new GetMethodWebRequest( "http://machine.com/repository/internal/" + commonsLangJar );
514         InvocationContext ic = sc.newInvocation( request );
515         servlet = (RepositoryServlet) ic.getServlet();
516         servlet.setDavSessionProvider( davSessionProvider );
517
518         ArchivaDavResourceFactory archivaDavResourceFactory = (ArchivaDavResourceFactory) servlet.getResourceFactory();
519         archivaDavResourceFactory.setHttpAuth( httpAuth );
520         archivaDavResourceFactory.setServletAuth( servletAuth );
521
522         servlet.setResourceFactory( archivaDavResourceFactory );
523         
524         AuthenticationResult result = new AuthenticationResult();
525         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
526         servletAuthControl.expectAndReturn( servletAuth.isAuthenticated( null, null ), true );
527
528      // ArchivaDavResourceFactory#isAuthorized()
529         SecuritySession session = new DefaultSecuritySession();
530         httpAuthControl.expectAndReturn( httpAuth.getAuthenticationResult( null, null ), result );
531         httpAuthControl.expectAndReturn( httpAuth.getSecuritySession( ic.getRequest().getSession( true ) ), session );
532         servletAuthControl.expectAndReturn( servletAuth.isAuthenticated( null, result ), true );
533         servletAuthControl.expectAndThrow( servletAuth.isAuthorized( null, session, "internal", true ),
534                                            new UnauthorizedException( "User not authorized to read repository." ) );
535         
536         httpAuthControl.replay();
537         servletAuthControl.replay();
538         
539         WebResponse response = sc.getResponse( request );
540
541         httpAuthControl.verify();
542         servletAuthControl.verify();
543         
544         assertEquals( HttpServletResponse.SC_UNAUTHORIZED, response.getResponseCode() );
545     }
546 }