Skip to content

Commit

Permalink
Adds authorization support for preconfigured AAS Environment (#325)
Browse files Browse the repository at this point in the history
* Adds Authorization for Preconfigured AAS Environment

Signed-off-by: FriedJannik <[email protected]>
Co-authored-by: Aaron Zielstorff <[email protected]>
Co-authored-by: Mohammad Ghazanfar Ali Danish <[email protected]>

* Adds Dummy Component for Preconfigured Environment

* Adds Check if Authorization is enabled
- Embeds Preconfiguration to current Test Classes

Signed-off-by: FriedJannik <[email protected]>
Co-authored-by: Aaron Zielstorff <[email protected]>

* Removes wrong set config flag

* Updates realm/BaSyx-realm.json

* Revert "Updates realm/BaSyx-realm.json"

This reverts commit cd32396.

* Adapts BaSyx-realm.json

* Adapts changes according to code review

* Refactors Preconfiguration Authorization

* Adds missing Bean for test

* Re-adds necessary configuration for tests

* Adapts Preconfiguration Loader

* Adds missing Bean

Signed-off-by: FriedJannik <[email protected]>

* Adds Test for AccessTokenProviderFactory
Co-authored-by: Aaron Zielstorff <[email protected]>
Signed-off-by: FriedJannik <[email protected]>

* Adapts changes according to code review

---------

Signed-off-by: FriedJannik <[email protected]>
Co-authored-by: Aaron Zielstorff <[email protected]>
Co-authored-by: Mohammad Ghazanfar Ali Danish <[email protected]>
  • Loading branch information
3 people authored Jul 23, 2024
1 parent 4b3218a commit d9de413
Show file tree
Hide file tree
Showing 17 changed files with 336 additions and 38 deletions.
4 changes: 4 additions & 0 deletions basyx.aasenvironment/basyx.aasenvironment-client/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@
<artifactId>basyx.aasenvironment-feature-authorization</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.eclipse.digitaltwin.basyx</groupId>
<artifactId>basyx.aasenvironment-feature-authorization</artifactId>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,11 @@
import org.eclipse.digitaltwin.basyx.aasenvironment.AasEnvironmentFactory;
import org.eclipse.digitaltwin.basyx.aasenvironment.feature.AasEnvironmentFeature;
import org.eclipse.digitaltwin.basyx.aasenvironment.feature.DecoratedAasEnvironmentFactory;
import org.eclipse.digitaltwin.basyx.aasenvironment.preconfiguration.AasEnvironmentPreconfigurationLoader;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ResourceLoader;

/**
* Dummy configuration for testing the {@link AasEnvironment} authorization
Expand All @@ -51,4 +53,10 @@ public static AasEnvironment getAasEnvironment(AasEnvironmentFactory aasEnvironm
return new DecoratedAasEnvironmentFactory(aasEnvironmentFactory, features).create();
}

@Bean
@ConditionalOnMissingBean
public static AasEnvironmentPreconfigurationLoader getPreconfigurationLoaderForAasEnvironment(ResourceLoader resourceLoader, List<String> pathsToLoad) {
return new AasEnvironmentPreconfigurationLoader(resourceLoader, pathsToLoad);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,13 @@
package org.eclipse.digitaltwin.basyx.aasenvironment.preconfiguration;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.interfaces.RSAPublicKey;
import java.util.ArrayList;

import java.util.List;
import java.util.stream.Collectors;

Expand All @@ -36,10 +41,13 @@
import org.eclipse.digitaltwin.basyx.aasenvironment.AasEnvironment;
import org.eclipse.digitaltwin.basyx.aasenvironment.environmentloader.CompleteEnvironment;
import org.eclipse.digitaltwin.basyx.aasenvironment.environmentloader.CompleteEnvironment.EnvironmentType;
import org.eclipse.digitaltwin.basyx.authorization.CommonAuthorizationProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ResourceLoader;
import org.springframework.integration.file.RecursiveDirectoryScanner;
import org.springframework.stereotype.Component;
Expand All @@ -50,17 +58,15 @@
* @author fried, mateusmolina, despen, witt, jungjan, danish
*
*/
@Component
public class AasEnvironmentPreconfigurationLoader {

private Logger logger = LoggerFactory.getLogger(AasEnvironmentPreconfigurationLoader.class);

@Value("${basyx.environment:#{null}}")
private List<String> pathsToLoad;

private ResourceLoader resourceLoader;

@Autowired

public AasEnvironmentPreconfigurationLoader(ResourceLoader resourceLoader, List<String> pathsToLoad) {
this.resourceLoader = resourceLoader;
this.pathsToLoad = pathsToLoad;
Expand Down Expand Up @@ -134,4 +140,5 @@ private void logLoadingProcess(int current, int overall, String filename) {
logger.info("Loading AAS Environment ({}/{}) from file '{}'", current, overall, filename);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ basyx.feature.authorization.rbac.file = <Class path of the Rbac rules file if au
spring.security.oauth2.resourceserver.jwt.issuer-uri= <URI of the resource server>
```

If you want to use a preconfigured environment with authorization, you need to set the following options as well:
```
basyx.aasenvironment.authorization.preconfiguration.token-endpoint = <Endpoint to the KeyCloak Server>
basyx.aasenvironment.authorization.preconfiguration.grant-type = <Grant Type>
basyx.aasenvironment.authorization.preconfiguration.client-id = <ClientID>
basyx.aasenvironment.authorization.preconfiguration.client-secret= <Client Secret>
basyx.aasenvironment.authorization.preconfiguration.username = <Username>
basyx.aasenvironment.authorization.preconfiguration.password = <Password>
basyx.aasenvironment.authorization.preconfiguration.scopes = <Scopes>
```

Note: Only Role Based Access Control (RBAC) is supported as authorization type as of now, also Keycloak is the only Jwt token provider supported now and it is also a default provider.

To know more about the RBAC, please refer [Authorization Services Guide](https://www.keycloak.org/docs/latest/authorization_services/index.html)
Expand All @@ -26,6 +37,15 @@ basyx.feature.authorization.rbac.file = classpath:rbac_rules.json
spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/realms/BaSyx
```

And for preconfiguration (Grant Type would be CLIENT_CREDENTIALS in this case):

```
basyx.aasenvironment.authorization.preconfiguration.token-endpoint = http://localhost:9096/realms/BaSyx/protocol/openid-connect/token
basyx.aasenvironment.authorization.preconfiguration.grant-type = CLIENT_CREDENTIALS
basyx.aasenvironment.authorization.preconfiguration.client-id = workstation-1
basyx.aasenvironment.authorization.preconfiguration.client-secret = nY0mjyECF60DGzNmQUjL81XurSl8etom
```

## RBAC rule configuration

For configuring RBAC rules, all the rbac rules should be configured inside a json file, the rules are defined as below:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@
<groupId>org.eclipse.digitaltwin.basyx</groupId>
<artifactId>basyx.http</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.digitaltwin.basyx</groupId>
<artifactId>basyx.client</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.digitaltwin.basyx</groupId>
<artifactId>basyx.authorization</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/*******************************************************************************
* Copyright (C) 2024 the Eclipse BaSyx Authors
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the
* "Software"), to deal in the Software without restriction, including
* without limitation the rights to use, copy, modify, merge, publish,
* distribute, sublicense, and/or sell copies of the Software, and to
* permit persons to whom the Software is furnished to do so, subject to
* the following conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* SPDX-License-Identifier: MIT
******************************************************************************/

package org.eclipse.digitaltwin.basyx.aasenvironment.feature.authorization;

import org.apache.commons.io.IOUtils;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.eclipse.digitaltwin.aas4j.v3.dataformat.core.DeserializationException;
import org.eclipse.digitaltwin.basyx.aasenvironment.AasEnvironment;
import org.eclipse.digitaltwin.basyx.aasenvironment.preconfiguration.AasEnvironmentPreconfigurationLoader;
import org.eclipse.digitaltwin.basyx.authorization.jwt.JwtTokenDecoder;
import org.eclipse.digitaltwin.basyx.authorization.jwt.PublicKeyUtils;
import org.eclipse.digitaltwin.basyx.client.internal.authorization.AccessTokenProviderFactory;
import org.eclipse.digitaltwin.basyx.client.internal.authorization.TokenManager;
import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.AccessTokenProvider;
import org.eclipse.digitaltwin.basyx.client.internal.authorization.grant.GrantType;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.interfaces.RSAPublicKey;
import java.util.Collection;
import java.util.List;

public class AuthorizedAASEnvironmentPreconfigurationLoader extends AasEnvironmentPreconfigurationLoader {

@Value("${basyx.aasenvironment.authorization.preconfiguration.token-endpoint:#{null}}")
private String authenticationServerTokenEndpoint;

@Value("${basyx.aasenvironment.authorization.preconfiguration.client-id:#{null}}")
private String clientId;

@Value("${basyx.aasenvironment.authorization.preconfiguration.client-secret:#{null}}")
private String clientSecret;

@Value("${basyx.aasenvironment.authorization.preconfiguration.username:#{null}}")
private String username;

@Value("${basyx.aasenvironment.authorization.preconfiguration.password:#{null}}")
private String password;

@Value("${basyx.aasenvironment.authorization.preconfiguration.grant-type:#{null}}")
private String grantType;

@Value("${basyx.aasenvironment.authorization.preconfiguration.scopes:#{null}}")
private Collection<String> scopes;

@Value("${basyx.environment:#{null}}")
private String basyxEnvironment;

private AccessTokenProvider tokenProvider;

public AuthorizedAASEnvironmentPreconfigurationLoader(ResourceLoader resourceLoader, List<String> pathsToLoad) {
super(resourceLoader, pathsToLoad);
}

@Override
public void loadPreconfiguredEnvironments(AasEnvironment aasEnvironment) throws IOException, InvalidFormatException, DeserializationException {
if(isEnvironmentSet()) {
setUpTokenProvider();
configureSecurityContext();
}
super.loadPreconfiguredEnvironments(aasEnvironment);
SecurityContextHolder.clearContext();
}


private void setUpTokenProvider() {
AccessTokenProviderFactory factory = new AccessTokenProviderFactory(GrantType.valueOf(grantType),scopes);
factory.setClientCredentials(clientId, clientSecret);
factory.setPasswordCredentials(username, password);
this.tokenProvider = factory.create();
}

private void configureSecurityContext() throws FileNotFoundException, IOException {
TokenManager tokenManager = new TokenManager(authenticationServerTokenEndpoint, tokenProvider);
String adminToken = tokenManager.getAccessToken();

String modulus = getStringFromFile("authorization/modulus.txt");
String exponent = "AQAB";

RSAPublicKey rsaPublicKey = PublicKeyUtils.buildPublicKey(modulus, exponent);

Jwt jwt = JwtTokenDecoder.decodeJwt(adminToken, rsaPublicKey);

SecurityContextHolder.getContext().setAuthentication(new JwtAuthenticationToken(jwt));
}


private String getStringFromFile(String fileName) throws FileNotFoundException, IOException {
ClassPathResource classPathResource = new ClassPathResource(fileName);
InputStream in = classPathResource.getInputStream();
return IOUtils.toString(in, StandardCharsets.UTF_8.name());
}

private boolean isEnvironmentSet() {
return basyxEnvironment != null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import org.eclipse.digitaltwin.basyx.aasenvironment.AasEnvironment;
import org.eclipse.digitaltwin.basyx.aasenvironment.feature.authorization.rbac.AasEnvironmentTargetPermissionVerifier;
import org.eclipse.digitaltwin.basyx.aasenvironment.preconfiguration.AasEnvironmentPreconfigurationLoader;
import org.eclipse.digitaltwin.basyx.authorization.CommonAuthorizationProperties;
import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacPermissionResolver;
import org.eclipse.digitaltwin.basyx.authorization.rbac.RbacStorage;
Expand All @@ -36,6 +37,10 @@
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ResourceLoader;

import java.util.List;

/**
* Configuration for authorized {@link AasEnvironment}
Expand All @@ -57,4 +62,10 @@ public RbacPermissionResolver<AasEnvironmentTargetInformation> getAasEnvironment
return new SimpleRbacPermissionResolver<>(rbacStorage, roleProvider, targetPermissionVerifier);
}

@Bean
@Primary
public static AasEnvironmentPreconfigurationLoader getAuthorizedAasEnvironmentPreconfigurationLoader(ResourceLoader resourceLoader, List<String> pathsToLoad) {
return new AuthorizedAASEnvironmentPreconfigurationLoader(resourceLoader, pathsToLoad);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -97,17 +97,6 @@ public static void tearDown() {
appContext.close();
}

@Before
public void initializeRepositories() throws FileNotFoundException, IOException {
configureSecurityContext();

createDummyShellsOnRepository(TestAASEnvironmentSerialization.createDummyShells(), aasRepo);
createDummySubmodelsOnRepository(TestAASEnvironmentSerialization.createDummySubmodels(), submodelRepo);
createDummyConceptDescriptionsOnRepository(TestAASEnvironmentSerialization.createDummyConceptDescriptions(), conceptDescriptionRepo);

clearSecurityContext();
}

@After
public void reset() throws FileNotFoundException, IOException {
configureSecurityContext();
Expand All @@ -119,7 +108,12 @@ public void reset() throws FileNotFoundException, IOException {
assetAdministrationShells.stream().forEach(aas -> aasRepo.deleteAas(aas.getId()));
submodels.stream().forEach(sm -> submodelRepo.deleteSubmodel(sm.getId()));
conceptDescriptions.stream().forEach(cd -> conceptDescriptionRepo.deleteConceptDescription(cd.getId()));



createDummyShellsOnRepository(TestAASEnvironmentSerialization.createDummyShells(), aasRepo);
createDummySubmodelsOnRepository(TestAASEnvironmentSerialization.createDummySubmodels(), submodelRepo);
createDummyConceptDescriptionsOnRepository(TestAASEnvironmentSerialization.createDummyConceptDescriptions(), conceptDescriptionRepo);

clearSecurityContext();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,7 @@
import org.eclipse.digitaltwin.basyx.core.pagination.PaginationInfo;
import org.eclipse.digitaltwin.basyx.http.serialization.BaSyxHttpTestUtils;
import org.eclipse.digitaltwin.basyx.submodelrepository.SubmodelRepository;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.*;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.http.HttpStatus;
Expand All @@ -80,8 +78,8 @@ public class TestAuthorizedAasEnvironmentUpload {

private static String healthEndpointUrl = "http://127.0.0.1:8081/actuator/health";

@Before
public void setUp() throws FileNotFoundException, IOException {
@BeforeClass
public static void setUp() throws FileNotFoundException, IOException {
tokenProvider = new AccessTokenProvider(authenticaltionServerTokenEndpoint, clientId);

appContext = new SpringApplication(DummyAasEnvironmentComponent.class).run(new String[] {});
Expand All @@ -91,7 +89,7 @@ public void setUp() throws FileNotFoundException, IOException {
conceptDescriptionRepo = appContext.getBean(ConceptDescriptionRepository.class);
}

@After
@Before
public void reset() throws FileNotFoundException, IOException {

configureSecurityContext();
Expand All @@ -105,7 +103,10 @@ public void reset() throws FileNotFoundException, IOException {
conceptDescriptions.stream().forEach(cd -> conceptDescriptionRepo.deleteConceptDescription(cd.getId()));

clearSecurityContext();

}

@AfterClass
public static void shutDown(){
appContext.close();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ basyx.backend = InMemory
# To load from Classpath (src/main/resources) use classpath:path/to/file.end
# To load from Filesystem ( On your local machine ) use the prefix file:
#
# basyx.environment = classpath:testEnvironment.json,classpath:testEnvironment.xml,file:C:\\Users\\Administrator\\Documents\\01_Festo.aasx,file:/var/www/html/01_Submodel.json
basyx.environment = classpath:testEnvironment.aasx
#

####################################################################################
Expand All @@ -39,6 +39,10 @@ basyx.feature.authorization.jwtBearerTokenProvider = keycloak
basyx.feature.authorization.rbac.file = classpath:rbac_rules.json
spring.security.oauth2.resourceserver.jwt.issuer-uri= http://localhost:9096/realms/BaSyx

basyx.aasenvironment.authorization.preconfiguration.token-endpoint=http://localhost:9096/realms/BaSyx/protocol/openid-connect/token
basyx.aasenvironment.authorization.preconfiguration.grant-type = CLIENT_CREDENTIALS
basyx.aasenvironment.authorization.preconfiguration.client-id=workstation-1
basyx.aasenvironment.authorization.preconfiguration.client-secret=nY0mjyECF60DGzNmQUjL81XurSl8etom
####################################################################################
# Operation Delegation
####################################################################################
Expand Down
Loading

0 comments on commit d9de413

Please sign in to comment.