Skip to content
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

Restrict runtime config property changes #45606

Open
antonwiens opened this issue Jan 15, 2025 · 8 comments
Open

Restrict runtime config property changes #45606

antonwiens opened this issue Jan 15, 2025 · 8 comments
Labels
area/config kind/enhancement New feature or request

Comments

@antonwiens
Copy link

Description

I would like support to restrict (all) quarkus properties from being changed at runtime and only allow changes to specific quarkus properties or custom properties. This would be nice for distributed applications to be protected from users changing sensitive properties that should not be changed. this should also work in native images.

Currently i do not see the possibility to do this. If there is a workaround for this i would really appreciate someone commenting it here.

Implementation ideas

No response

@antonwiens antonwiens added the kind/enhancement New feature or request label Jan 15, 2025
Copy link

quarkus-bot bot commented Jan 15, 2025

/cc @radcortez (config)

@radcortez
Copy link
Member

I believe there are a couple of options to implement this:

You can add a config source with max ordinality. That will place that source at the top of lookup, effectively negating any overrides. We do something similar for our configuration, that is, build time and runtime fixed:

  • @BuildStep
    void buildTimeRunTimeConfig(
    ConfigurationBuildItem configItem,
    BuildProducer<GeneratedClassBuildItem> generatedClass,
    BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
    BuildProducer<StaticInitConfigBuilderBuildItem> staticInitConfigBuilder,
    BuildProducer<RunTimeConfigBuilderBuildItem> runTimeConfigBuilder) {
    String builderClassName = "io.quarkus.runtime.generated.BuildTimeRunTimeFixedConfigSourceBuilder";
    try (ClassCreator classCreator = ClassCreator.builder()
    .classOutput(new GeneratedClassGizmoAdaptor(generatedClass, true))
    .className(builderClassName)
    .interfaces(ConfigBuilder.class)
    .setFinal(true)
    .build()) {
    FieldDescriptor source = FieldDescriptor.of(classCreator.getClassName(), "source", ConfigSource.class);
    classCreator.getFieldCreator(source).setModifiers(Opcodes.ACC_STATIC | Opcodes.ACC_FINAL);
    MethodCreator clinit = classCreator.getMethodCreator("<clinit>", void.class);
    clinit.setModifiers(Opcodes.ACC_STATIC);
    ResultHandle map = clinit.newInstance(MethodDescriptor.ofConstructor(HashMap.class));
    MethodDescriptor put = MethodDescriptor.ofMethod(Map.class, "put", Object.class, Object.class, Object.class);
    for (Map.Entry<String, ConfigValue> entry : configItem.getReadResult().getBuildTimeRunTimeValues().entrySet()) {
    clinit.invokeInterfaceMethod(put, map, clinit.load(entry.getKey()), clinit.load(entry.getValue().getValue()));
    }
    ResultHandle defaultValuesSource = clinit.newInstance(
    MethodDescriptor.ofConstructor(DefaultValuesConfigSource.class, Map.class, String.class, int.class), map,
    clinit.load("BuildTime RunTime Fixed"), clinit.load(Integer.MAX_VALUE));
    ResultHandle disableableConfigSource = clinit.newInstance(
    MethodDescriptor.ofConstructor(DisableableConfigSource.class, ConfigSource.class),
    defaultValuesSource);
    clinit.writeStaticField(source, disableableConfigSource);
    clinit.returnVoid();
    MethodCreator method = classCreator.getMethodCreator(CONFIG_BUILDER);
    ResultHandle configBuilder = method.getMethodParam(0);
    ResultHandle configSources = method.newArray(ConfigSource.class, 1);
    method.writeArrayValue(configSources, 0, method.readStaticField(source));
    method.invokeVirtualMethod(WITH_SOURCES, configBuilder, configSources);
    method.returnValue(configBuilder);
    }
    reflectiveClass.produce(ReflectiveClassBuildItem.builder(builderClassName).reason(getClass().getName()).build());
    staticInitConfigBuilder.produce(new StaticInitConfigBuilderBuildItem(builderClassName));
    runTimeConfigBuilder.produce(new RunTimeConfigBuilderBuildItem(builderClassName));
    }
  • public void handleConfigChange(Map<String, ConfigValue> buildTimeRuntimeValues) {
    SmallRyeConfig config = ConfigProvider.getConfig().unwrap(SmallRyeConfig.class);
    // Disable the BuildTime RunTime Fixed (has the highest ordinal), because a lookup will get the expected value,
    // and we have no idea if the user tried to override it in another source.
    Optional<ConfigSource> builtTimeRunTimeFixedConfigSource = config.getConfigSource("BuildTime RunTime Fixed");
    if (builtTimeRunTimeFixedConfigSource.isPresent()) {
    ConfigSource configSource = builtTimeRunTimeFixedConfigSource.get();
    if (configSource instanceof DisableableConfigSource) {
    ((DisableableConfigSource) configSource).disable();
    }
    }
    List<String> mismatches = new ArrayList<>();
    for (Map.Entry<String, ConfigValue> entry : buildTimeRuntimeValues.entrySet()) {
    ConfigValue currentValue = config.getConfigValue(entry.getKey());
    // Check for changes. Also, we only have a change if the source ordinal is higher
    // The config value can be null (for ex. if the property uses environment variables not available at build time)
    if (currentValue.getValue() != null && !Objects.equals(entry.getValue().getValue(), currentValue.getValue())
    && entry.getValue().getSourceOrdinal() < currentValue.getSourceOrdinal()) {
    mismatches.add(
    " - " + entry.getKey() + " is set to '" + currentValue.getValue()
    + "' but it is build time fixed to '"
    + entry.getValue().getValue() + "'. Did you change the property " + entry.getKey()
    + " after building the application?");
    }
    }
    // Enable the BuildTime RunTime Fixed. It should be fine doing these operations, because this is on startup
    if (builtTimeRunTimeFixedConfigSource.isPresent()) {
    ConfigSource configSource = builtTimeRunTimeFixedConfigSource.get();
    if (configSource instanceof DisableableConfigSource) {
    ((DisableableConfigSource) configSource).enable();
    }
    }
    if (!mismatches.isEmpty()) {
    String msg = "Build time property cannot be changed at runtime:\n" + String.join("\n", mismatches);
    // TODO - This should use ConfigConfig, but for some reason, the test fails sometimes with mapping not found when looking ConfigConfig
    BuildTimeMismatchAtRuntime buildTimeMismatchAtRuntime = config
    .getOptionalValue("quarkus.config.build-time-mismatch-at-runtime", BuildTimeMismatchAtRuntime.class)
    .orElse(warn);
    if (fail.equals(buildTimeMismatchAtRuntime)) {
    throw new IllegalStateException(msg);
    } else if (warn.equals(buildTimeMismatchAtRuntime)) {
    log.warn(msg);
    }
    }
    }

