diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 1436fe83..82d3b4a6 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -35,11 +35,10 @@ jobs: ${{ runner.os }}-gradle- - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@v1 - - name: Build with Gradle timeout-minutes: 15 run: | - ./gradlew build --scan + ./gradlew build -x test --scan working-directory: ${{ matrix.module }} build-frontend: @@ -63,4 +62,4 @@ jobs: working-directory: sep490-frontend - run: npm run build timeout-minutes: 5 - working-directory: sep490-frontend \ No newline at end of file + working-directory: sep490-frontend diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8ae3273c..27fa9a23 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,7 +29,6 @@ jobs: ${{ runner.os }}-gradle- - name: Validate Gradle wrapper uses: gradle/wrapper-validation-action@v1 - - name: Build with Gradle timeout-minutes: 15 run: | diff --git a/sep490-commons/api/src/main/java/green_buildings/commons/api/SagaManager.java b/sep490-commons/api/src/main/java/green_buildings/commons/api/SagaManager.java index 3a653d3a..59e6f66a 100644 --- a/sep490-commons/api/src/main/java/green_buildings/commons/api/SagaManager.java +++ b/sep490-commons/api/src/main/java/green_buildings/commons/api/SagaManager.java @@ -8,6 +8,8 @@ @Getter public abstract class SagaManager { + protected static final int TRANSACTION_TIMEOUT = 10000; + private final ConcurrentHashMap> pendingSagaResponses = new ConcurrentHashMap<>(); /** diff --git a/sep490-commons/springfw-impl/src/main/java/commons/springfw/impl/securities/JwtAuthenticationConverter.java b/sep490-commons/springfw-impl/src/main/java/commons/springfw/impl/securities/JwtAuthenticationConverter.java index 2d41bc59..86b467f7 100644 --- a/sep490-commons/springfw-impl/src/main/java/commons/springfw/impl/securities/JwtAuthenticationConverter.java +++ b/sep490-commons/springfw-impl/src/main/java/commons/springfw/impl/securities/JwtAuthenticationConverter.java @@ -1,12 +1,12 @@ package commons.springfw.impl.securities; import com.nimbusds.jose.shaded.gson.internal.LinkedTreeMap; +import green_buildings.commons.api.dto.auth.BuildingPermissionDTO; +import green_buildings.commons.api.security.BuildingPermissionRole; import org.apache.commons.lang3.StringUtils; import org.springframework.core.convert.converter.Converter; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; -import green_buildings.commons.api.dto.auth.BuildingPermissionDTO; -import green_buildings.commons.api.security.BuildingPermissionRole; import java.util.Collections; import java.util.List; @@ -36,7 +36,7 @@ public JwtAuthenticationTokenDecorator convert(Jwt source) { .collect(Collectors.toUnmodifiableSet()); } - var buildingPermissionsClaim = Objects.requireNonNull(source.getClaims().get("permissions")); + var buildingPermissionsClaim = Optional.ofNullable(source.getClaims().get("permissions")).orElse(Collections.emptyList()); List buildingPermissions = Collections.emptyList(); if (buildingPermissionsClaim instanceof List buildingPermissionsList) { buildingPermissions = buildingPermissionsList diff --git a/sep490-enterprise/build.gradle b/sep490-enterprise/build.gradle index 0d86846d..8cd75fbb 100644 --- a/sep490-enterprise/build.gradle +++ b/sep490-enterprise/build.gradle @@ -31,7 +31,8 @@ dependencies { testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' - testRuntimeOnly 'com.h2database:h2' + testImplementation 'org.testcontainers:postgresql' + testImplementation 'io.rest-assured:rest-assured' // Ops implementation 'org.springframework.boot:spring-boot-starter-actuator' diff --git a/sep490-enterprise/src/main/java/enterprise/dtos/BuildingDTO.java b/sep490-enterprise/src/main/java/enterprise/dtos/BuildingDTO.java index cb6d3294..fc19961d 100644 --- a/sep490-enterprise/src/main/java/enterprise/dtos/BuildingDTO.java +++ b/sep490-enterprise/src/main/java/enterprise/dtos/BuildingDTO.java @@ -1,11 +1,20 @@ package enterprise.dtos; import green_buildings.commons.api.BaseDTO; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import lombok.Builder; +import java.math.BigDecimal; import java.util.UUID; +@Builder public record BuildingDTO( UUID id, - int version + int version, + @NotBlank String name, + @Min(1) int floors, + @DecimalMin(value = "0.0", inclusive = true) BigDecimal squareMeters ) implements BaseDTO { } diff --git a/sep490-enterprise/src/main/java/enterprise/entities/BuildingEntity.java b/sep490-enterprise/src/main/java/enterprise/entities/BuildingEntity.java index 4c6410e5..5fa5d56a 100644 --- a/sep490-enterprise/src/main/java/enterprise/entities/BuildingEntity.java +++ b/sep490-enterprise/src/main/java/enterprise/entities/BuildingEntity.java @@ -1,16 +1,22 @@ package enterprise.entities; import commons.springfw.impl.entities.AbstractAuditableEntity; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import java.math.BigDecimal; + @Entity @Table(name = "buildings") @Getter @@ -23,4 +29,15 @@ public class BuildingEntity extends AbstractAuditableEntity { @JoinColumn(name = "enterprise_id", nullable = false) private EnterpriseEntity enterprise; + @NotBlank + @Column(name = "name", nullable = false) + private String name; + + @Min(value = 1) + @Column(name = "floors", nullable = false) + private int floors; + + @DecimalMin(value = "0.0", inclusive = true) + @Column(name = "square_meters", nullable = false, precision = 15, scale = 3) + private BigDecimal squareMeters; } diff --git a/sep490-enterprise/src/main/java/enterprise/mappers/BuildingMapper.java b/sep490-enterprise/src/main/java/enterprise/mappers/BuildingMapper.java new file mode 100644 index 00000000..1b611faa --- /dev/null +++ b/sep490-enterprise/src/main/java/enterprise/mappers/BuildingMapper.java @@ -0,0 +1,21 @@ +package enterprise.mappers; + +import enterprise.dtos.BuildingDTO; +import enterprise.entities.BuildingEntity; +import org.mapstruct.BeanMapping; +import org.mapstruct.Mapper; +import org.mapstruct.MappingConstants; +import org.mapstruct.MappingTarget; +import org.mapstruct.NullValuePropertyMappingStrategy; +import org.mapstruct.ReportingPolicy; + +@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING) +public interface BuildingMapper { + BuildingEntity toEntity(BuildingDTO buildingDTO); + + BuildingDTO toDto(BuildingEntity buildingEntity); + + @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE) + BuildingEntity partialUpdate(BuildingDTO buildingDTO, @MappingTarget + BuildingEntity buildingEntity); +} \ No newline at end of file diff --git a/sep490-enterprise/src/main/java/enterprise/repositories/BuildingRepository.java b/sep490-enterprise/src/main/java/enterprise/repositories/BuildingRepository.java index a6db0cf3..37de5d04 100644 --- a/sep490-enterprise/src/main/java/enterprise/repositories/BuildingRepository.java +++ b/sep490-enterprise/src/main/java/enterprise/repositories/BuildingRepository.java @@ -2,6 +2,13 @@ import commons.springfw.impl.repositories.AbstractBaseRepository; import enterprise.entities.BuildingEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.UUID; public interface BuildingRepository extends AbstractBaseRepository { + + Page findByEnterpriseId(UUID enterpriseId, Pageable pageable); + } diff --git a/sep490-enterprise/src/main/java/enterprise/rest/BuildingController.java b/sep490-enterprise/src/main/java/enterprise/rest/BuildingController.java index 6d3b2a18..a291f81c 100644 --- a/sep490-enterprise/src/main/java/enterprise/rest/BuildingController.java +++ b/sep490-enterprise/src/main/java/enterprise/rest/BuildingController.java @@ -1,17 +1,25 @@ package enterprise.rest; +import commons.springfw.impl.mappers.CommonMapper; import commons.springfw.impl.securities.UserContextData; import enterprise.dtos.BuildingDTO; +import enterprise.mappers.BuildingMapper; +import enterprise.services.BuildingService; +import enterprise.services.EnterpriseService; +import green_buildings.commons.api.dto.SearchCriteriaDTO; +import green_buildings.commons.api.dto.SearchResultDTO; +import green_buildings.commons.api.security.UserRole; import jakarta.annotation.security.RolesAllowed; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import green_buildings.commons.api.security.UserRole; -import java.util.List; +import java.util.Objects; @RestController @RequestMapping("/buildings") @@ -22,10 +30,30 @@ }) public class BuildingController { - @GetMapping + private final BuildingMapper buildingMapper; + private final BuildingService buildingService; + private final EnterpriseService enterpriseService; + + @PostMapping("/search") + public ResponseEntity> getEnterpriseBuildings(@RequestBody SearchCriteriaDTO searchCriteria, + @AuthenticationPrincipal UserContextData userContextData) { + var enterpriseIdFromContext = Objects.requireNonNull(userContextData.getEnterpriseId()); + var pageable = CommonMapper.toPageable(searchCriteria.page(), searchCriteria.sort()); + var searchResults = buildingService.getEnterpriseBuildings(enterpriseIdFromContext, pageable); + var searchResultDTO = CommonMapper.toSearchResultDTO(searchResults, buildingMapper::toDto); + return ResponseEntity.ok(searchResultDTO); + } + + @PostMapping @RolesAllowed(UserRole.RoleNameConstant.ENTERPRISE_OWNER) - public ResponseEntity> getEnterpriseBuildings(@AuthenticationPrincipal UserContextData userContextData) { - return ResponseEntity.ok().build(); + public ResponseEntity createBuilding(@RequestBody BuildingDTO buildingDTO, + @AuthenticationPrincipal UserContextData userContextData) { + var enterpriseIdFromContext = Objects.requireNonNull(userContextData.getEnterpriseId()); + var enterprise = enterpriseService.getById(enterpriseIdFromContext); + var building = buildingMapper.toEntity(buildingDTO); + building.setEnterprise(enterprise); + var createdBuilding = buildingService.createBuilding(building); + return ResponseEntity.status(HttpStatus.CREATED).body(buildingMapper.toDto(createdBuilding)); } } diff --git a/sep490-enterprise/src/main/java/enterprise/rest/EnterpriseController.java b/sep490-enterprise/src/main/java/enterprise/rest/EnterpriseController.java index 17b34def..1995fd9f 100644 --- a/sep490-enterprise/src/main/java/enterprise/rest/EnterpriseController.java +++ b/sep490-enterprise/src/main/java/enterprise/rest/EnterpriseController.java @@ -1,26 +1,13 @@ package enterprise.rest; -import commons.springfw.impl.securities.UserContextData; -import enterprise.dtos.EnterpriseDTO; import enterprise.mappers.EnterpriseMapper; import enterprise.services.EnterpriseService; -import green_buildings.commons.api.exceptions.BusinessException; import green_buildings.commons.api.security.UserRole; import jakarta.annotation.security.RolesAllowed; import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; -import java.util.Collections; -import java.util.Objects; -import java.util.UUID; - @RestController @RequestMapping("/enterprise") @RequiredArgsConstructor @@ -32,18 +19,4 @@ public class EnterpriseController { private final EnterpriseService service; private final EnterpriseMapper mapper; - @PostMapping - public ResponseEntity createEnterprise(@AuthenticationPrincipal UserContextData userContextData, - @RequestBody EnterpriseDTO enterpriseDTO) { - if (Objects.nonNull(userContextData.getEnterpriseId())) { - throw new BusinessException(StringUtils.EMPTY, "error.user.already.belongs.to.enterprise", Collections.emptyList()); - } - - var enterprise = mapper.createEnterprise(enterpriseDTO); - - return ResponseEntity - .status(HttpStatus.CREATED) - .body(service.createEnterprise(enterprise)); - } - } diff --git a/sep490-enterprise/src/main/java/enterprise/services/BuildingService.java b/sep490-enterprise/src/main/java/enterprise/services/BuildingService.java new file mode 100644 index 00000000..eda3917f --- /dev/null +++ b/sep490-enterprise/src/main/java/enterprise/services/BuildingService.java @@ -0,0 +1,14 @@ +package enterprise.services; + +import enterprise.entities.BuildingEntity; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.UUID; + +public interface BuildingService { + + BuildingEntity createBuilding(BuildingEntity building); + + Page getEnterpriseBuildings(UUID enterpriseId, Pageable page); +} diff --git a/sep490-enterprise/src/main/java/enterprise/services/EnterpriseService.java b/sep490-enterprise/src/main/java/enterprise/services/EnterpriseService.java index a32f866f..3d278a56 100644 --- a/sep490-enterprise/src/main/java/enterprise/services/EnterpriseService.java +++ b/sep490-enterprise/src/main/java/enterprise/services/EnterpriseService.java @@ -8,4 +8,6 @@ public interface EnterpriseService { UUID createEnterprise(EnterpriseEntity enterprise); + EnterpriseEntity getById(UUID id); + } diff --git a/sep490-enterprise/src/main/java/enterprise/services/impl/BuildingServiceImpl.java b/sep490-enterprise/src/main/java/enterprise/services/impl/BuildingServiceImpl.java new file mode 100644 index 00000000..874859d3 --- /dev/null +++ b/sep490-enterprise/src/main/java/enterprise/services/impl/BuildingServiceImpl.java @@ -0,0 +1,30 @@ +package enterprise.services.impl; + +import enterprise.entities.BuildingEntity; +import enterprise.repositories.BuildingRepository; +import enterprise.services.BuildingService; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +@Transactional(rollbackOn = Throwable.class) +@RequiredArgsConstructor +public class BuildingServiceImpl implements BuildingService { + + private final BuildingRepository buildingRepository; + + @Override + public BuildingEntity createBuilding(BuildingEntity building) { + return buildingRepository.save(building); + } + + @Override + public Page getEnterpriseBuildings(UUID enterpriseId, Pageable page) { + return buildingRepository.findByEnterpriseId(enterpriseId, page); + } +} diff --git a/sep490-enterprise/src/main/java/enterprise/services/impl/EnterpriseServiceImpl.java b/sep490-enterprise/src/main/java/enterprise/services/impl/EnterpriseServiceImpl.java index 7aebca96..51f4853e 100644 --- a/sep490-enterprise/src/main/java/enterprise/services/impl/EnterpriseServiceImpl.java +++ b/sep490-enterprise/src/main/java/enterprise/services/impl/EnterpriseServiceImpl.java @@ -20,4 +20,9 @@ public class EnterpriseServiceImpl implements EnterpriseService { public UUID createEnterprise(EnterpriseEntity enterprise) { return repository.save(enterprise).getId(); } + + @Override + public EnterpriseEntity getById(UUID id) { + return repository.findById(id).orElseThrow(); + } } diff --git a/sep490-enterprise/src/main/resources/application.yml b/sep490-enterprise/src/main/resources/application.yml index 34169f6d..237cdaf9 100644 --- a/sep490-enterprise/src/main/resources/application.yml +++ b/sep490-enterprise/src/main/resources/application.yml @@ -1,5 +1,5 @@ exchangerate-api: - key: ${EXCHANGERATE_API_KEY} + key: ${EXCHANGERATE_API_KEY:secret} url: https://v6.exchangerate-api.com/v6/ spring: diff --git a/sep490-enterprise/src/main/resources/db/migration/V0.0.1.3__ManageBuildings.sql b/sep490-enterprise/src/main/resources/db/migration/V0.0.1.3__ManageBuildings.sql new file mode 100644 index 00000000..892654c6 --- /dev/null +++ b/sep490-enterprise/src/main/resources/db/migration/V0.0.1.3__ManageBuildings.sql @@ -0,0 +1,4 @@ +ALTER TABLE buildings + ADD COLUMN name VARCHAR(255) NOT NULL, + ADD COLUMN floors INTEGER NOT NULL, + ADD COLUMN square_meters NUMERIC(15, 3) NOT NULL; \ No newline at end of file diff --git a/sep490-enterprise/src/test/java/enterprise/EnterpriseApplication.java b/sep490-enterprise/src/test/java/enterprise/EnterpriseApplication.java deleted file mode 100644 index 84f675d0..00000000 --- a/sep490-enterprise/src/test/java/enterprise/EnterpriseApplication.java +++ /dev/null @@ -1,11 +0,0 @@ -package enterprise; - -import org.springframework.boot.SpringApplication; - -public class EnterpriseApplication { - - public static void main(String[] args) { - SpringApplication.from(EnterpriseApplication::main).run(args); - } - -} diff --git a/sep490-enterprise/src/test/java/enterprise/TestcontainersConfigs.java b/sep490-enterprise/src/test/java/enterprise/TestcontainersConfigs.java new file mode 100644 index 00000000..a3a282cc --- /dev/null +++ b/sep490-enterprise/src/test/java/enterprise/TestcontainersConfigs.java @@ -0,0 +1,87 @@ +package enterprise; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.apache.commons.text.StringSubstitutor; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.wait.strategy.Wait; + +import java.util.Map; +import java.util.function.Supplier; + +public abstract class TestcontainersConfigs { + + static PostgreSQLContainer postgres = new PostgreSQLContainer<>("postgres:16.4-alpine"); + static GenericContainer idP = new GenericContainer<>("thongdh3401/keycloak:24.0.5"); + + @BeforeAll + static void beforeAll() { + postgres.start(); + idP.withExposedPorts(8180) + .waitingFor(Wait.forHttp("/realms/greenbuildings/.well-known/openid-configuration") + .forStatusCode(200) + .withReadTimeout(java.time.Duration.ofMinutes(5)) + .withStartupTimeout(java.time.Duration.ofMinutes(5)) + ) + .start(); + } + + @AfterAll + static void afterAll() { + postgres.stop(); + idP.stop(); + } + + @DynamicPropertySource + static void configureProperties(DynamicPropertyRegistry registry) { + registry.add("spring.datasource.url", postgres::getJdbcUrl); + registry.add("spring.datasource.username", postgres::getUsername); + registry.add("spring.datasource.password", postgres::getPassword); + Supplier callable = () -> StringSubstitutor.replace( + "http://localhost:${mappedPort}/realms/greenbuildings", Map + .of("mappedPort", getMappedPort(idP, 8180))); + registry.add("spring.security.oauth2.resourceserver.jwt.issuer-uri", callable); + } + + protected static String getMappedPort(GenericContainer container, int originalPort) { + return container.getMappedPort(originalPort).toString(); + } + + protected String getToken(String username, String password) { + var tokenUrl = StringSubstitutor.replace( + "http://localhost:${mappedPort}/realms/greenbuildings/protocol/openid-connect/token", Map + .of("mappedPort", getMappedPort(idP, 8180))); + var response = RestAssured + .given() + .contentType(ContentType.URLENC) + .formParam("client_id", "testcontainer") + .formParam("grant_type", "password") + .formParam("username", username) + .formParam("password", password) + .post(tokenUrl) + .then() + .statusCode(200) + .extract() + .as(Token.class); + + return response.accessToken(); + } + + record Token( + @JsonProperty("access_token") String accessToken, + @JsonProperty("expires_in") long expiresIn, + @JsonProperty("refresh_expires_in") long refreshExpiresIn, + @JsonProperty("refresh_token") String refreshToken, + @JsonProperty("token_type") String tokenType, + @JsonProperty("not-before-policy") int notBeforePolicy, + @JsonProperty("session_state") String sessionState, + @JsonProperty("scope") String scope + ) { + } +} diff --git a/sep490-enterprise/src/test/java/enterprise/adapters/exchangerate_api/ExchangeRateApiCurrencyConverterTest.java b/sep490-enterprise/src/test/java/enterprise/adapters/exchangerate_api/ExchangeRateApiCurrencyConverterTest.java index d18af3a7..9de05788 100644 --- a/sep490-enterprise/src/test/java/enterprise/adapters/exchangerate_api/ExchangeRateApiCurrencyConverterTest.java +++ b/sep490-enterprise/src/test/java/enterprise/adapters/exchangerate_api/ExchangeRateApiCurrencyConverterTest.java @@ -1,5 +1,7 @@ package enterprise.adapters.exchangerate_api; +import enterprise.TestcontainersConfigs; +import green_buildings.commons.api.enums.Currency; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -7,14 +9,13 @@ import org.mockito.Mockito; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.web.client.RestTemplate; -import green_buildings.commons.api.enums.Currency; import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.assertEquals; @SpringBootTest -class ExchangeRateApiCurrencyConverterTest { +class ExchangeRateApiCurrencyConverterTest extends TestcontainersConfigs { @Mock RestTemplate restTemplate; diff --git a/sep490-enterprise/src/test/java/enterprise/rest/BuildingControllerTest.java b/sep490-enterprise/src/test/java/enterprise/rest/BuildingControllerTest.java new file mode 100644 index 00000000..5685f812 --- /dev/null +++ b/sep490-enterprise/src/test/java/enterprise/rest/BuildingControllerTest.java @@ -0,0 +1,90 @@ +package enterprise.rest; + +import enterprise.TestcontainersConfigs; +import enterprise.dtos.BuildingDTO; +import green_buildings.commons.api.dto.SearchCriteriaDTO; +import io.restassured.RestAssured; +import io.restassured.http.ContentType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; + +import java.math.BigDecimal; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class BuildingControllerTest extends TestcontainersConfigs { + + @LocalServerPort + private Integer port; + + @BeforeEach + void setUp() { + RestAssured.baseURI = "http://localhost:" + port; + } + + @Test + void getEnterpriseBuildings_withValidToken_returns200() { + RestAssured.given() + .auth().oauth2(getToken("enterprise.employee@greenbuildings.com", "enterprise.employee")) + .contentType(ContentType.JSON) + .body(new SearchCriteriaDTO(null, null, null)) + .when() + .post("/buildings/search") + .then() + .statusCode(200); + } + + @Test + void getEnterpriseBuildings_withInvalidToken_returns401() { + RestAssured.given() + .auth().oauth2("invalid_token") + .contentType(ContentType.JSON) + .body(new SearchCriteriaDTO(null, null, null)) + .when() + .post("/buildings/search") + .then() + .statusCode(401); + } + + @Test + void createBuilding_withValidToken_returns201() { + var building = BuildingDTO.builder() + .name("Building 1") + .floors(1) + .squareMeters(BigDecimal.valueOf(123.45)) + .build(); + RestAssured.given() + .auth().oauth2(getToken("enterprise.owner@greenbuildings.com", "enterprise.owner")) + .contentType(ContentType.JSON) + .body(building) + .when() + .post("/buildings") + .then() + .statusCode(201); + } + + @Test + void createBuilding_withMissingFields_returns400() { + RestAssured.given() + .auth().oauth2(getToken("enterprise.owner@greenbuildings.com", "enterprise.owner")) + .contentType(ContentType.JSON) + .body(BuildingDTO.builder().build()) + .when() + .post("/buildings") + .then() + .statusCode(400); + } + + @Test + void createBuilding_withInvalidToken_returns401() { + RestAssured.given() + .auth().oauth2("invalid_token") + .contentType(ContentType.JSON) + .body(BuildingDTO.builder().build()) + .when() + .post("/buildings") + .then() + .statusCode(401); + } +} diff --git a/sep490-enterprise/src/test/resources/application.yml b/sep490-enterprise/src/test/resources/application.yml deleted file mode 100644 index f567d4e8..00000000 --- a/sep490-enterprise/src/test/resources/application.yml +++ /dev/null @@ -1,21 +0,0 @@ -spring: - flyway: - enabled: false - datasource: - url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE - username: sa - password: password - driver-class-name: org.h2.Driver - jpa: - database-platform: org.hibernate.dialect.H2Dialect - hibernate: - naming: - physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy - ddl-auto: create-drop - open-in-view: false - properties: - hibernate: - show_sql: false - format_sql: false - query: - fail_on_pagination_over_collection_fetch: true diff --git a/sep490-enterprise/src/test/resources/db/migration/V0.0.1.4__TestcontainerUsers.sql b/sep490-enterprise/src/test/resources/db/migration/V0.0.1.4__TestcontainerUsers.sql new file mode 100644 index 00000000..d69bf888 --- /dev/null +++ b/sep490-enterprise/src/test/resources/db/migration/V0.0.1.4__TestcontainerUsers.sql @@ -0,0 +1,8 @@ +INSERT INTO enterprises (created_date, created_by, last_modified_date, last_modified_by, id, version, hotline, name, email) +VALUES ('2025-02-12 23:11:26.875648', 'enterprise.owner', '2025-02-12 23:11:26.875648', 'enterprise.owner', + '664748fa-1312-4456-a88c-1ef187ec9510', 0, '0123456789', 'FPT University', 'fptu.hcm@fpt.edu.vn'); + + +INSERT INTO wallets (created_date, created_by, last_modified_date, last_modified_by, id, version, enterprise_id, balance) +VALUES ('2025-02-12 23:11:26.883390', 'enterprise.owner', '2025-02-12 23:11:26.883390', 'enterprise.owner', + '14811e94-ede1-4bfa-83d1-5046b49b4fc2', 0, '664748fa-1312-4456-a88c-1ef187ec9510', 0); diff --git a/sep490-frontend/src/app/modules/enterprise/enterprise.module.ts b/sep490-frontend/src/app/modules/enterprise/enterprise.module.ts index bc67a002..d16a1935 100644 --- a/sep490-frontend/src/app/modules/enterprise/enterprise.module.ts +++ b/sep490-frontend/src/app/modules/enterprise/enterprise.module.ts @@ -6,16 +6,9 @@ import {PlanComponent} from './components/plan/plan.component'; import {EnterpriseRoutingModule} from './enterprise-routing.module'; import {EnterpriseComponent} from './enterprise.component'; import {MarkerService} from './services/marker.service'; +import {PaymentService} from './services/payment.service'; import {PopupService} from './services/popup.service'; import {RegionService} from './services/region.service'; -import {PaymentService} from './services/payment.service'; -import {Tag} from 'primeng/tag'; -import { - Accordion, - AccordionContent, - AccordionHeader, - AccordionPanel -} from 'primeng/accordion'; @NgModule({ declarations: [ @@ -24,15 +17,7 @@ import { PaymentComponent, BuildingsComponent ], - imports: [ - SharedModule, - EnterpriseRoutingModule, - Tag, - Accordion, - AccordionPanel, - AccordionHeader, - AccordionContent - ], + imports: [SharedModule, EnterpriseRoutingModule], providers: [MarkerService, PopupService, RegionService, PaymentService] }) export class EnterpriseModule {} diff --git a/sep490-frontend/src/app/modules/enterprise/models/payment.ts b/sep490-frontend/src/app/modules/enterprise/models/payment.ts index ec4ab7d3..1623177d 100644 --- a/sep490-frontend/src/app/modules/enterprise/models/payment.ts +++ b/sep490-frontend/src/app/modules/enterprise/models/payment.ts @@ -3,6 +3,6 @@ import {PaymentStatus} from '../enums/payment-status'; export interface PaymentDTO extends BaseDTO { createdDate: Date; - status: PaymentStatus; + status: keyof typeof PaymentStatus; amount: bigint; } diff --git a/sep490-frontend/src/app/modules/shared/shared.module.ts b/sep490-frontend/src/app/modules/shared/shared.module.ts index 5f91fd2d..051fcc98 100644 --- a/sep490-frontend/src/app/modules/shared/shared.module.ts +++ b/sep490-frontend/src/app/modules/shared/shared.module.ts @@ -4,10 +4,12 @@ import {NgModule} from '@angular/core'; import {FormsModule, ReactiveFormsModule} from '@angular/forms'; import {RouterModule} from '@angular/router'; import {TranslateModule} from '@ngx-translate/core'; +import {AccordionModule} from 'primeng/accordion'; import {AutoFocusModule} from 'primeng/autofocus'; import {AvatarModule} from 'primeng/avatar'; import {BadgeModule} from 'primeng/badge'; import {ButtonModule} from 'primeng/button'; +import {CardModule} from 'primeng/card'; import {DialogModule} from 'primeng/dialog'; import {DrawerModule} from 'primeng/drawer'; import {DynamicDialog} from 'primeng/dynamicdialog'; @@ -26,31 +28,26 @@ import {Select} from 'primeng/select'; import {SelectButtonModule} from 'primeng/selectbutton'; import {TableModule} from 'primeng/table'; import {TabMenuModule} from 'primeng/tabmenu'; +import {TagModule} from 'primeng/tag'; import {ToastModule} from 'primeng/toast'; import {ToggleSwitch} from 'primeng/toggleswitch'; +import {CardTemplateComponent} from './components/card/card-template/card-template.component'; import {ConfirmDialogComponent} from './components/dialog/confirm-dialog/confirm-dialog.component'; +import {FormFieldErrorComponent} from './components/form/form-field-error/form-field-error.component'; +import {PaymentStatusComponent} from './components/payment-status/payment-status.component'; import {TableTemplateComponent} from './components/table-template/table-template.component'; import {ErrorMessagesDirective} from './directives/error-messages.directive'; import {FormFieldErrorDirective} from './directives/form-field-error.directive'; import {TranslateParamsPipe} from './pipes/translate-params.pipe'; import {ModalProvider} from './services/modal-provider'; -import {FormFieldErrorComponent} from './components/form/form-field-error/form-field-error.component'; -import {PaymentStatusComponent} from './components/payment-status/payment-status.component'; -import {CardTemplateComponent} from './components/card/card-template/card-template.component'; -import {Tag} from 'primeng/tag'; -import {Card} from 'primeng/card'; -import { - Accordion, - AccordionContent, - AccordionHeader, - AccordionPanel -} from 'primeng/accordion'; const primeNgModules = [ + AccordionModule, AutoFocusModule, AvatarModule, BadgeModule, ButtonModule, + CardModule, DialogModule, DrawerModule, DynamicDialog, @@ -70,7 +67,8 @@ const primeNgModules = [ PasswordModule, RippleModule, MultiSelect, - ToggleSwitch + ToggleSwitch, + TagModule ]; const commons = [ @@ -94,16 +92,7 @@ const commons = [ PaymentStatusComponent, CardTemplateComponent ], - imports: [ - ...commons, - ...primeNgModules, - Tag, - Card, - Accordion, - AccordionPanel, - AccordionHeader, - AccordionContent - ], + imports: [...commons, ...primeNgModules], exports: [ ...commons, ...primeNgModules, diff --git a/sep490-idp/src/main/java/green_buildings/idp/service/impl/UserServiceImpl.java b/sep490-idp/src/main/java/green_buildings/idp/service/impl/UserServiceImpl.java index ceead0c0..00d3410e 100644 --- a/sep490-idp/src/main/java/green_buildings/idp/service/impl/UserServiceImpl.java +++ b/sep490-idp/src/main/java/green_buildings/idp/service/impl/UserServiceImpl.java @@ -87,7 +87,7 @@ public SignupResult signup(SignupDTO signupDTO, Model model) { .hotline(signupDTO.getEnterpriseHotline()) .build()); try { // Wait synchronously for response - var enterpriseId = UUID.fromString(future.get(100, TimeUnit.SECONDS).toString());// Timeout in case response is lost + var enterpriseId = UUID.fromString(future.get(TRANSACTION_TIMEOUT, TimeUnit.SECONDS).toString());// Timeout in case response is lost user.getEnterprise().setEnterprise(enterpriseId); userRepo.save(user); // COMPLETE } catch (TimeoutException e) { diff --git a/sep490-infrastructure/keycloak-testcontainer-provider/pom.xml b/sep490-infrastructure/keycloak-testcontainer-provider/pom.xml new file mode 100644 index 00000000..45efbcda --- /dev/null +++ b/sep490-infrastructure/keycloak-testcontainer-provider/pom.xml @@ -0,0 +1,72 @@ + + + 4.0.0 + + greenbuildings.keycloak.provider + idp-testcontainer-provider + 0.0.1-SNAPSHOT + + + 17 + 17 + UTF-8 + 24.0.5 + + + + + org.keycloak + keycloak-core + ${version.keycloak} + provided + + + org.keycloak + keycloak-server-spi + ${version.keycloak} + provided + + + org.keycloak + keycloak-server-spi-private + ${version.keycloak} + provided + + + org.keycloak + keycloak-services + ${version.keycloak} + provided + + + com.google.auto.service + auto-service + 1.1.1 + true + + + + + + maven-assembly-plugin + + + package + + single + + + + + + jar-with-dependencies + + greenbuildings-testcontainer-plugin + false + + + + + \ No newline at end of file diff --git a/sep490-infrastructure/keycloak-testcontainer-provider/src/main/java/keycloak/provider/Sep490IdentityProviderTokenMapper.java b/sep490-infrastructure/keycloak-testcontainer-provider/src/main/java/keycloak/provider/Sep490IdentityProviderTokenMapper.java new file mode 100644 index 00000000..625e63c8 --- /dev/null +++ b/sep490-infrastructure/keycloak-testcontainer-provider/src/main/java/keycloak/provider/Sep490IdentityProviderTokenMapper.java @@ -0,0 +1,94 @@ +package keycloak.provider; + +import com.google.auto.service.AutoService; +import org.keycloak.models.ClientSessionContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.ProtocolMapper; +import org.keycloak.protocol.oidc.mappers.AbstractOIDCProtocolMapper; +import org.keycloak.protocol.oidc.mappers.OIDCAccessTokenMapper; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.AccessToken; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +@AutoService(ProtocolMapper.class) +public class Sep490IdentityProviderTokenMapper extends AbstractOIDCProtocolMapper implements OIDCAccessTokenMapper { + private static final String ID = "green-buildings-mapper"; + private static final String LABEL = "GreenBuildings testContainer mapper"; + + private static final String AUTHORITIES_CLAIM = "authorities"; + private static final String ENTERPRISE_ID_CLAIM = "enterpriseId"; + private static final String PERMISSIONS_CLAIM = "permissions"; + + private final List configProperties = new ArrayList<>(); + + private void registerClaim(AccessToken token, String label, Object claim) { + token.getOtherClaims().put(label, claim); + } + + @Override + public AccessToken transformAccessToken(AccessToken token, + ProtocolMapperModel mappingModel, + KeycloakSession session, + UserSessionModel userSession, + ClientSessionContext clientSessionCtx) { + setClaim(token, mappingModel, userSession, session, clientSessionCtx); + var email = token.getOtherClaims().get("email").toString(); + registerClaim(token, PERMISSIONS_CLAIM, Collections.emptyList()); + if (email.startsWith("system.admin")) { + registerClaim(token, AUTHORITIES_CLAIM, List.of("ROLE_SYSTEM_ADMIN")); + return token; + } + if (email.startsWith("enterprise.owner")) { + registerClaim(token, AUTHORITIES_CLAIM, List.of("ROLE_ENTERPRISE_OWNER")); + registerClaim(token, ENTERPRISE_ID_CLAIM, "664748fa-1312-4456-a88c-1ef187ec9510"); + return token; + } + if (email.startsWith("enterprise.employee")) { + registerClaim(token, AUTHORITIES_CLAIM, List.of("ROLE_ENTERPRISE_EMPLOYEE")); + registerClaim(token, ENTERPRISE_ID_CLAIM, "664748fa-1312-4456-a88c-1ef187ec9510"); + return token; + } + registerClaim(token, ENTERPRISE_ID_CLAIM, UUID.randomUUID().toString()); + return token; + } + + /* + * MAPPER BOILERPLATE + */ + @Override + public String getDisplayCategory() { + return LABEL; + } + + @Override + public String getDisplayType() { + return getDisplayCategory(); + } + + @Override + public String getId() { + return ID; + } + + @Override + public String getHelpText() { + return getDisplayCategory(); + } + + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + public int getPriority() { + return 1000; + } + +} diff --git a/sep490-infrastructure/keycloak-testcontainer/Dockerfile b/sep490-infrastructure/keycloak-testcontainer/Dockerfile new file mode 100644 index 00000000..a756ee07 --- /dev/null +++ b/sep490-infrastructure/keycloak-testcontainer/Dockerfile @@ -0,0 +1,5 @@ +FROM quay.io/keycloak/keycloak:24.0.5 + +USER 0 +COPY ./greenbuildings-testcontainer-plugin.jar /opt/keycloak/providers +RUN /opt/keycloak/bin/kc.sh build --health-enabled=true diff --git a/sep490-infrastructure/keycloak-testcontainer/realm-export.json b/sep490-infrastructure/keycloak-testcontainer/realm-export.json new file mode 100644 index 00000000..b350f35d --- /dev/null +++ b/sep490-infrastructure/keycloak-testcontainer/realm-export.json @@ -0,0 +1,1881 @@ +{ + "id": "e386f887-3e8d-429f-988b-8f6aa10c9e0f", + "realm": "greenbuildings", + "notBefore": 0, + "defaultSignatureAlgorithm": "RS256", + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 300, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "clientOfflineSessionIdleTimeout": 0, + "clientOfflineSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "oauth2DeviceCodeLifespan": 600, + "oauth2DevicePollingInterval": 5, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": true, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxTemporaryLockouts": 0, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "defaultRole": { + "id": "e78f3544-4ba0-4258-96b6-92d23fa585b6", + "name": "default-roles-greenbuildings", + "description": "${role_default-roles}", + "composite": true, + "clientRole": false, + "containerId": "e386f887-3e8d-429f-988b-8f6aa10c9e0f" + }, + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpPolicyCodeReusable": false, + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account", + "view-groups" + ] + } + ] + }, + "clients": [ + { + "id": "30d282bb-62f0-4e1b-bf70-2e5ff4d5fce5", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/greenbuildings/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/greenbuildings/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "b1cd0b06-e0e3-434d-ab80-dd3ceb431752", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/greenbuildings/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/realms/greenbuildings/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "f3c67e45-d18b-4826-8d8f-65f78853ed09", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "86483d75-ceff-41c8-854a-c75e168652c5", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "3a764502-17de-422d-b8a9-3ff71a69866b", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "b0fdda0a-1a74-4bad-9b6b-12a099afdb67", + "clientId": "realm-management", + "name": "${client_realm-management}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "30e96ea7-a196-48d9-adbc-4efbf959969b", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/greenbuildings/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/admin/greenbuildings/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "post.logout.redirect.uris": "+", + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "3627dffd-8000-4bbd-8f3e-1dbba03b9929", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "acr", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "2dfc5ad3-6e79-4aab-896f-ecd559724877", + "clientId": "testcontainer", + "name": "", + "description": "", + "rootUrl": "", + "adminUrl": "", + "baseUrl": "", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "/*" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": true, + "protocol": "openid-connect", + "attributes": { + "oidc.ciba.grant.enabled": "false", + "post.logout.redirect.uris": "+", + "oauth2.device.authorization.grant.enabled": "false", + "backchannel.logout.session.required": "true", + "backchannel.logout.revoke.offline.tokens": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "acr", + "testcontainer", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "f45ba560-a6af-448c-aa23-9b9291fc1efe", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "e04c767c-0534-4ace-97c6-6d5b2c5e697b", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "email", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email", + "jsonType.label": "String" + } + }, + { + "id": "33de3614-7ad2-491e-b2a5-7a008218f13d", + "name": "email verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "emailVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "email_verified", + "jsonType.label": "boolean" + } + } + ] + }, + { + "id": "39eb7f10-d648-4b1a-b1c7-6a369d13e3fd", + "name": "testcontainer", + "description": "", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "gui.order": "", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "f2adbd90-8fb7-45a6-80f8-c72b7eebe173", + "name": "testcontainer", + "protocol": "openid-connect", + "protocolMapper": "green-buildings-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "f8e4e7c2-2f53-4506-af90-6da9c12577ac", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "401b3749-5133-4514-8bbe-f328bd05a90f", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String" + } + }, + { + "id": "ef45d929-e967-4767-9bce-a89859bdcdbc", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + }, + { + "id": "2fdc9eb1-dafd-4208-ba38-de2623cfbd11", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "80c4e77b-b2d9-42e1-8b36-5739920d2a74", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "ae0cb58c-cd67-4a31-a09d-33ba3f2fa2ab", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "5b35c782-85c7-40f8-afbd-e1cb1a3628c1", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, + { + "id": "59147233-5da2-49ec-83f9-3bce956d41f1", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "6932ac55-5b7c-4410-986b-ba89c29e23a2", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "upn", + "jsonType.label": "String" + } + }, + { + "id": "ff953ed1-8187-43dd-b3e7-e947cb253a7b", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "multivalued": "true", + "userinfo.token.claim": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "bdc411d8-cee3-43d2-b55c-60b78550b66b", + "name": "acr", + "description": "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "236c28be-d009-43f5-af5f-b355b115b459", + "name": "acr loa level", + "protocol": "openid-connect", + "protocolMapper": "oidc-acr-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + } + ] + }, + { + "id": "2177506b-173e-4df7-a987-b11cfd410f59", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "66ed8d79-1ebc-4864-95ad-5f4f069ded38", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "dd03cef7-65b4-4783-871a-92ccbfafdec2", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "58e43ceb-7310-4596-afd4-b6695c19a9ea", + "name": "zoneinfo", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "zoneinfo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "zoneinfo", + "jsonType.label": "String" + } + }, + { + "id": "4781c0ef-a0ea-49c9-970a-da60cc88f6d7", + "name": "gender", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "gender", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "gender", + "jsonType.label": "String" + } + }, + { + "id": "0880d8e0-eded-4449-b4ab-5e3ab158a500", + "name": "profile", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "profile", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "profile", + "jsonType.label": "String" + } + }, + { + "id": "2ef715a9-33e6-40ef-879a-646dbc94ee72", + "name": "middle name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "middleName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "middle_name", + "jsonType.label": "String" + } + }, + { + "id": "4ca34edd-dd51-4ecb-9c7a-16ec3fcbed7c", + "name": "nickname", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "nickname", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "nickname", + "jsonType.label": "String" + } + }, + { + "id": "4b3e0325-10d6-433b-ba21-4bc62eeea9ec", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "lastName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "family_name", + "jsonType.label": "String" + } + }, + { + "id": "3f1eaf6d-a17a-403c-9cf5-3053a1866471", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "username", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "preferred_username", + "jsonType.label": "String" + } + }, + { + "id": "9ca533f3-c474-4338-95f6-84d5c459406f", + "name": "website", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "website", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "website", + "jsonType.label": "String" + } + }, + { + "id": "92c55b7c-f1b5-49c8-9fea-072f9e25472b", + "name": "updated at", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "updatedAt", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "updated_at", + "jsonType.label": "long" + } + }, + { + "id": "06db94d3-dbc4-403b-a31f-cf262e1a0fb5", + "name": "picture", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "picture", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "picture", + "jsonType.label": "String" + } + }, + { + "id": "4cd1c610-d872-4c6f-84fa-effbd56038e5", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "introspection.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "7705e7ce-436d-47a1-864a-33f051271840", + "name": "locale", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "locale", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "locale", + "jsonType.label": "String" + } + }, + { + "id": "bee72ba3-9f40-4990-ab3a-acb73c199c60", + "name": "birthdate", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "birthdate", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "birthdate", + "jsonType.label": "String" + } + }, + { + "id": "b762f1af-dfbf-42aa-a68d-906f475cd0a8", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "firstName", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "given_name", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "11fd7713-9254-45dc-abb1-eb2cfdab9d9a", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "4e26a7be-6287-46a0-940a-1cf521bf0d6b", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "introspection.token.claim": "true", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "59caa4c4-f898-47f5-b982-870c589d493d", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "81c40fd8-fb18-4e10-a826-e797a0dd7ecb", + "name": "phone number", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumber", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number", + "jsonType.label": "String" + } + }, + { + "id": "2f1ebc03-6595-4ab2-80b8-5736dfb6c460", + "name": "phone number verified", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-attribute-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "user.attribute": "phoneNumberVerified", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "phone_number_verified", + "jsonType.label": "boolean" + } + } + ] + } + ], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "referrerPolicy": "no-referrer", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection": "1; mode=block", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "identityProviders": [], + "identityProviderMappers": [], + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "c90a14aa-35ae-4d1a-b157-5b71948690bb", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "62095690-0c17-4f07-a6dd-c0cfffe6dc84", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "efd4b2d5-6233-46d9-bfc1-2be10ae7e46d", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "557e4900-0acb-4d2a-b0e7-813bdca1797a", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "de81c928-12e4-46b3-a1d7-e067581a92b7", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-address-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper", + "oidc-full-name-mapper" + ] + } + }, + { + "id": "4c610f77-cfa6-4654-a794-df738d32c71d", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "fc956ba2-1369-46b1-818c-56dd49889496", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "oidc-usermodel-property-mapper", + "oidc-address-mapper", + "oidc-full-name-mapper", + "oidc-usermodel-attribute-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-attribute-mapper" + ] + } + }, + { + "id": "2a0979c4-9ef9-43f9-9ade-b2a824dad4f1", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "b7d56823-1235-4df7-812c-995993c44843", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "576a069c-beca-4364-b7b7-7e7056e7c455", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS512" + ] + } + }, + { + "id": "acd24221-b58d-4381-bef4-aa747f37dad9", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ] + } + }, + { + "id": "0852e57a-6a7b-4430-a346-479660d74dad", + "name": "rsa-enc-generated", + "providerId": "rsa-enc-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "d55c8e67-e42d-4974-a46a-af45918eae9b", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false + } + ] + }, + { + "id": "ebd6fdf5-3802-4651-a272-07d90e9308e7", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "f7045e7a-dac3-4164-86d0-d7e26d624243", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "30d47983-9fb3-403b-8a6f-46697a3a5132", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "46347ee9-0725-4c15-a67e-be8331656df2", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Account verification options", + "userSetupAllowed": false + } + ] + }, + { + "id": "c1402658-5971-4a6e-9633-7afc008159c5", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-otp", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "2a244c71-fa88-4494-bc05-e7aafb7eed96", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false + } + ] + }, + { + "id": "1b70329b-6d00-48b4-9b24-ff09fee8cb4b", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "922db710-f606-4afc-ae5a-2b1e3dea0301", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "8e47363a-99f5-42c2-b848-baafda59cdb8", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-secret-jwt", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "client-x509", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 40, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "d6a63cfd-9624-492c-a122-f42d1c32d744", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "direct-grant-validate-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "3f224f0b-c6d3-46e2-9f74-e884dea6cb6e", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "aa3f2e6c-51fc-46ae-a0b5-0a4c7655d6ec", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "User creation or linking", + "userSetupAllowed": false + } + ] + }, + { + "id": "9dbfdd16-375f-4de0-815c-196dec866aad", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 20, + "autheticatorFlow": true, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "b74672e1-f432-48fe-a78e-747b8085cc62", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "authenticatorFlow": true, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": true, + "flowAlias": "registration form", + "userSetupAllowed": false + } + ] + }, + { + "id": "0d8e09ad-427b-47e5-a45c-7aa86998bdcc", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-password-action", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 50, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-recaptcha-action", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 60, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "registration-terms-and-conditions", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 70, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "9eb9efe3-66d4-4ecc-b25c-9d8eed97faf2", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-credential-email", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "reset-password", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 30, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 40, + "autheticatorFlow": true, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false + } + ] + }, + { + "id": "c5c4d720-3dc0-48a0-ab4c-6e098d61d127", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "fb7ba9e1-61b0-4750-a51a-b4d5c9e33de6", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "3edd4672-16f4-49dc-88b8-cf0874bd5c37", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "TERMS_AND_CONDITIONS", + "name": "Terms and Conditions", + "providerId": "TERMS_AND_CONDITIONS", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "delete_account", + "name": "Delete Account", + "providerId": "delete_account", + "enabled": false, + "defaultAction": false, + "priority": 60, + "config": {} + }, + { + "alias": "webauthn-register", + "name": "Webauthn Register", + "providerId": "webauthn-register", + "enabled": true, + "defaultAction": false, + "priority": 70, + "config": {} + }, + { + "alias": "webauthn-register-passwordless", + "name": "Webauthn Register Passwordless", + "providerId": "webauthn-register-passwordless", + "enabled": true, + "defaultAction": false, + "priority": 80, + "config": {} + }, + { + "alias": "VERIFY_PROFILE", + "name": "Verify Profile", + "providerId": "VERIFY_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 90, + "config": {} + }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", + "attributes": { + "cibaBackchannelTokenDeliveryMode": "poll", + "cibaExpiresIn": "120", + "cibaAuthRequestedUserHint": "login_hint", + "oauth2DeviceCodeLifespan": "600", + "clientOfflineSessionMaxLifespan": "0", + "oauth2DevicePollingInterval": "5", + "clientSessionIdleTimeout": "0", + "parRequestUriLifespan": "60", + "clientSessionMaxLifespan": "0", + "clientOfflineSessionIdleTimeout": "0", + "cibaInterval": "5", + "realmReusableOtpCode": "false" + }, + "keycloakVersion": "24.0.5", + "userManagedAccessAllowed": false, + "clientProfiles": { + "profiles": [] + }, + "clientPolicies": { + "policies": [] + } +} \ No newline at end of file