package com.google.inject; import static com.google.common.truth.Truth.assertThat; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE_USE; import static org.junit.Assert.assertThrows; import com.google.common.collect.ImmutableSet; import com.google.common.testing.TestLogHandler; import com.google.inject.RestrictedBindingSource.RestrictionLevel; import com.google.inject.spi.ElementSource; import com.google.inject.spi.Elements; import com.google.inject.spi.InjectionPoint; import com.google.inject.spi.ModuleAnnotatedMethodScanner; import com.google.inject.util.Modules; import java.lang.annotation.Annotation; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Method; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import jakarta.inject.Named; import jakarta.inject.Qualifier; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; /** * Tests for {@link RestrictedBindingSource}. * * @author vzm@google.com (Vladimir Makaric) */ @RunWith(JUnit4.class) public class RestrictedBindingSourceTest { // -------------------------------------------------------------------------- // Core Functionality Tests // -------------------------------------------------------------------------- private static final String BINDING_PERMISSION_ERROR = "Unable to bind key"; private static final String USE_NETWORK_MODULE = "Please install NetworkModule."; private static final String USE_ROUTING_MODULE = "Please install RoutingModule."; private static final String NETWORK_ANNOTATION_IS_RESTRICTED = "The @Network annotation can only be used to annotate Network library Keys."; @RestrictedBindingSource.Permit @Retention(RetentionPolicy.RUNTIME) @interface NetworkLibrary {} @Qualifier @RestrictedBindingSource( explanation = USE_NETWORK_MODULE, permits = {NetworkLibrary.class}) @Retention(RetentionPolicy.RUNTIME) @interface GatewayIpAdress {} @Qualifier @RestrictedBindingSource( explanation = USE_NETWORK_MODULE, permits = {NetworkLibrary.class}) @Retention(RetentionPolicy.RUNTIME) @interface Hostname {} @NetworkLibrary private static class NetworkModule extends AbstractModule { @Provides @GatewayIpAdress int provideIpAddress() { return 21321566; } @Override protected void configure() { bind(String.class).annotatedWith(Hostname.class).toInstance("google.com"); } } @Test public void networkLibraryCanProvideItsBindings() { Guice.createInjector(new NetworkModule()); } @RestrictedBindingSource( explanation = USE_ROUTING_MODULE, permits = {NetworkLibrary.class}) @ImplementedBy(RoutingTableImpl.class) // For testing untargetted bindings. interface RoutingTable { int getNextHopIpAddress(int destinationIpAddress); } @NetworkLibrary private static class RoutingModule extends AbstractModule { @Provides RoutingTable provideRoutingTable(@GatewayIpAdress int gateway) { return destinationIp -> gateway; } } @Test public void networkBindingCantBeProvidedByOtherModules() { AbstractModule rogueModule = new AbstractModule() { // This will fail, the gateway IP can only be provided by the network library. @Provides @GatewayIpAdress int provideGatewayIp() { return 42; } @Override protected void configure() { install(new RoutingModule()); } }; CreationException expected = assertThatInjectorCreationFails(rogueModule); assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR); assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE); } @Test public void missingImplementationErrorForRestrictedBindingIncludesExplanation() { CreationException expected = assertThatInjectorCreationFails(new RoutingModule()); assertThat(expected) .hasMessageThat() .contains( "Hint: This key is restricted and cannot be bound directly. Restriction explanation:" + " Please install NetworkModule."); } @Test public void canBindRestrictedTypeWithUnrestrictedQualifierAnnotation() { Guice.createInjector( new AbstractModule() { @Provides @Named("custom") RoutingTable provideRoutingTable() { return ip -> ip; } }); } @Test public void twoRogueNetworkBindingsYieldTwoErrorMessages() { AbstractModule rogueModule = new AbstractModule() { @Provides @GatewayIpAdress int provideGatewayIp() { return 42; } @Provides RoutingTable provideRoutingTable() { return destinationIp -> 0; } }; CreationException expected = assertThatInjectorCreationFails(rogueModule); assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR); assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE); assertThat(expected).hasMessageThat().contains(USE_ROUTING_MODULE); } @RestrictedBindingSource.Permit @Retention(RetentionPolicy.RUNTIME) @interface NetworkTestLibrary {} @Qualifier @RestrictedBindingSource( explanation = USE_NETWORK_MODULE, permits = {NetworkLibrary.class, NetworkTestLibrary.class}) @Retention(RetentionPolicy.RUNTIME) @interface MacAddress {} @NetworkTestLibrary private static class TestMacAddressModule extends AbstractModule { @Provides @MacAddress String provideMacAddress() { return "deadbeef"; } } @Test public void bindingWithTwoPermitsAllowedIfOnePresent() { Guice.createInjector(new TestMacAddressModule()); } private static class RoutingTableImpl implements RoutingTable { @Inject RoutingTableImpl() {} @Override public int getNextHopIpAddress(int destinationIpAddress) { return destinationIpAddress + 2; } } @Test public void untargettedBindingAllowedWithPermit() { @NetworkLibrary class PermittedNetworkModule extends AbstractModule { @Override protected void configure() { bind(RoutingTable.class); } } Guice.createInjector(new PermittedNetworkModule()); } @Test public void untargettedBindingDisallowedWithoutPermit() { AbstractModule rogueModule = new AbstractModule() { @Override protected void configure() { bind(RoutingTable.class); } }; CreationException expected = assertThatInjectorCreationFails(rogueModule); assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR); assertThat(expected).hasMessageThat().contains(USE_ROUTING_MODULE); } @RestrictedBindingSource.Permit @Retention(RetentionPolicy.RUNTIME) @Target(TYPE_USE) @interface FooPermit {} @Qualifier @RestrictedBindingSource( explanation = "Only modules with FooPermit can bind @Foo bindings.", permits = {FooPermit.class}) @Retention(RetentionPolicy.RUNTIME) @interface Foo {} @Test public void permitOnAnonymousClassWorks() { Guice.createInjector( new @FooPermit AbstractModule() { @Provides @Foo String provideFooString() { return "foo"; } }); } @Qualifier @RestrictedBindingSource( explanation = USE_NETWORK_MODULE, permits = {NetworkLibrary.class}, restrictionLevel = RestrictionLevel.WARNING) @Retention(RetentionPolicy.RUNTIME) @interface HostIp {} @Test public void rogueBindingWithWarningRestrictionLevel() { Logger logger = Logger.getLogger(RestrictedBindingSource.class.getName()); TestLogHandler testLogHandler = new TestLogHandler(); logger.addHandler(testLogHandler); Guice.createInjector( new AbstractModule() { @Provides @HostIp int provideRogueHostIp() { return 4; } }); List logs = testLogHandler.getStoredLogRecords(); assertThat(logs).hasSize(1); assertThat(logs.get(0).getLevel()).isEqualTo(Level.WARNING); assertThat(logs.get(0).getMessage()).contains(USE_NETWORK_MODULE); assertThat(logs.get(0).getMessage()).contains("provideRogueHostIp"); logger.removeHandler(testLogHandler); } // -------------------------------------------------------------------------- // Module Exemption Tests // -------------------------------------------------------------------------- private static final String USE_DNS_MODULE = "Use the official DNS module"; @Qualifier @RestrictedBindingSource( explanation = USE_DNS_MODULE, permits = {NetworkLibrary.class}, exemptModules = "com.google.inject.RestrictedBindingSourceTest\\$FooRogueDnsModule" + "|com.google.inject.RestrictedBindingSourceTest\\$BarRogueDnsModule" + "|com.google.inject.RestrictedBindingSourceTest\\$TopLevelModulePrivatelyBindingDnsAddress") @Retention(RetentionPolicy.RUNTIME) @interface DnsAddress {} static class FooRogueDnsModule extends AbstractModule { @Provides @DnsAddress int rogueDns() { return 4; } } static class BarRogueDnsModule extends AbstractModule { @Provides @DnsAddress int rogueDns() { return 5; } } // Non-exempt static class BazRogueDnsModule extends AbstractModule { @Provides @DnsAddress int rogueDns() { return 5; } } static class TopLevelModulePrivatelyBindingDnsAddress extends AbstractModule { @Override protected void configure() { install( new PrivateModule() { @Override protected void configure() { // Non-exempt module. install(new BazRogueDnsModule()); } }); } } @Test public void exemptModulesCanCreateRestrictedBinding() { Guice.createInjector(new FooRogueDnsModule()); Guice.createInjector(new BarRogueDnsModule()); } @Test public void nonExemptModuleCantCreateRestrictedBinding() { CreationException expected = assertThatInjectorCreationFails(new BazRogueDnsModule()); assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR); assertThat(expected).hasMessageThat().contains(USE_DNS_MODULE); } @Test public void parentModuleExeptionAppliesToChildPrivateModule() { Guice.createInjector(new TopLevelModulePrivatelyBindingDnsAddress()); } @Test public void exemptModuleCanBeOverridenIfRestrictedBindingIsNotOverriden() { // This tests that we check for exemptions on the module stack of the original element source. Guice.createInjector( Modules.override( new AbstractModule() { @Override protected void configure() { install(new BarRogueDnsModule()); } @Provides String random() { return "foo"; } }) .with( new AbstractModule() { @Provides String random() { return "bar"; } })); } // -------------------------------------------------------------------------- // Binder.withSource Tests // -------------------------------------------------------------------------- @NetworkLibrary private static class PermittedModule extends AbstractModule { @Override protected void configure() { Method userUnpermittedModuleMethod; try { userUnpermittedModuleMethod = UnpermittedModule.class.getMethod("foo"); } catch (NoSuchMethodException e) { throw new RuntimeException(e); } binder() .withSource(userUnpermittedModuleMethod) .bind(String.class) .annotatedWith(Hostname.class) .toInstance("google.com"); } } private static class UnpermittedModule extends AbstractModule { public String foo() { return "bar"; } } @Test public void permittedModuleCanWithSourceAnUnpermittedModuleMethod() { Guice.createInjector(new PermittedModule()); } @Test public void unpermittedModuleCantWithSourceAPermittedModule() { AbstractModule rogueModule = new AbstractModule() { @Override protected void configure() { binder() .withSource(PermittedModule.class) .bindConstant() .annotatedWith(GatewayIpAdress.class) .to(0); } }; CreationException expected = assertThatInjectorCreationFails(rogueModule); assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR); assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE); } @Test public void getElements_getModule_works() { Guice.createInjector(Elements.getModule(Elements.getElements(new NetworkModule()))); } // -------------------------------------------------------------------------- // ModuleAnnotatedMethodScanner tests // -------------------------------------------------------------------------- @Target(METHOD) @Retention(RetentionPolicy.RUNTIME) private @interface NetworkProvides {} @Qualifier @RestrictedBindingSource( explanation = NETWORK_ANNOTATION_IS_RESTRICTED, permits = {NetworkLibrary.class}) @Retention(RetentionPolicy.RUNTIME) @interface Network {} // Adds the NetworkLibrary-owned Network annotation to keys produced by @NetworkProvides methods. private static class NetworkProvidesScanner extends ModuleAnnotatedMethodScanner { @Override public String toString() { return "NetworkProvidesScanner"; } @Override public Set> annotationClasses() { return ImmutableSet.of(NetworkProvides.class); } @Override public Key prepareMethod( Binder binder, Annotation annotation, Key key, InjectionPoint injectionPoint) { return key.withAnnotation(Network.class); } } @Test public void rogueBindingByMethodScannerDenied() { AbstractModule rogueModule = new AbstractModule() { @NetworkProvides String provideNetworkString() { return "lorem ipsum"; } }; CreationException expected = assertThatInjectorCreationFails(rogueModule, scannerModule(new NetworkProvidesScanner())); assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR); assertThat(expected).hasMessageThat().contains(NETWORK_ANNOTATION_IS_RESTRICTED); } @Test public void bindingsAddedByMethodScannerAllowedByNetworkLib() { @NetworkLibrary class NetworkModuleWithCustomProvides extends AbstractModule { @NetworkProvides String provideNetworkString() { return "the real network string"; } } Guice.createInjector( new NetworkModuleWithCustomProvides(), scannerModule(new NetworkProvidesScanner())); } @Test public void scannerWithPermitCanCreateRestrictedBinding() { @NetworkLibrary class NetworkProvidesScannerWithPermit extends NetworkProvidesScanner {} AbstractModule moduleWithNetworkProvidesMethod = new AbstractModule() { @NetworkProvides String provideNetworkString() { return "lorem ipsum"; } }; Guice.createInjector( moduleWithNetworkProvidesMethod, scannerModule(new NetworkProvidesScannerWithPermit())); } @Test public void scannerWithPermitCanCreateRestrictedBindings() { @NetworkLibrary class NetworkProvidesScannerWithPermit extends NetworkProvidesScanner { @Override public Key prepareMethod( Binder binder, Annotation annotation, Key key, InjectionPoint injectionPoint) { binder.install( new AbstractModule() { @Provides @GatewayIpAdress int provideGatewayIp() { return 42; } }); return Key.get(key.getTypeLiteral(), Network.class); } } AbstractModule moduleWithNetworkProvidesMethod = new AbstractModule() { @NetworkProvides String provideNetworkString() { return "lorem ipsum"; } }; Injector injector = Guice.createInjector( moduleWithNetworkProvidesMethod, scannerModule(new NetworkProvidesScannerWithPermit())); assertThat(injector.getInstance(Key.get(Integer.class, GatewayIpAdress.class))).isEqualTo(42); } @Test public void moduleInstalledByScannerInheritsMethodModulePermit() { class NetworkProvidesScannerWithoutPermit extends NetworkProvidesScanner { @Override public Key prepareMethod( Binder binder, Annotation annotation, Key key, InjectionPoint injectionPoint) { binder.install( new AbstractModule() { @Provides @GatewayIpAdress int provideGatewayIp() { return 42; } }); return key.withAnnotation(Network.class); } } @NetworkLibrary class ScannedModuleWithPermit extends AbstractModule { @NetworkProvides String provideNetworkString() { return "lorem ipsum"; } } Injector injector = Guice.createInjector( new ScannedModuleWithPermit(), scannerModule(new NetworkProvidesScannerWithoutPermit())); assertThat(injector.getInstance(Key.get(Integer.class, GatewayIpAdress.class))).isEqualTo(42); } private static Module scannerModule(ModuleAnnotatedMethodScanner scanner) { return new AbstractModule() { @Override protected void configure() { binder().scanModulesForAnnotatedMethods(scanner); } }; } // -------------------------------------------------------------------------- // Modules.override tests // -------------------------------------------------------------------------- @Test public void modulesOverrideCantOverrideRestrictedBinding() { Module rogueModule = Modules.override(new NetworkModule()) .with( new AbstractModule() { @Provides @GatewayIpAdress int provideRogueGatewayIp() { return 12345; } }); CreationException expected = assertThatInjectorCreationFails(rogueModule); assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR); assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE); } @Test public void modulesOverrideRestrictedBindingAllowedIfParentHasPermit() { @NetworkLibrary class NetworkModuleVersion2 extends AbstractModule { @Override protected void configure() { install( Modules.override(new NetworkModule()) .with( new AbstractModule() { @Provides @GatewayIpAdress int provideGatewayIpV2() { return 2; } })); } } assertThat( Guice.createInjector(new NetworkModuleVersion2()) .getInstance(Key.get(Integer.class, GatewayIpAdress.class))) .isEqualTo(2); } @Qualifier @Retention(RetentionPolicy.RUNTIME) @interface UnrestrictedQualifier {} @Test public void modulesOverrideCanOverrideUnrestrictedBinding() { Module overrideModule = Modules.override( new NetworkModule(), new AbstractModule() { @Override protected void configure() { bindConstant().annotatedWith(UnrestrictedQualifier.class).to("foo"); } }) .with( new AbstractModule() { @Provides @UnrestrictedQualifier String provideRogueGatewayIp() { return "bar"; } }); assertThat( Guice.createInjector(overrideModule) .getInstance(Key.get(String.class, UnrestrictedQualifier.class))) .isEqualTo("bar"); } @Test public void nestedModulesOverrideCanOverrideUnrestrictedBindings() { Module overrideModule = Modules.override( Modules.override( new NetworkModule(), new AbstractModule() { @Override protected void configure() { bindConstant().annotatedWith(UnrestrictedQualifier.class).to("foo"); bindConstant().annotatedWith(UnrestrictedQualifier.class).to(42); } }) .with( new AbstractModule() { @Provides @UnrestrictedQualifier String overrideString() { return "bar"; } })) .with( new AbstractModule() { @Provides @UnrestrictedQualifier int overrideLong() { return 45; } }); Injector injector = Guice.createInjector(overrideModule); assertThat(injector.getInstance(Key.get(String.class, UnrestrictedQualifier.class))) .isEqualTo("bar"); assertThat(injector.getInstance(Key.get(Integer.class, UnrestrictedQualifier.class))) .isEqualTo(45); } @Test public void modulesOverridePrivateModule() { Guice.createInjector( Modules.override( new PrivateModule() { @Override protected void configure() { install(new NetworkModule()); expose(Key.get(String.class, Hostname.class)); } @Provides @Exposed @Named("custom-gateway") int customGateway(@GatewayIpAdress int gateway) { return gateway + 4; } }) .with( new AbstractModule() { @Provides @Named("custom-gateway") int provideCustomGatewayOverride() { return 12345; } })); } @Test public void originalElementSourceNotTrustedIfSetExternally() { ElementSource networkElementSource = (ElementSource) Elements.getElements(new NetworkModule()).get(0).getSource(); AbstractModule rogueModule = new AbstractModule() { @Override protected void configure() { binder() .withSource(networkElementSource) .bindConstant() .annotatedWith(GatewayIpAdress.class) .to(12); } }; // Confirm that the original element source was spoofed. @SuppressWarnings("unchecked") Binding rogueGatewayBinding = (Binding) Elements.getElements(rogueModule).get(0); assertThat(((ElementSource) rogueGatewayBinding.getSource()).getOriginalElementSource()) .isEqualTo(networkElementSource); // Will fail because the original element source isn't trusted, becase it wasn't set by Guice // internals. CreationException expected = assertThatInjectorCreationFails(rogueModule); assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR); assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE); } // -------------------------------------------------------------------------- // PrivateModule tests // -------------------------------------------------------------------------- @NetworkLibrary private static class NetworkModuleThatInstalls extends AbstractModule { final Module module; NetworkModuleThatInstalls(Module module) { this.module = module; } @Override protected void configure() { install(module); } } private static class PrivateModuleCreatesUnexposedNetworkBinding extends PrivateModule { @Override protected void configure() { bindConstant().annotatedWith(GatewayIpAdress.class).to(0); } } @Test public void parentHasPermit_childPrivateModuleCanBind() { Guice.createInjector( new NetworkModuleThatInstalls( // Allowed because the parent has the @NetworkLibrary permit. new PrivateModuleCreatesUnexposedNetworkBinding())); } @Test public void noPermitOnStack_privateModuleCantBind() { AbstractModule rogueModule = new AbstractModule() { @Override protected void configure() { // Disallowed because there's no permit on the module stack. install(new PrivateModuleCreatesUnexposedNetworkBinding()); } }; CreationException expected = assertThatInjectorCreationFails(rogueModule); assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR); assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE); } private static class PrivateModuleExposesNetworkBinding extends PrivateModule { @Override protected void configure() { install( new AbstractModule() { @Override protected void configure() { bindConstant().annotatedWith(GatewayIpAdress.class).to(0); } }); expose(Key.get(Integer.class, GatewayIpAdress.class)); } } @Test public void parentHasPermit_childPrivateModuleCanExposeBinding() { Guice.createInjector( new NetworkModuleThatInstalls( // Allowed because the parent has the @NetworkLibrary permit. new PrivateModuleExposesNetworkBinding())); } @Test public void noPermitOnStack_childPrivateModuleCantExposeBinding() { AbstractModule rogueModule = new AbstractModule() { @Override protected void configure() { // Disallowed because there's no permit on the module stack. install(new PrivateModuleExposesNetworkBinding()); } }; CreationException expected = assertThatInjectorCreationFails(rogueModule); assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR); assertThat(expected).hasMessageThat().contains(USE_NETWORK_MODULE); } // -------------------------------------------------------------------------- // Child Injector tests // -------------------------------------------------------------------------- @Test public void childInjectorCantBindRestrictedBindingWithoutPermit() { Injector parent = Guice.createInjector(new NetworkModule()); AbstractModule rogueModule = new AbstractModule() { @Provides RoutingTable provideRoutingTable() { return destinationIp -> 0; } }; CreationException expected = assertThrows(CreationException.class, () -> parent.createChildInjector(rogueModule)); assertThat(expected).hasMessageThat().contains(BINDING_PERMISSION_ERROR); assertThat(expected).hasMessageThat().contains(USE_ROUTING_MODULE); } @Test public void childInjectorCanBindRestrictedBindingWithPermit() { Injector parent = Guice.createInjector(new NetworkModule()); parent.createChildInjector(new RoutingModule()); } CreationException assertThatInjectorCreationFails(Module... modules) { return assertThrows(CreationException.class, () -> Guice.createInjector(modules)); } }