-
Notifications
You must be signed in to change notification settings - Fork 5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Bug 66665: Provide option to supply role mapping from a properties file
- Loading branch information
Showing
12 changed files
with
391 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
168 changes: 168 additions & 0 deletions
168
java/org/apache/catalina/core/PropertiesRoleMappingListener.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
/* | ||
* Licensed to the Apache Software Foundation (ASF) under one or more | ||
* contributor license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright ownership. | ||
* The ASF licenses this file to You under the Apache License, Version 2.0 | ||
* (the "License"); you may not use this file except in compliance with | ||
* the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.apache.catalina.core; | ||
|
||
import java.io.FileNotFoundException; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.util.Map.Entry; | ||
import java.util.Objects; | ||
import java.util.Properties; | ||
|
||
import org.apache.catalina.Context; | ||
import org.apache.catalina.Lifecycle; | ||
import org.apache.catalina.LifecycleEvent; | ||
import org.apache.catalina.LifecycleListener; | ||
import org.apache.juli.logging.Log; | ||
import org.apache.juli.logging.LogFactory; | ||
import org.apache.tomcat.util.file.ConfigFileLoader; | ||
import org.apache.tomcat.util.res.StringManager; | ||
|
||
/** | ||
* Implementation of {@code LifecycleListener} that will populate the context's role mapping from a properties file. | ||
* <p> | ||
* This listener must only be nested within {@link Context} elements. | ||
* <p> | ||
* The keys represent application roles (e.g., admin, user, uservisor, etc.) while the values represent technical roles | ||
* (e.g., DNs, SIDs, UUIDs, etc.). A key can also be prefixed if, e.g., the properties file contains generic | ||
* application configuration as well: {@code app-roles.}. | ||
* <p> | ||
* Note: The default value for the {@code roleMappingFile} is {@code webapp:/WEB-INF/role-mapping.properties}. | ||
*/ | ||
public class PropertiesRoleMappingListener implements LifecycleListener { | ||
|
||
private static final String WEBAPP_PROTOCOL = "webapp:"; | ||
|
||
private static final Log log = LogFactory.getLog(PropertiesRoleMappingListener.class); | ||
/** | ||
* The string manager for this package. | ||
*/ | ||
private static final StringManager sm = StringManager.getManager(ContextNamingInfoListener.class); | ||
|
||
private String roleMappingFile = WEBAPP_PROTOCOL + "/WEB-INF/role-mapping.properties"; | ||
private String keyPrefix; | ||
|
||
/** | ||
* Sets the path to the role mapping properties file. You can use protocol {@code webapp:} and whatever | ||
* {@link ConfigFileLoader} supports. | ||
* | ||
* @param roleMappingFile the role mapping properties file to load from | ||
* @throws NullPointerException if roleMappingFile is null | ||
* @throws IllegalArgumentException if roleMappingFile is empty | ||
*/ | ||
public void setRoleMappingFile(String roleMappingFile) { | ||
Objects.requireNonNull(roleMappingFile, sm.getString("propertiesRoleMappingListener.roleMappingFileNull")); | ||
if (roleMappingFile.isEmpty()) { | ||
throw new IllegalArgumentException(sm.getString("propertiesRoleMappingListener.roleMappingFileEmpty")); | ||
} | ||
|
||
this.roleMappingFile = roleMappingFile; | ||
} | ||
|
||
/** | ||
* Gets the path to the role mapping properties file. | ||
* | ||
* @return the path to the role mapping properties file | ||
*/ | ||
public String getRoleMappingFile() { | ||
return roleMappingFile; | ||
} | ||
|
||
/** | ||
* Sets the prefix to filter from property keys. All other keys will be ignored which do not have the prefix. | ||
* | ||
* @param keyPrefix the properties key prefix | ||
*/ | ||
public void setKeyPrefix(String keyPrefix) { | ||
this.keyPrefix = keyPrefix; | ||
} | ||
|
||
/** | ||
* Gets the prefix to filter from property keys. | ||
* | ||
* @return the properties key prefix | ||
*/ | ||
public String getKeyPrefix() { | ||
return keyPrefix; | ||
} | ||
|
||
@Override | ||
public void lifecycleEvent(LifecycleEvent event) { | ||
if (event.getType().equals(Lifecycle.CONFIGURE_START_EVENT)) { | ||
if (!(event.getLifecycle() instanceof Context)) { | ||
log.warn(sm.getString("listener.notContext", event.getLifecycle().getClass().getSimpleName())); | ||
return; | ||
} | ||
Context context = (Context) event.getLifecycle(); | ||
|
||
InputStream is; | ||
if (roleMappingFile.startsWith(WEBAPP_PROTOCOL)) { | ||
String path = roleMappingFile.substring(WEBAPP_PROTOCOL.length()); | ||
is = context.getServletContext().getResourceAsStream(path); | ||
} else { | ||
try { | ||
is = ConfigFileLoader.getSource().getResource(roleMappingFile).getInputStream(); | ||
} catch (FileNotFoundException e1) { | ||
is = null; | ||
} catch (IOException e2) { | ||
throw new IllegalStateException( | ||
sm.getString("propertiesRoleMappingListener.roleMappingFileFail", roleMappingFile), e2); | ||
} | ||
} | ||
|
||
if (is == null) { | ||
throw new IllegalStateException( | ||
sm.getString("propertiesRoleMappingListener.roleMappingFileNotFound", roleMappingFile)); | ||
} | ||
|
||
Properties props = new Properties(); | ||
|
||
try (InputStream _is = is) { | ||
props.load(_is); | ||
} catch (IOException e) { | ||
throw new IllegalStateException( | ||
sm.getString("propertiesRoleMappingListener.roleMappingFileFail", roleMappingFile), e); | ||
} | ||
|
||
int linkCount = 0; | ||
for (Entry<Object, Object> prop : props.entrySet()) { | ||
String role = (String) prop.getKey(); | ||
|
||
if (keyPrefix != null) { | ||
if (role.startsWith(keyPrefix)) { | ||
role = role.substring(keyPrefix.length()); | ||
} else { | ||
continue; | ||
} | ||
} | ||
|
||
String link = (String) prop.getValue(); | ||
|
||
if (log.isTraceEnabled()) { | ||
log.trace(sm.getString("propertiesRoleMappingListener.linkedRole", role, link)); | ||
} | ||
context.addRoleMapping(role, link); | ||
linkCount++; | ||
} | ||
|
||
if (log.isDebugEnabled()) { | ||
log.debug(sm.getString("propertiesRoleMappingListener.linkedRoleCount", linkCount)); | ||
} | ||
} | ||
} | ||
|
||
} |
168 changes: 168 additions & 0 deletions
168
test/org/apache/catalina/core/TestPropertiesRoleMappingListener.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
/* | ||
* Licensed to the Apache Software Foundation (ASF) under one or more | ||
* contributor license agreements. See the NOTICE file distributed with | ||
* this work for additional information regarding copyright ownership. | ||
* The ASF licenses this file to You under the Apache License, Version 2.0 | ||
* (the "License"); you may not use this file except in compliance with | ||
* the License. You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
package org.apache.catalina.core; | ||
|
||
import java.io.File; | ||
import java.io.IOException; | ||
import java.nio.charset.StandardCharsets; | ||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
import org.apache.catalina.Context; | ||
import org.apache.catalina.LifecycleException; | ||
import org.apache.catalina.authenticator.BasicAuthenticator; | ||
import org.apache.catalina.servlets.DefaultServlet; | ||
import org.apache.catalina.startup.TesterMapRealm; | ||
import org.apache.catalina.startup.Tomcat; | ||
import org.apache.catalina.startup.TomcatBaseTest; | ||
import org.apache.tomcat.util.buf.ByteChunk; | ||
import org.apache.tomcat.util.codec.binary.Base64; | ||
import org.apache.tomcat.util.descriptor.web.LoginConfig; | ||
import org.apache.tomcat.util.descriptor.web.SecurityCollection; | ||
import org.apache.tomcat.util.descriptor.web.SecurityConstraint; | ||
import org.junit.Assert; | ||
import org.junit.Test; | ||
|
||
import jakarta.servlet.http.HttpServletRequest; | ||
|
||
public class TestPropertiesRoleMappingListener extends TomcatBaseTest { | ||
|
||
@Test(expected = NullPointerException.class) | ||
public void testNullRoleMappingFile() throws Exception { | ||
PropertiesRoleMappingListener listener = new PropertiesRoleMappingListener(); | ||
listener.setRoleMappingFile(null); | ||
} | ||
|
||
@Test(expected = IllegalArgumentException.class) | ||
public void testEmptyRoleMappingFile() throws Exception { | ||
PropertiesRoleMappingListener listener = new PropertiesRoleMappingListener(); | ||
listener.setRoleMappingFile(""); | ||
} | ||
|
||
@Test(expected = LifecycleException.class) | ||
public void testNotFoundRoleMappingFile() throws Exception { | ||
Tomcat tomcat = getTomcatInstance(); | ||
|
||
Context ctx = tomcat.addContext("", null); | ||
|
||
PropertiesRoleMappingListener listener = new PropertiesRoleMappingListener(); | ||
ctx.addLifecycleListener(listener); | ||
|
||
try { | ||
tomcat.start(); | ||
} finally { | ||
tomcat.stop(); | ||
} | ||
} | ||
|
||
@Test | ||
public void testFileFromServletContext() throws Exception { | ||
doTest("webapp:/WEB-INF/role-mapping.properties", null); | ||
} | ||
|
||
@Test | ||
public void testFileFromServletContextWithKeyPrefix() throws Exception { | ||
doTest("webapp:/WEB-INF/prefixed-role-mapping.properties", "app-roles."); | ||
} | ||
|
||
@Test | ||
public void testFileFromClasspath() throws Exception { | ||
doTest("classpath:/com/example/role-mapping.properties", null); | ||
} | ||
|
||
@Test | ||
public void testFileFromClasspathWithKeyPrefix() throws Exception { | ||
doTest("classpath:/com/example/prefixed-role-mapping.properties", "app-roles."); | ||
} | ||
|
||
@Test | ||
public void testFileFromFile() throws Exception { | ||
File appDir = new File("test/webapp-role-mapping"); | ||
File file = new File(appDir, "WEB-INF/role-mapping.properties"); | ||
doTest(file.getAbsoluteFile().toURI().toASCIIString(), null); | ||
} | ||
|
||
@Test | ||
public void testFileFromFileWithKeyPrefix() throws Exception { | ||
File appDir = new File("test/webapp-role-mapping"); | ||
File file = new File(appDir, "WEB-INF/prefixed-role-mapping.properties"); | ||
doTest(file.getAbsoluteFile().toURI().toASCIIString(), "app-roles."); | ||
} | ||
|
||
private void doTest(String roleMappingFile, String keyPrefix) throws Exception { | ||
Tomcat tomcat = getTomcatInstance(); | ||
|
||
File appDir = new File("test/webapp-role-mapping"); | ||
Context ctx = tomcat.addContext("", appDir.getAbsolutePath()); | ||
|
||
PropertiesRoleMappingListener listener = new PropertiesRoleMappingListener(); | ||
listener.setRoleMappingFile(roleMappingFile); | ||
listener.setKeyPrefix(keyPrefix); | ||
ctx.addLifecycleListener(listener); | ||
|
||
Tomcat.addServlet(ctx, "default", new DefaultServlet()); | ||
ctx.addServletMappingDecoded("/", "default"); | ||
|
||
LoginConfig loginConfig = new LoginConfig(); | ||
loginConfig.setAuthMethod(HttpServletRequest.BASIC_AUTH); | ||
ctx.setLoginConfig(loginConfig); | ||
ctx.getPipeline().addValve(new BasicAuthenticator()); | ||
|
||
TesterMapRealm realm = new TesterMapRealm(); | ||
realm.addUser("foo", "bar"); | ||
// role 'admin' | ||
realm.addUserRole("foo", "de25f8f5-e534-4980-9351-e316384b1127"); | ||
realm.addUser("waldo", "fred"); | ||
// role 'user' | ||
realm.addUserRole("waldo", "13f6b886-cba8-4b5b-9a1b-06a6fe533356"); | ||
// role 'supervisor' | ||
realm.addUserRole("waldo", "45071e9a-13ef-11ee-89dc-20677cd45840"); | ||
ctx.setRealm(realm); | ||
|
||
for (String role : Arrays.asList("admin", "user", "unmapped")) { | ||
SecurityCollection securityCollection = new SecurityCollection(); | ||
securityCollection.addPattern("/" + role); | ||
SecurityConstraint constraint = new SecurityConstraint(); | ||
constraint.addAuthRole(role); | ||
constraint.addCollection(securityCollection); | ||
ctx.addConstraint(constraint); | ||
ctx.addSecurityRole(role); | ||
} | ||
|
||
tomcat.start(); | ||
|
||
testRequest("foo:bar", "/admin", 200); | ||
testRequest("waldo:fred", "/user", 200); | ||
testRequest("waldo:fred", "/unmapped", 403); | ||
testRequest("bar:baz", "/user", 401); | ||
} | ||
|
||
private void testRequest(String credentials, String path, int statusCode) throws IOException { | ||
ByteChunk out = new ByteChunk(); | ||
Map<String, List<String>> reqHead = new HashMap<>(); | ||
List<String> head = new ArrayList<>(); | ||
head.add(HttpServletRequest.BASIC_AUTH + " " + | ||
Base64.encodeBase64String(credentials.getBytes(StandardCharsets.ISO_8859_1))); | ||
reqHead.put("Authorization", head); | ||
int rc = getUrl("http://localhost:" + getPort() + path, out, reqHead, null); | ||
Assert.assertEquals(statusCode, rc); | ||
} | ||
|
||
} |
2 changes: 2 additions & 0 deletions
2
test/webapp-role-mapping/WEB-INF/classes/com/example/prefixed-role-mapping.properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
app-roles.admin=de25f8f5-e534-4980-9351-e316384b1127 | ||
app-roles.user=13f6b886-cba8-4b5b-9a1b-06a6fe533356 |
2 changes: 2 additions & 0 deletions
2
test/webapp-role-mapping/WEB-INF/classes/com/example/role-mapping.properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
admin=de25f8f5-e534-4980-9351-e316384b1127 | ||
user=13f6b886-cba8-4b5b-9a1b-06a6fe533356 |
2 changes: 2 additions & 0 deletions
2
test/webapp-role-mapping/WEB-INF/prefixed-role-mapping.properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
app-roles.admin=de25f8f5-e534-4980-9351-e316384b1127 | ||
app-roles.user=13f6b886-cba8-4b5b-9a1b-06a6fe533356 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
admin=de25f8f5-e534-4980-9351-e316384b1127 | ||
user=13f6b886-cba8-4b5b-9a1b-06a6fe533356 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
admin |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
unmapped |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
user |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.