-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Support ApiServer to enforce POST requests for state changing APIs and requests with timestamps #10899
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support ApiServer to enforce POST requests for state changing APIs and requests with timestamps #10899
Changes from all commits
57919be
d233aa6
8543de0
559891e
cc55e20
1d10bd0
7ffb32f
7276762
1f3c330
a1f2604
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -22,8 +22,11 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.net.UnknownHostException; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Arrays; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.HashMap; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.HashSet; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.List; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Map; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.regex.Pattern; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Set; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import javax.inject.Inject; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import javax.servlet.ServletConfig; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -46,6 +49,7 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.cloudstack.context.CallContext; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.cloudstack.managed.context.ManagedContext; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.cloudstack.utils.consoleproxy.ConsoleAccessUtils; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.commons.collections.MapUtils; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.logging.log4j.Logger; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import org.apache.logging.log4j.LogManager; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -78,6 +82,39 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final Logger ACCESSLOGGER = LogManager.getLogger("apiserver." + ApiServlet.class.getName()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final String REPLACEMENT = "_"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final String LOGGER_REPLACEMENTS = "[\n\r\t]"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final Pattern GET_REQUEST_COMMANDS = Pattern.compile("^(get|list|query|find)(\\w+)+$"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final HashSet<String> GET_REQUEST_COMMANDS_LIST = new HashSet<>(Set.of("isaccountallowedtocreateofferingswithtags", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "readyforshutdown", "cloudianisenabled", "quotabalance", "quotasummary", "quotatarifflist", "quotaisenabled", "quotastatement", "verifyoauthcodeandgetuser")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private static final HashSet<String> POST_REQUESTS_TO_DISABLE_LOGGING = new HashSet<>(Set.of( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "login", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "oauthlogin", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "createaccount", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "createuser", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "updateuser", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "forgotpassword", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "resetpassword", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "importrole", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "updaterolepermission", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "updateprojectrolepermission", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "createstoragepool", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "addhost", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "updatehostpassword", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "addcluster", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "addvmwaredc", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "configureoutofbandmanagement", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "uploadcustomcertificate", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "addciscovnmcresource", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "addnetscalerloadbalancer", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "createtungstenfabricprovider", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "addnsxcontroller", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "configtungstenfabricservice", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "createnetworkacl", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "updatenetworkaclitem", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "quotavalidateactivationrule", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "quotatariffupdate", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "listandswitchsamlaccount", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "uploadresourceicon" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Inject | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ApiServerService apiServer; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -193,11 +230,24 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| utf8Fixup(req, params); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final Object[] commandObj = params.get(ApiConstants.COMMAND); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String command = commandObj == null ? null : (String) commandObj[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // logging the request start and end in management log for easy debugging | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String reqStr = ""; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String cleanQueryString = StringUtils.cleanString(req.getQueryString()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (LOGGER.isDebugEnabled()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reqStr = auditTrailSb.toString() + " " + cleanQueryString; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (req.getMethod().equalsIgnoreCase("POST") && org.apache.commons.lang3.StringUtils.isNotBlank(command)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!POST_REQUESTS_TO_DISABLE_LOGGING.contains(command.toLowerCase()) && !reqParams.containsKey(ApiConstants.USER_DATA)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String cleanParamsString = getCleanParamsString(reqParams); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (org.apache.commons.lang3.StringUtils.isNotBlank(cleanParamsString)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reqStr += "\n" + cleanParamsString; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| reqStr += " " + command; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LOGGER.debug("===START=== " + reqStr); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -213,8 +263,6 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| responseType = (String)responseTypeParam[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final Object[] commandObj = params.get(ApiConstants.COMMAND); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String command = commandObj == null ? null : (String) commandObj[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final Object[] userObj = params.get(ApiConstants.USERNAME); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String username = userObj == null ? null : (String)userObj[0]; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (LOGGER.isTraceEnabled()) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -317,6 +365,19 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (apiServer.isPostRequestsAndTimestampsEnforced() && !isStateChangingCommandUsingPOST(command, req.getMethod(), params)) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| String errorText = String.format("State changing command %s needs to be sent using POST request", command); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (command.equalsIgnoreCase("updateConfiguration") && params.containsKey("name")) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| errorText = String.format("Changes for configuration %s needs to be sent using POST request", params.get("name")[0]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| auditTrailSb.append(" " + HttpServletResponse.SC_BAD_REQUEST + " " + errorText); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| final String serializedResponse = | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| apiServer.getSerializedApiError(new ServerApiException(ApiErrorCode.BAD_REQUEST, errorText), params, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| responseType); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| HttpUtils.writeHttpResponse(resp, serializedResponse, HttpServletResponse.SC_BAD_REQUEST, responseType, ApiServer.JSONcontentType.value()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Long userId = null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isNew) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| userId = (Long)session.getAttribute("userid"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -407,6 +468,15 @@ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return verify2FA; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private boolean isStateChangingCommandUsingPOST(String command, String method, Map<String, Object[]> params) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (command == null || (!GET_REQUEST_COMMANDS.matcher(command.toLowerCase()).matches() && !GET_REQUEST_COMMANDS_LIST.contains(command.toLowerCase()) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && !command.equalsIgnoreCase("updateConfiguration") && !method.equals("POST"))) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return false; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return !command.equalsIgnoreCase("updateConfiguration") || method.equals("POST") || (params.containsKey("name") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| && params.get("name")[0].toString().equalsIgnoreCase(ApiServer.EnforcePostRequestsAndTimestamps.key())); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+472
to
+477
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (command == null || (!GET_REQUEST_COMMANDS.matcher(command.toLowerCase()).matches() && !GET_REQUEST_COMMANDS_LIST.contains(command.toLowerCase()) | |
| && !command.equalsIgnoreCase("updateConfiguration") && !method.equals("POST"))) { | |
| return false; | |
| } | |
| return !command.equalsIgnoreCase("updateConfiguration") || method.equals("POST") || (params.containsKey("name") | |
| && params.get("name")[0].toString().equalsIgnoreCase(ApiServer.EnforcePostRequestsAndTimestamps.key())); | |
| // Check if the command is null | |
| if (command == null) { | |
| return false; | |
| } | |
| // Convert the command to lowercase for case-insensitive comparison | |
| String commandLowerCase = command.toLowerCase(); | |
| // Condition 1: Check if the command matches GET_REQUEST_COMMANDS or is in GET_REQUEST_COMMANDS_LIST | |
| boolean isGetRequestCommand = GET_REQUEST_COMMANDS.matcher(commandLowerCase).matches() || GET_REQUEST_COMMANDS_LIST.contains(commandLowerCase); | |
| // Condition 2: Check if the command is "updateConfiguration" and the method is not "POST" | |
| boolean isUpdateConfigWithNonPost = command.equalsIgnoreCase("updateConfiguration") && !method.equals("POST"); | |
| // If neither condition 1 nor condition 2 is satisfied, return false | |
| if (!isGetRequestCommand && !isUpdateConfigWithNonPost) { | |
| return false; | |
| } | |
| // Condition 3: Check if the command is "updateConfiguration" and the method is "POST" | |
| boolean isUpdateConfigWithPost = command.equalsIgnoreCase("updateConfiguration") && method.equals("POST"); | |
| // Condition 4: Check if the "name" parameter exists and matches the required key | |
| boolean isEnforcePostRequest = params.containsKey("name") && | |
| params.get("name")[0].toString().equalsIgnoreCase(ApiServer.EnforcePostRequestsAndTimestamps.key()); | |
| // Return true if either condition 3 or condition 4 is satisfied | |
| return isUpdateConfigWithPost || isEnforcePostRequest; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,18 +23,10 @@ import { | |
| ACCESS_TOKEN | ||
| } from '@/store/mutation-types' | ||
|
|
||
| export function api (command, args = {}, method = 'GET', data = {}) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @sureshanaparti wouldn't it be easier to refactor this method itself to enforce use of POST/GET based on configuration/API type? We won't have to modify each caller in that case. Current change will cause merge conflicts for almost all UI related active PRs. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. will check it @shwstppr There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @shwstppr As discussed offline, the api method is refactored to smaller, separate functions to handle GET & POST calls. These new calls also indicate the underlying method (GET / POST) used for the API call. Also, it's better to have POST method calls for all non-listing, state changing APIs from UI, so keeping these changes as it is. Any merge conflicts / issues, we(or the authors)'ll resolve them. |
||
| let params = {} | ||
| export function getAPI (command, args = {}) { | ||
| args.command = command | ||
| args.response = 'json' | ||
|
|
||
| if (data) { | ||
| params = new URLSearchParams() | ||
| Object.entries(data).forEach(([key, value]) => { | ||
| params.append(key, value) | ||
| }) | ||
| } | ||
|
|
||
| const sessionkey = vueProps.$localStorage.get(ACCESS_TOKEN) || Cookies.get('sessionkey') | ||
| if (sessionkey) { | ||
| args.sessionkey = sessionkey | ||
|
|
@@ -45,8 +37,30 @@ export function api (command, args = {}, method = 'GET', data = {}) { | |
| ...args | ||
| }, | ||
| url: '/', | ||
| method, | ||
| data: params || {} | ||
| method: 'GET' | ||
| }) | ||
| } | ||
|
|
||
| export function postAPI (command, data = {}) { | ||
| const params = new URLSearchParams() | ||
| params.append('command', command) | ||
| params.append('response', 'json') | ||
| if (data) { | ||
| Object.entries(data).forEach(([key, value]) => { | ||
| if (value !== undefined && value !== null && value !== '') { | ||
| params.append(key, value) | ||
| } | ||
| }) | ||
| } | ||
|
|
||
| const sessionkey = vueProps.$localStorage.get(ACCESS_TOKEN) || Cookies.get('sessionkey') | ||
| if (sessionkey) { | ||
| params.append('sessionkey', sessionkey) | ||
| } | ||
| return axios({ | ||
| url: '/', | ||
| method: 'POST', | ||
| data: params | ||
| }) | ||
| } | ||
|
|
||
|
|
@@ -56,7 +70,7 @@ export function login (arg) { | |
| } | ||
|
|
||
| // Logout before login is called to purge any duplicate sessionkey cookies | ||
| api('logout') | ||
| postAPI('logout') | ||
|
|
||
| const params = new URLSearchParams() | ||
| params.append('command', 'login') | ||
|
|
@@ -66,7 +80,7 @@ export function login (arg) { | |
| params.append('response', 'json') | ||
| return axios({ | ||
| url: '/', | ||
| method: 'post', | ||
| method: 'POST', | ||
| data: params, | ||
| headers: { | ||
| 'content-type': 'application/x-www-form-urlencoded' | ||
|
|
@@ -77,7 +91,7 @@ export function login (arg) { | |
| export function logout () { | ||
| message.destroy() | ||
| notification.destroy() | ||
| return api('logout') | ||
| return postAPI('logout') | ||
| } | ||
|
|
||
| export function oauthlogin (arg) { | ||
|
|
@@ -86,7 +100,7 @@ export function oauthlogin (arg) { | |
| } | ||
|
|
||
| // Logout before login is called to purge any duplicate sessionkey cookies | ||
| api('logout') | ||
| postAPI('logout') | ||
|
|
||
| const params = new URLSearchParams() | ||
| params.append('command', 'oauthlogin') | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If the configuration 'enforce.post.requests.and.timestamps' might change at runtime, consider updating isPostRequestsAndTimestampsEnforced dynamically rather than only at startup.