Using ACLs

This section describes the practical aspects of configuring Access Control Lists (ACLs) with the Java SDK, if you are not sure what ACLs are or how they work, see Understanding ACLs first.

Configuring ACLs

Kalix ACLs consist of two lists of principal matchers. One to allow to invoke a method, and the other to deny to invoke a method. For a request to be allowed, at least one principal associated with a request must be matched by at least one principal matcher in the allow list, and no principals associated with the request may match any principal matchers in the deny list.

Here is an example ACL on a method:

@PostMapping
@Acl(allow = @Acl.Matcher(service = "*"),
        deny = @Acl.Matcher(service = "service-b"))
public Effect<String> createUser(@RequestBody CreateUser create) {
    //...
}

The above ACL allows traffic to all services except the service called service-b.

To allow all traffic:

@Acl(allow = @Acl.Matcher(principal = Acl.Principal.ALL))

To allow only traffic from the internet:

@Acl(allow = @Acl.Matcher(principal = Acl.Principal.INTERNET))

To allow traffic from service-a and service-b:

@Acl(allow = {
        @Acl.Matcher(service = "service-a"),
        @Acl.Matcher(service = "service-b")})

To block all traffic, an ACL with no allows can be configured:

@Acl(allow = {})

Sharing ACLs between methods

The above examples show how to configure an ACL on a method. ACLs can also be shared between all methods on a component by specifying them on the class, at type level:

@Acl(allow = @Acl.Matcher(service = "service-a"))
public class EmployeeAction extends Action {
    //...
}

The component’s ACL can be overridden by individual methods, by specifying the ACL on the method. Note that an ACL defined on a method completely overrides an ACL defined on a component. It does not add to it. So for example, in the following component:

@Acl(allow = @Acl.Matcher(service = "service-a"))
public class EmployeeAction extends Action {
    //...

    @PostMapping
    @Acl(allow = @Acl.Matcher(service = "service-b"))
    public Effect<String> createEmployee(@RequestBody CreateEmployee create) {
        //...
    }
}

The createEmployee method will allow calls by service-b, but not by service-a.

Configuring the default policy

The default policy can be configured by specifying a project level annotation in the Main, for example, to set a default policy of allowing all local services:

@Acl(allow = @Acl.Matcher(service = "*"))
public class Main {

An ACL declared at the project level is used as the default for all services that don’t declare their own explicit ACL.

Default ACL in project templates

If no ACLs is defined at all in a Kalix service, Kalix will allow requests from both other services and the internet to all components of a Kalix service.

The Maven archetype include a less permissive ACL for the entire service, to not accidentally make services available to the public internet, just like the one described in the next section.

Customizing the deny code

When a request is denied, by default a 403 Forbbiden, is sent. The code that is returned when a request is denied can be customised using the deny_code property.

For example, to make Kalix reply with 404, Not Found:

@PostMapping
@Acl(allow = @Acl.Matcher(service = "*"), denyCode = Acl.DenyStatusCode.NOT_FOUND)
public Effect<String> updateUser(@RequestBody CreateUser create) {
    //...
}

Deny codes, if not specified on an ACL, are inherited from the service, or the default, so updating the deny_code in the default ACL policy will set it for all methods:

@Acl(denyCode = Acl.DenyStatusCode.NOT_FOUND)
public class UserAction extends Action {
    //...
}

ACLs on eventing methods

Any method with an @Subscribe annotation on it will not automatically inherit either the default or its component’s ACL, rather, all outside communication will be blocked, since it’s assumed that a method that subscribes to an event stream must only be intended to be invoked in response to events on that stream. This can be overridden by explicitly defining an ACL on that method:

@Subscribe.ValueEntity(Counter.class)
@PostMapping("/counter")
@Acl(allow = @Acl.Matcher(service = "*"))
public Effect<Done> changes(@RequestBody CounterState counterState) {
 //...
}

Backoffice and self invocations

Invocations of methods from the same service, or from the backoffice using the kalix service proxy command, are always permitted, regardless of what ACLs are defined on them.

Local development with ACLs

When testing or running in development, by default, all calls to your service will be accessible as ACLs are disabled by default.

Enabling ACLs in local development

When running a service during local development, it may be useful to enable ACL check. This can be done by setting the ACL_ENABLED environment variable to true in your docker-compose.yml file:

kalix-runtime:
  image: gcr.io/kalix-public/kalix-runtime:latest
  ports:
    - "9000:9000"
  environment:
    ACL_ENABLED: 'true'

Once you enable ACL, calls to the Runtime are treated as if they are coming from the Internet.

If the ACL is configured to only allow calls from other Kalix services, you will have to impersonate a local service. This can be done by setting the Impersonate-Kalix-Service header on the requests you make.

Service identification in local development

If running multiple services in local development, you may want to run with ACLs enabled to verify that they work for cross-service communication. In order to do this, you need to ensure that when services communicate with each other, they are able to identify themselves to one another. This can be done by setting the SERVICE_NAME environment variable in your docker-compose.yml file:

kalix-runtime:
  image: gcr.io/kalix-public/kalix-runtime:latest
  ports:
    - "9000:9000"
  environment:
    SERVICE_NAME: my-service-name
    ACL_ENABLED: 'true'

Note that in local development, the services don’t actually authenticate with each other, they only pass their identity in a header. It is assumed in local development that a client can be trusted to set that header correctly.

Programmatically accessing principals

The current principal associated with a request can be accessed by reading metadata headers. If the request came from another service, the _kalix-src-svc header will be set to the name of the service that made the request. Kalix guarantees that this header will only be present from an authenticated principal, it can’t be spoofed.

For internet, self and backoffice requests, the _kalix-src header will be set to internet, self and backoffice respectively. Backoffice requests are requests that have been made using the kalix service proxy command, they are authenticated and authorized to ensure only developers of your project can make them.

Inspecting the principal inside a service

Checking the ACLs are in general done for you by Kalix, however in some cases programmatic access to the principal of a call can be useful.

Accessing the principal of a call inside a service is possible through the request metadata Metadata.principals(). The Metadata for a call is available through the context (actionContext, commandContext) of the component.

ACLs when running unit tests

In the generated unit test testkits, the ACLs are ignored.

ACLs when running integration tests

When running integration tests, ACLs are disabled by default but can be explicitly enabled per test by adding a @Configuration class with a @Bean creating a kalix.javasdk.testkit.KalixTestKit.Settings with ACL enabled like this:

@Configuration
public class TestKitConfiguration {
  @Bean
  public KalixTestKit.Settings settings() {
    return KalixTestKit.Settings.DEFAULT
        .withAclEnabled() (1)
  }
}
1 Using the DEFAULT Settings and enabling ACL.

Once this @Configuration is available you can @Import it to your integration test like the following:

@SpringBootTest(classes = Main.class)
@Import(TestKitConfiguration.class)
public class CounterIntegrationTest extends KalixIntegrationTestKitSupport { (1)

}
1 Importing TestkitConfig as @Configuration.

For integration tests that call other services that have ACLs limiting access to specific service names Settings.withServiceName allows specifying what the service identifies itself as to other services.