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
- JACC disabled in legacy security subsystem
<subsystem xmlns="urn:jboss:domain:security:2.0">
<security-management initialize-jacc="false"/>
...
- JACC policy activated in elytron subsystem
<subsystem xmlns="urn:wildfly:elytron:13.0" ...>
<policy name="jacc">
<jacc-policy/>
</policy>
...
- 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"/>
...
- 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.
- 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;
}
}
- 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();
}
- 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();
}
}
- 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>
...