This is not 100% bullet proof. A user could provide a source with max ordinality as well, and in that case, the result is undefined, because of the sort. For us, this has been good enough.

Another option is to provide an interceptor and throw an exception if the value is coming from an unexpected source. This can probably be implemented in a 100% reliable way. To avoid leaks, the ConfigValue only carries the ConfigSource name, so in theory, someone could also provide a source with the same name to try to bypass the protection. The interceptor instance could also check for a specific generated key from the "good" source to filter out the fake ones.

Ideally, SmallRyeConfig should have an API on its builder to provide a value that cannot be overridden, but we don't have that, and we didn't really need it so far. It is something we could consider.

@antonwiens
Copy link
Author

antonwiens commented Jan 16, 2025

Is there a way to have the config source interceptor only filter only during runtime and not build time?
And if possible, only include an interceptor for native-image?

@radcortez
Copy link
Member

Are you using a Quarkus extension, or is it just part of the application? The Quarkus extension is straightforward, but from the application, not so much.

@antonwiens
Copy link
Author

antonwiens commented Jan 17, 2025

Are you using a Quarkus extension, or is it just part of the application? The Quarkus extension is straightforward, but from the application, not so much.

I am using it in a application, but i am also used to writing extensions.

Your answer is indicating that i need to write an extension with a buildstep registering the service class only for runtime?

@radcortez
Copy link
Member

It is easier with an extension because you can use RunTimeConfigBuilderBuildItem and ImageMode to check. In the application, the only option I see is to check the CL for specific pieces to know which config phase you are in and if you are doing a native build.

@antonwiens
Copy link
Author

Do you have some recommendation for specific classes to check? I need a quick solution before creating a extension. Would really appreciate that.

@antonwiens
Copy link
Author

antonwiens commented Jan 17, 2025

Something like this is working for me to check for runtime env:

 private val isRuntime = kotlin.runCatching {
        Thread.currentThread().contextClassLoader.loadClass("io.quarkus.runtime.generated.BuildTimeRunTimeFixedConfigSourceBuilder")
        true
    }.getOrElse { false }

Do i miss something?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area/config kind/enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants