Annotation @RunAs doesn't work on WildFly with Java EE Security API (JSR 375) enabled

Viewed 304

I'm testing Java EE Security API (JSR 375) on WildFly 23 and annotation @RunAs is not working as i expected.
Inside EJB with @RunAs annotation method javax.security.enterprise.SecurityContext::isCallerInRole always returns false.

I set up the server according to the article .
As result i have the following changes in standalone.xml

  1. JACC disabled in legacy security subsystem
<subsystem xmlns="urn:jboss:domain:security:2.0">
    <security-management initialize-jacc="false"/>
    ...
  1. JACC policy activated in elytron subsystem
<subsystem xmlns="urn:wildfly:elytron:13.0" ...>
    <policy name="jacc">
        <jacc-policy/>
    </policy>    
    ...
  1. New security domain in ejb subsystem. Additionally i've disabled option default-missing-method-permissions-deny-access to making EJBs available for all roles.
<subsystem xmlns="urn:jboss:domain:ejb3:9.0">
    <application-security-domains>
        <application-security-domain name="other" security-domain="ApplicationDomain" enable-jacc="true"/>
    </application-security-domains>
    ...
    <default-missing-method-permissions-deny-access value="false"/>
    ...
  1. New security domain in undertow subsystem
<subsystem xmlns="urn:jboss:domain:undertow:12.0" ...>
    <application-security-domains>
        <application-security-domain name="other" security-domain="ApplicationDomain" enable-jacc="true" integrated-jaspi="false"/>
    </application-security-domains>
    ...

I wrote a simple application github.

  1. IdentityStore
@ApplicationScoped
@CustomFormAuthenticationMechanismDefinition(loginToContinue = @LoginToContinue(loginPage = "/login.xhtml", errorPage = "/login.xhtml"))
public class SimpleIdentityStore implements IdentityStore {
    @Override
    public CredentialValidationResult validate(Credential credential) {
        UsernamePasswordCredential usernamePasswordCredential = (UsernamePasswordCredential) credential;
        if (usernamePasswordCredential.compareTo("z", "z")) {
            return new CredentialValidationResult("z", Collections.singleton("Admin"));
        }
        return CredentialValidationResult.INVALID_RESULT;
    }
}
  1. Backing bean for login page
@Named
@RequestScoped
public class LoginMB {
    @Inject
    private SecurityContext securityContext;

    @Inject
    private EjbWithoutRunAs ejbWithoutRunAs;
    @Inject
    private EjbRunAsSystem ejbRunAsSystem;

    public void login(ActionEvent event) {
        ExternalContext externalContext = FacesContext.getCurrentInstance()
                .getExternalContext();

        HttpServletRequest request = (HttpServletRequest) externalContext.getRequest();
        HttpServletResponse response = (HttpServletResponse) externalContext.getResponse();

        Credential credential = new UsernamePasswordCredential("z", new Password("z"));
        AuthenticationStatus status = securityContext.authenticate(request, response,
                AuthenticationParameters.withParams().credential(credential));
    }

    public void test(ActionEvent event) {
        System.out.println("cdi isAdmin: " + securityContext.isCallerInRole("Admin"));
        System.out.println("cdi isSystem: " + securityContext.isCallerInRole("System"));
        System.out.println();

        ejbWithoutRunAs.test();
        ejbRunAsSystem.test();
        ejbRunAsSystem.testAnotherEjb();
    }
  1. EJB without @RunAs annotation
@LocalBean
@Stateless
public class EjbWithoutRunAs {
    @Inject
    private SecurityContext securityContext;

    public void test() {
        System.out.println("ejbWithoutRunAs.test isSystem: " + securityContext.isCallerInRole("System"));
        System.out.println("ejbWithoutRunAs.test isAdmin: " + securityContext.isCallerInRole("Admin"));
        System.out.println();
    }

    public void testFromRunAsSystem() {
        System.out.println("ejbWithoutRunAs.testFromRunAsSystem isSystem: " + securityContext.isCallerInRole("System"));
        System.out.println("ejbWithoutRunAs.testFromRunAsSystem isAdmin: " + securityContext.isCallerInRole("Admin"));
        System.out.println();
    }
}
  1. EJB with @RunAs annotation
@LocalBean
@Stateless
@RunAs("System")
public class EjbRunAsSystem {
    @Inject
    private SecurityContext securityContext;
    @Inject
    private EjbWithoutRunAs ejbWithoutRunAs;

    public void test() {
        System.out.println("ejbRunAsSystem.test isSystem: " + securityContext.isCallerInRole("System"));
        System.out.println("ejbRunAsSystem.test isAdmin: " + securityContext.isCallerInRole("Admin"));
        System.out.println();
    }

    public void testAnotherEjb() {
        ejbWithoutRunAs.testFromRunAsSystem();
    }
}

After start application i call method test from login page. And i see following output

cdi isAdmin: false
cdi isSystem: false

ejbWithoutRunAs.test isSystem: false
ejbWithoutRunAs.test isAdmin: false

ejbRunAsSystem.test isSystem: false
ejbRunAsSystem.test isAdmin: false

ejbWithoutRunAs.testFromRunAsSystem isSystem: false
ejbWithoutRunAs.testFromRunAsSystem isAdmin: false

When i expected

...
ejbRunAsSystem.test isSystem: true
...
ejbWithoutRunAs.testFromRunAsSystem isSystem: true
...

After login i call method test again and see

cdi isAdmin: true
cdi isSystem: false

ejbWithoutRunAs.test isSystem: false
ejbWithoutRunAs.test isAdmin: true

ejbRunAsSystem.test isSystem: false
ejbRunAsSystem.test isAdmin: false

ejbWithoutRunAs.testFromRunAsSystem isSystem: false
ejbWithoutRunAs.testFromRunAsSystem isAdmin: false

So method isCallerInRole always return false in EJB annotated with @RunAs.

After debugging i found workaround. I had to write custom JACC Policy. But maybe anyone know a way to fix this without a custom module? Maybe i missed something in configuration? Or it's a bug in WildFly?

Workaround
I found in org.wildfly.security.authz.jacc.JaccDelegatingPolicy method extractRolesFromCurrentIdentity. In this method roles are obtained using identity.getRoles()

    private void extractRolesFromCurrentIdentity(Set<String> roles) throws PolicyContextException, ClassNotFoundException {
        SecurityIdentity identity = getCurrentSecurityIdentity();

        if (identity != null) {
            Roles identityRoles = identity.getRoles();

But in identity the role from @RunAs is in field roleMappers. So the role can be obtained only with method identity.getRoles("ejb")

So i wrote a module for WildFly with only one class

public class MyJaccDelegatingPolicy extends JaccDelegatingPolicy {
    @Override
    public boolean implies(ProtectionDomain domain, Permission permission) {
        if (permission instanceof EJBRoleRefPermission) {
            SecurityDomain currentSecurityDomain = SecurityDomain.getCurrent();
            if (currentSecurityDomain != null) {
                SecurityIdentity currentSecurityIdentity = currentSecurityDomain.getCurrentSecurityIdentity();
                if (currentSecurityIdentity != null) {
                    String requiredRole = permission.getActions();
                    Roles runAsRoles = currentSecurityIdentity.getRoles("ejb");
                    return runAsRoles.contains(requiredRole);
                }
            }
        }
        return super.implies(domain, permission);
    }
}

module.xml

<?xml version='1.0' encoding='UTF-8'?>
<module xmlns="urn:jboss:module:1.1" name="org.example.my-wildfly-jacc-policy">
    <resources>
        <resource-root path="my-wildfly-jacc-policy-1.0-SNAPSHOT.jar"/>
    </resources>
    <dependencies>
        <module name="javax.api"/>
        <module name="javax.security.jacc.api"/>
        <module name="org.wildfly.security.elytron"/>
    </dependencies>
</module>

And registered module in standalone.xml

<subsystem xmlns="urn:wildfly:elytron:13.0" ...>
    <policy name="jacc">
        <jacc-policy policy="org.example.MyJaccDelegatingPolicy" module="org.example.my-wildfly-jacc-policy"/>
    </policy>
    ...
0 Answers
Related