diff --git a/Dockerfile b/Dockerfile index 2f7737ef..4cb0e9ce 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,10 +4,10 @@ ARG APP_VERSION WORKDIR /usr/app COPY . /usr/app RUN if [ "${RELEASE_MODE}" = true ]; then \ - gradle build --exclude-task test \ + gradle build --no-build-cache --exclude-task test \ -PreleaseMode=true \ -Dorg.gradle.project.version=${APP_VERSION}; \ - else gradle build --exclude-task test -Dorg.gradle.project.version=${APP_VERSION}; fi + else gradle build --no-build-cache --exclude-task test -Dorg.gradle.project.version=${APP_VERSION}; fi # For ARM build use flag: `--platform linux/arm64` FROM --platform=$BUILDPLATFORM amazoncorretto:21.0.1 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9fc028aa --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,460 @@ +version: "3.8" +services: + + ## External dependencies + + gateway: + image: traefik:v2.0.7 + container_name: traefik + logging: &logging + driver: "json-file" + options: + max-size: 100m + max-file: "5" + ports: + - "8080:8080" # ReportPortal UI + - "8081:8081" # Traefik dashboard + volumes: + - /var/run/docker.sock:/var/run/docker.sock + command: + - --providers.docker=true + - --providers.docker.constraints=Label(`traefik.expose`, `true`) + - --entrypoints.web.address=:8080 + - --entrypoints.traefik.address=:8081 + - --api.dashboard=true + - --api.insecure=true + networks: + - reportportal + restart: always + + opensearch: + image: opensearchproject/opensearch:2.11.0 + container_name: opensearch + logging: + <<: *logging + environment: + discovery.type: single-node + plugins.security.disabled: "true" + bootstrap.memory_lock: "true" + OPENSEARCH_JAVA_OPTS: -Xms512m -Xmx512m + DISABLE_INSTALL_DEMO_CONFIG: "true" + ulimits: + memlock: + soft: -1 + hard: -1 + ## Expose OpenSearch + # ports: + # - "9200:9200" + # - "9600:9600" + volumes: + - opensearch:/usr/share/opensearch/data + healthcheck: + test: ["CMD", "curl","-s" ,"-f", "http://0.0.0.0:9200/_cat/health"] + networks: + - reportportal + + postgres: + image: postgres:12.17-alpine3.17 + container_name: postgres + logging: + <<: *logging + shm_size: '512m' + environment: + POSTGRES_USER: &db_user rpuser + POSTGRES_PASSWORD: &db_password rppass + POSTGRES_DB: &db_name reportportal + volumes: + - postgres:/var/lib/postgresql/data + ## Expose Database + ports: + - "5432:5432" + command: + ## PostgrsSQL performance tuning + ## Ref: https://reportportal.io/docs/installation-steps/OptimalPerformanceHardwareSetup#5-postgresql-performance-tuning + -c checkpoint_completion_target=0.9 + -c work_mem=96MB + -c wal_writer_delay=20ms + -c synchronous_commit=off + -c wal_buffers=32MB + -c min_wal_size=2GB + -c max_wal_size=4GB + healthcheck: + test: ["CMD-SHELL", "pg_isready -d $$POSTGRES_DB -U $$POSTGRES_USER"] + interval: 10s + timeout: 120s + retries: 10 + networks: + - reportportal + restart: always + + rabbitmq: + image: bitnami/rabbitmq:3.12.2-debian-11-r8 + container_name: rabbitmq + logging: + <<: *logging + ## Expose RabbitMQ + # ports: + # - "5672:5672" + # - "15672:15672" + environment: + RABBITMQ_DEFAULT_USER: &rabbitmq_user rabbitmq + RABBITMQ_DEFAULT_PASS: &rabbitmq_password rabbitmq + healthcheck: + test: ["CMD", "rabbitmqctl", "status"] + interval: 30s + timeout: 30s + retries: 5 + networks: + - reportportal + restart: always + + ## ReportPortal services + + index: + image: reportportal/service-index:5.11.0 + container_name: reportportal-index + logging: + <<: *logging + depends_on: + gateway: + condition: service_started + environment: + LB_URL: http://gateway:8081 + TRAEFIK_V2_MODE: 'true' + healthcheck: + test: wget -q --spider http://0.0.0.0:8080/health + interval: 30s + timeout: 30s + retries: 10 + start_period: 10s + labels: + - "traefik.http.routers.index.rule=PathPrefix(`/`)" + - "traefik.http.routers.index.service=index" + - "traefik.http.services.index.loadbalancer.server.port=8080" + - "traefik.http.services.index.loadbalancer.server.scheme=http" + - "traefik.expose=true" + networks: + - reportportal + restart: always + + ui: + image: reportportal/service-ui:5.11.1 + container_name: reportportal-ui + environment: + RP_SERVER_PORT: "8080" + healthcheck: + test: wget -q --spider http://0.0.0.0:8080/health + interval: 30s + timeout: 30s + retries: 10 + start_period: 10s + labels: + - "traefik.http.middlewares.ui-strip-prefix.stripprefix.prefixes=/ui" + - "traefik.http.routers.ui.middlewares=ui-strip-prefix@docker" + - "traefik.http.routers.ui.rule=PathPrefix(`/ui`)" + - "traefik.http.routers.ui.service=ui" + - "traefik.http.services.ui.loadbalancer.server.port=8080" + - "traefik.http.services.ui.loadbalancer.server.scheme=http" + - "traefik.expose=true" + networks: + - reportportal + restart: always + + api: + image: reportportal/service-api:5.11.1 + container_name: reportportal-api + logging: + <<: *logging + depends_on: + rabbitmq: + condition: service_healthy + gateway: + condition: service_started + postgres: + condition: service_healthy + environment: + ## Double entry moves test logs from PostgreSQL to Elastic-type engines + ## Ref: https://reportportal.io/blog/double-entry-in-5.7.2 + ## RP_ELASTICSEARCH_HOST: http://opensearch:9200 + RP_DB_HOST: postgres + RP_DB_USER: *db_user + RP_DB_PASS: *db_password + RP_DB_NAME: *db_name + RP_AMQP_HOST: &rabbitmq_host rabbitmq + RP_AMQP_PORT: &rabbitmq_port 5672 + RP_AMQP_USER: *rabbitmq_user + RP_AMQP_PASS: *rabbitmq_password + RP_AMQP_APIUSER: *rabbitmq_user + RP_AMQP_APIPASS: *rabbitmq_password + RP_AMQP_ANALYZER-VHOST: analyzer + DATASTORE_TYPE: filesystem + LOGGING_LEVEL_ORG_HIBERNATE_SQL: info + RP_REQUESTLOGGING: "false" + AUDIT_LOGGER: "OFF" + MANAGEMENT_HEALTH_ELASTICSEARCH_ENABLED: "false" + RP_ENVIRONMENT_VARIABLE_ALLOW_DELETE_ACCOUNT: "false" + JAVA_OPTS: > + -Xmx1g + -XX:+HeapDumpOnOutOfMemoryError + -XX:HeapDumpPath=/tmp + -Dcom.sun.management.jmxremote.rmi.port=12349 + -Dcom.sun.management.jmxremote + -Dcom.sun.management.jmxremote.local.only=false + -Dcom.sun.management.jmxremote.port=9010 + -Dcom.sun.management.jmxremote.authenticate=false + -Dcom.sun.management.jmxremote.ssl=false + -Djava.rmi.server.hostname=0.0.0.0 + RP_JOBS_BASEURL: http://jobs:8686 + COM_TA_REPORTPORTAL_JOB_INTERRUPT_BROKEN_LAUNCHES_CRON: PT1H + RP_ENVIRONMENT_VARIABLE_PATTERN-ANALYSIS_BATCH-SIZE: 100 + RP_ENVIRONMENT_VARIABLE_PATTERN-ANALYSIS_PREFETCH-COUNT: 1 + RP_ENVIRONMENT_VARIABLE_PATTERN-ANALYSIS_CONSUMERS-COUNT: 1 + volumes: + - storage:/data/storage + healthcheck: + test: curl -f http://0.0.0.0:8585/health + interval: 60s + timeout: 30s + retries: 10 + start_period: 60s + labels: + - "traefik.http.middlewares.api-strip-prefix.stripprefix.prefixes=/api" + - "traefik.http.routers.api.middlewares=api-strip-prefix@docker" + - "traefik.http.routers.api.rule=PathPrefix(`/api`)" + - "traefik.http.routers.api.service=api" + - "traefik.http.services.api.loadbalancer.server.port=8585" + - "traefik.http.services.api.loadbalancer.server.scheme=http" + - "traefik.expose=true" + networks: + - reportportal + restart: always + + uat: + build: + context: . + dockerfile: Dockerfile + logging: + <<: *logging + environment: + RP_DB_HOST: postgres + RP_DB_USER: *db_user + RP_DB_PASS: *db_password + RP_DB_NAME: *db_name + RP_AMQP_HOST: *rabbitmq_host + RP_AMQP_PORT: *rabbitmq_port + RP_AMQP_USER: *rabbitmq_user + RP_AMQP_PASS: *rabbitmq_password + RP_AMQP_APIUSER: *rabbitmq_user + RP_AMQP_APIPASS: *rabbitmq_password + DATASTORE_TYPE: filesystem + RP_SESSION_LIVE: 86400 # in seconds + RP_SAML_SESSION-LIVE: 4320 + ## RP_INITIAL_ADMIN_PASSWORD - the initial password of the superadmin user for the first launch. This value can't change the password on redeployments. + RP_INITIAL_ADMIN_PASSWORD: "erebus" + JAVA_OPTS: -Djava.security.egd=file:/dev/./urandom -XX:MinRAMPercentage=60.0 -XX:MaxRAMPercentage=90.0 + volumes: + - storage:/data/storage + healthcheck: + test: curl -f http://0.0.0.0:9999/health + interval: 60s + timeout: 30s + retries: 10 + start_period: 60s + labels: + - "traefik.http.middlewares.uat-strip-prefix.stripprefix.prefixes=/uat" + - "traefik.http.routers.uat.middlewares=uat-strip-prefix@docker" + - "traefik.http.routers.uat.rule=PathPrefix(`/uat`)" + - "traefik.http.routers.uat.service=uat" + - "traefik.http.services.uat.loadbalancer.server.port=9999" + - "traefik.http.services.uat.loadbalancer.server.scheme=http" + - "traefik.expose=true" + depends_on: + postgres: + condition: service_healthy + networks: + - reportportal + restart: always + + jobs: + image: reportportal/service-jobs:5.11.1 + container_name: reportportal-jobs + logging: + <<: *logging + depends_on: + rabbitmq: + condition: service_healthy + gateway: + condition: service_started + postgres: + condition: service_healthy + environment: + ## Double entry moves test logs from PostgreSQL to Elastic-type engines + #№ Ref: https://reportportal.io/blog/double-entry-in-5.7.2 + ## RP_ELASTICSEARCH_HOST: http://opensearch:9200 + ## RP_ELASTICSEARCH_USERNAME: "" + ## RP_ELASTICSEARCH_PASSWORD: "" + RP_DB_HOST: postgres + RP_DB_USER: *db_user + RP_DB_PASS: *db_password + RP_DB_NAME: *db_name + RP_AMQP_HOST: *rabbitmq_host + RP_AMQP_PORT: *rabbitmq_port + RP_AMQP_USER: *rabbitmq_user + RP_AMQP_PASS: *rabbitmq_password + RP_AMQP_APIUSER: *rabbitmq_user + RP_AMQP_APIPASS: *rabbitmq_password + RP_AMQP_ANALYZER-VHOST: analyzer + DATASTORE_TYPE: filesystem + RP_ENVIRONMENT_VARIABLE_CLEAN_ATTACHMENT_CRON: 0 0 */24 * * * + RP_ENVIRONMENT_VARIABLE_CLEAN_LOG_CRON: 0 0 */24 * * * + RP_ENVIRONMENT_VARIABLE_CLEAN_LAUNCH_CRON: 0 0 */24 * * * + RP_ENVIRONMENT_VARIABLE_CLEAN_STORAGE_CRON: 0 0 */24 * * * + RP_ENVIRONMENT_VARIABLE_STORAGE_PROJECT_CRON: 0 */5 * * * * + RP_ENVIRONMENT_VARIABLE_CLEAN_EXPIREDUSER_CRON: 0 0 */24 * * * + RP_ENVIRONMENT_VARIABLE_CLEAN_EXPIREDUSER_RETENTIONPERIOD: 365 + RP_ENVIRONMENT_VARIABLE_NOTIFICATION_EXPIREDUSER_CRON: 0 0 */24 * * * + RP_ENVIRONMENT_VARIABLE_CLEAN_EVENTS_RETENTIONPERIOD: 365 + RP_ENVIRONMENT_VARIABLE_CLEAN_EVENTS_CRON: 0 30 05 * * * + RP_ENVIRONMENT_VARIABLE_CLEAN_STORAGE_CHUNKSIZE: 20000 + RP_PROCESSING_LOG_MAXBATCHSIZE: 2000 + RP_PROCESSING_LOG_MAXBATCHTIMEOUT: 6000 + RP_AMQP_MAXLOGCONSUMER: 1 + JAVA_OPTS: > + -Djava.security.egd=file:/dev/./urandom + -XX:+UseG1GC + -XX:+UseStringDeduplication + -XX:G1ReservePercent=20 + -XX:InitiatingHeapOccupancyPercent=60 + -XX:MaxRAMPercentage=70.0 + -XX:+HeapDumpOnOutOfMemoryError + -XX:HeapDumpPath=/tmp + volumes: + - storage:/data/storage + healthcheck: + test: curl -f http://0.0.0.0:8686/health || exit 1 + interval: 60s + timeout: 30s + retries: 10 + start_period: 60s + labels: + - traefik.http.middlewares.jobs-strip-prefix.stripprefix.prefixes=/jobs + - traefik.http.routers.jobs.middlewares=jobs-strip-prefix@docker + - traefik.http.routers.jobs.rule=PathPrefix(`/jobs`) + - traefik.http.routers.jobs.service=jobs + - traefik.http.services.jobs.loadbalancer.server.port=8686 + - traefik.http.services.jobs.loadbalancer.server.scheme=http + - traefik.expose=true + networks: + - reportportal + restart: always + + analyzer: + image: &analyzer_img reportportal/service-auto-analyzer:5.11.0-r1 + container_name: reportportal-analyzer + logging: + <<: *logging + environment: + LOGGING_LEVEL: info + AMQP_EXCHANGE_NAME: analyzer-default + AMQP_VIRTUAL_HOST: analyzer + AMQP_URL: amqp://rabbitmq:rabbitmq@rabbitmq:5672 + # ES_USER: + # ES_PASSWORD: + ES_HOSTS: http://opensearch:9200 + ANALYZER_BINARYSTORE_TYPE: filesystem + volumes: + - storage:/data/storage + depends_on: + opensearch: + condition: service_started + rabbitmq: + condition: service_healthy + networks: + - reportportal + restart: always + + analyzer-train: + image: *analyzer_img + container_name: reportportal-analyzer-train + logging: + <<: *logging + environment: + LOGGING_LEVEL: info + AMQP_EXCHANGE_NAME: analyzer-default + AMQP_VIRTUAL_HOST: analyzer + AMQP_URL: amqp://rabbitmq:rabbitmq@rabbitmq:5672 + # ES_USER: + # ES_PASSWORD: + ES_HOSTS: http://opensearch:9200 + INSTANCE_TASK_TYPE: train + UWSGI_WORKERS: 1 + ANALYZER_BINARYSTORE_TYPE: filesystem + volumes: + - storage:/data/storage + depends_on: + opensearch: + condition: service_started + rabbitmq: + condition: service_healthy + networks: + - reportportal + restart: always + + metrics-gatherer: + image: reportportal/service-metrics-gatherer:5.11.0-r1 + container_name: reportportal-metrics-gatherer + logging: + <<: *logging + environment: + LOGGING_LEVEL: info + # ES_USER: + # ES_PASSWORD: + ES_HOST: http://opensearch:9200 + POSTGRES_USER: *db_user + POSTGRES_PASSWORD: *db_password + POSTGRES_DB: *db_name + POSTGRES_HOST: postgres + POSTGRES_PORT: 5432 + ALLOWED_START_TIME: "22:00" + ALLOWED_END_TIME: "08:00" + # TZ: Europe/Minsk you can change a timezone like this to specify when metrics are gathered + AMQP_URL: amqp://rabbitmq:rabbitmq@rabbitmq:5672 + AMQP_VIRTUAL_HOST: analyzer + depends_on: + opensearch: + condition: service_started + networks: + - reportportal + restart: always + + migrations: + image: reportportal/migrations:5.11.0 + container_name: reportportal-migrations + logging: + <<: *logging + depends_on: + postgres: + condition: service_healthy + environment: + POSTGRES_SERVER: postgres + POSTGRES_PORT: 5432 + POSTGRES_DB: *db_name + POSTGRES_USER: *db_user + POSTGRES_PASSWORD: *db_password + OS_HOST: opensearch + OS_PORT: 9200 + OS_PROTOCOL: http + # OS_USER: + # OS_PASSWORD: + networks: + - reportportal + restart: on-failure + + +volumes: + opensearch: + storage: + postgres: + +networks: + reportportal: diff --git a/src/main/java/com/epam/reportportal/auth/endpoint/AuthConfigurationEndpoint.java b/src/main/java/com/epam/reportportal/auth/endpoint/AuthConfigurationEndpoint.java index ab941cbc..ce4f7fff 100644 --- a/src/main/java/com/epam/reportportal/auth/endpoint/AuthConfigurationEndpoint.java +++ b/src/main/java/com/epam/reportportal/auth/endpoint/AuthConfigurationEndpoint.java @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package com.epam.reportportal.auth.endpoint; import com.epam.reportportal.auth.integration.AuthIntegrationType; @@ -21,121 +20,106 @@ import com.epam.reportportal.auth.integration.handler.DeleteAuthIntegrationHandler; import com.epam.reportportal.auth.integration.handler.GetAuthIntegrationHandler; import com.epam.ta.reportportal.commons.ReportPortalUser; -import com.epam.reportportal.rules.exception.ReportPortalException; -import com.epam.reportportal.rules.exception.ErrorType; -import com.epam.reportportal.auth.OperationCompletionRS; -import com.epam.reportportal.model.integration.auth.AbstractAuthResource; -import com.epam.reportportal.model.integration.auth.UpdateAuthRQ; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import java.beans.PropertyEditorSupport; -import javax.validation.Valid; +import com.epam.ta.reportportal.exception.ReportPortalException; +import com.epam.ta.reportportal.ws.model.ErrorType; +import com.epam.ta.reportportal.ws.model.OperationCompletionRS; +import com.epam.ta.reportportal.ws.model.integration.auth.AbstractAuthResource; +import com.epam.ta.reportportal.ws.model.integration.auth.UpdateAuthRQ; +import io.swagger.annotations.ApiOperation; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.WebDataBinder; -import org.springframework.web.bind.annotation.DeleteMapping; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.InitBinder; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.PutMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.ResponseStatus; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.beans.PropertyEditorSupport; @RestController @RequestMapping("/settings/auth") -@Tag(name = "auth-configuration-endpoint", description = "Auth Configuration Endpoint") public class AuthConfigurationEndpoint { - private final CreateAuthIntegrationHandler createAuthIntegrationHandler; + private final CreateAuthIntegrationHandler createAuthIntegrationHandler; - private final DeleteAuthIntegrationHandler deleteAuthIntegrationHandler; + private final DeleteAuthIntegrationHandler deleteAuthIntegrationHandler; - private final GetAuthIntegrationHandler getAuthIntegrationHandler; + private final GetAuthIntegrationHandler getAuthIntegrationHandler; - @Autowired - public AuthConfigurationEndpoint(CreateAuthIntegrationHandler createAuthIntegrationHandler, - DeleteAuthIntegrationHandler deleteAuthIntegrationHandler, - GetAuthIntegrationHandler getAuthIntegrationHandler) { - this.createAuthIntegrationHandler = createAuthIntegrationHandler; - this.deleteAuthIntegrationHandler = deleteAuthIntegrationHandler; - this.getAuthIntegrationHandler = getAuthIntegrationHandler; - } + @Autowired + public AuthConfigurationEndpoint(CreateAuthIntegrationHandler createAuthIntegrationHandler, + DeleteAuthIntegrationHandler deleteAuthIntegrationHandler, GetAuthIntegrationHandler getAuthIntegrationHandler) { + this.createAuthIntegrationHandler = createAuthIntegrationHandler; + this.deleteAuthIntegrationHandler = deleteAuthIntegrationHandler; + this.getAuthIntegrationHandler = getAuthIntegrationHandler; + } - /** - * Creates or updates auth integration settings. - * - * @param request Update request - * @return Successful message or an error - */ - @Transactional - @PostMapping(value = "/{authType}") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "Create new auth integration") - public AbstractAuthResource createAuthIntegration(@RequestBody @Valid UpdateAuthRQ request, - @AuthenticationPrincipal ReportPortalUser user, - @PathVariable AuthIntegrationType authType) { - return createAuthIntegrationHandler.createAuthIntegration(authType, request, user); - } + /** + * Creates or updates auth integration settings + * + * @param request Update request + * @return Successful message or an error + */ + @Transactional + @PostMapping(value = "/{authType}") + @ResponseStatus(HttpStatus.OK) + @ApiOperation(value = "Create new auth integration") + public AbstractAuthResource createAuthIntegration(@RequestBody @Valid UpdateAuthRQ request, @AuthenticationPrincipal ReportPortalUser user, + @PathVariable AuthIntegrationType authType) { + return createAuthIntegrationHandler.createAuthIntegration(authType, request, user); + } - /** - * Creates or updates auth integration settings. - * - * @param request Update request - * @return Successful message or an error - */ - @Transactional - @PutMapping(value = "/{authType}/{integrationId}") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "Update auth integration") - public AbstractAuthResource updateAuthIntegration(@RequestBody @Valid UpdateAuthRQ request, - @AuthenticationPrincipal ReportPortalUser user, - @PathVariable AuthIntegrationType authType, @PathVariable Long integrationId) { - return createAuthIntegrationHandler.updateAuthIntegration(authType, integrationId, request, - user); - } + /** + * Creates or updates auth integration settings + * + * @param request Update request + * @return Successful message or an error + */ + @Transactional + @PutMapping(value = "/{authType}/{integrationId}") + @ResponseStatus(HttpStatus.OK) + @ApiOperation(value = "Update auth integration") + public AbstractAuthResource updateAuthIntegration(@RequestBody @Valid UpdateAuthRQ request, @AuthenticationPrincipal ReportPortalUser user, + @PathVariable AuthIntegrationType authType, @PathVariable Long integrationId) { + return createAuthIntegrationHandler.updateAuthIntegration(authType, integrationId, request, user); + } - /** - * Get auth settings by type. - * - * @param authType Type of Auth - * @return Successful message or an error - */ - @Transactional(readOnly = true) - @GetMapping(value = "/{authType}") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "Retrieves auth settings") - public AbstractAuthResource getSettings(@PathVariable AuthIntegrationType authType) { - return getAuthIntegrationHandler.getIntegrationByType(authType); - } + /** + * Get auth settings by type + * + * @param authType Type of Auth + * @return Successful message or an error + */ + @Transactional(readOnly = true) + @GetMapping(value = "/{authType}") + @ResponseStatus(HttpStatus.OK) + @ApiOperation(value = "Retrieves auth settings") + public AbstractAuthResource getSettings(@PathVariable AuthIntegrationType authType) { + return getAuthIntegrationHandler.getIntegrationByType(authType); + } - /** - * Deletes LDAP auth settings. - * - * @param integrationId Type of Auth - * @return Successful message or an error - */ - @Transactional - @DeleteMapping(value = "/{integrationId}") - @ResponseStatus(HttpStatus.OK) - @Operation(summary = "Retrieves auth settings") - public OperationCompletionRS deleteSettings(@PathVariable Long integrationId) { - return deleteAuthIntegrationHandler.deleteAuthIntegrationById(integrationId); - } + /** + * Deletes LDAP auth settings + * + * @param integrationId Type of Auth + * @return Successful message or an error + */ + @Transactional + @DeleteMapping(value = "/{integrationId}") + @ResponseStatus(HttpStatus.OK) + @ApiOperation(value = "Retrieves auth settings") + public OperationCompletionRS deleteSettings(@PathVariable Long integrationId) { + return deleteAuthIntegrationHandler.deleteAuthIntegrationById(integrationId); + } - @InitBinder - public void initBinder(final WebDataBinder webdataBinder) { - webdataBinder.registerCustomEditor(AuthIntegrationType.class, new PropertyEditorSupport() { - @Override - public void setAsText(String text) throws IllegalArgumentException { - setValue(AuthIntegrationType.fromId(text) - .orElseThrow( - () -> new ReportPortalException(ErrorType.INCORRECT_AUTHENTICATION_TYPE, text))); - } - }); - } -} + @InitBinder + public void initBinder(final WebDataBinder webdataBinder) { + webdataBinder.registerCustomEditor(AuthIntegrationType.class, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + setValue(AuthIntegrationType.fromId(text) + .orElseThrow(() -> new ReportPortalException(ErrorType.INCORRECT_AUTHENTICATION_TYPE, text))); + } + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/epam/reportportal/auth/integration/saml/SamlUserReplicator.java b/src/main/java/com/epam/reportportal/auth/integration/saml/SamlUserReplicator.java index 9557fb67..ff9015aa 100644 --- a/src/main/java/com/epam/reportportal/auth/integration/saml/SamlUserReplicator.java +++ b/src/main/java/com/epam/reportportal/auth/integration/saml/SamlUserReplicator.java @@ -112,7 +112,6 @@ public User replicateUser(ReportPortalSamlAuthentication samlAuthentication) { } user.setUserType(UserType.SAML); - user.setRole(UserRole.USER); user.setExpired(false); Project project = generatePersonalProject(user); @@ -160,6 +159,8 @@ private void populateUserDetails(User user, List details) { findAttributeValue(details, UserAttribute.FIRST_NAME.toString(), String.class); String lastName = findAttributeValue(details, UserAttribute.LAST_NAME.toString(), String.class); user.setFullName(String.join(" ", firstName, lastName)); + + user.setRole(UserRole.USER); } private void populateUserDetailsIfSettingsArePresent(User user, Integration integration, @@ -173,7 +174,7 @@ private void populateUserDetailsIfSettingsArePresent(User user, Integration inte Optional idpFullNameOptional = SamlParameter.FULL_NAME_ATTRIBUTE.getParameter(integration); - if (!idpFullNameOptional.isPresent()) { + if (idpFullNameOptional.isEmpty()) { String firstName = findAttributeValue(details, SamlParameter.FIRST_NAME_ATTRIBUTE.getParameter(integration).orElse(null), String.class ); @@ -185,6 +186,15 @@ private void populateUserDetailsIfSettingsArePresent(User user, Integration inte String fullName = findAttributeValue(details, idpFullNameOptional.get(), String.class); user.setFullName(fullName); } + + String roles = findAttributeValue(details, + SamlParameter.ROLES_ATTRIBUTE.getParameter(integration).orElse(null), String.class + ); + if (Objects.nonNull(roles) && roles.toLowerCase().contains("admin")) { + user.setRole(UserRole.ADMINISTRATOR); + } else { + user.setRole(UserRole.USER); + } } private T findAttributeValue(List attributes, String lookingFor, Class castTo) { diff --git a/src/main/java/com/epam/reportportal/auth/model/SamlProvidersResource.java b/src/main/java/com/epam/reportportal/auth/model/SamlProvidersResource.java new file mode 100644 index 00000000..3510bd3d --- /dev/null +++ b/src/main/java/com/epam/reportportal/auth/model/SamlProvidersResource.java @@ -0,0 +1,33 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.auth.model; + +import com.epam.ta.reportportal.ws.model.integration.auth.AbstractAuthResource; +import com.fasterxml.jackson.annotation.JsonInclude; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.Valid; +import java.util.List; + +@Setter +@Getter +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SamlProvidersResource extends AbstractAuthResource { + @Valid + private List providers; +} diff --git a/src/main/java/com/epam/reportportal/auth/model/SamlResource.java b/src/main/java/com/epam/reportportal/auth/model/SamlResource.java new file mode 100644 index 00000000..6e31b2c7 --- /dev/null +++ b/src/main/java/com/epam/reportportal/auth/model/SamlResource.java @@ -0,0 +1,91 @@ +/* + * Copyright 2024 EPAM Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.epam.reportportal.auth.model; + +import com.epam.ta.reportportal.ws.model.integration.auth.AbstractAuthResource; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.annotations.ApiModel; +import lombok.Getter; +import lombok.Setter; + +import javax.validation.constraints.NotEmpty; + +/** + * @author Ihar Kahadouski + */ + +@Setter +@Getter +@ApiModel +@JsonInclude(JsonInclude.Include.NON_NULL) +public class SamlResource extends AbstractAuthResource { + + private Long id; + + @JsonProperty(value = "callbackUrl") + private String callbackUrl; + + /** + * Provider name associated with IDP + */ + @NotEmpty + private String identityProviderName; + /** + * Alias associated with IDP + */ + private String identityProviderAlias; + /** + * IDP metadata URL + */ + @NotEmpty + private String identityProviderMetadataUrl; + /** + * Attribute Name Format Id associated with IDP for user identification + */ + private String identityProviderNameId; + /** + * IDP URL + */ + private String identityProviderUrl; + /** + * Attribute name associated with full name of user in SAML response + */ + private String fullNameAttribute; + /** + * Attribute name associated with first name of user in SAML response + */ + private String firstNameAttribute; + /** + * Attribute name associated with last name of user in SAML response + */ + private String lastNameAttribute; + /** + * Attribute name associated with email of user in SAML response + */ + @NotEmpty + private String emailAttribute; + /** + * Attribute name associated with roles of user in SAML response + */ + private String rolesAttribute; + /** + * Indicates IDP availability for authentication + */ + private boolean enabled; + +} \ No newline at end of file diff --git a/tests/requests/saml.http b/tests/requests/saml.http new file mode 100644 index 00000000..2c9205ad --- /dev/null +++ b/tests/requests/saml.http @@ -0,0 +1,47 @@ +@token = "" + + +POST http://localhost:8080/uat/settings/auth/saml/ +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "enabled":true, + "integrationParameters": + { + "rolesAttribute":"jobRole", + "firstNameAttribute":"givenname", + "lastNameAttribute":"surname", + "emailAttribute":"mail", + "identityProviderMetadataUrl":"https://login.microsoftonline.com/...", + "identityProviderUrl":"https://sts.windows.net/...", + "identityProviderName":"Azure", + "identityProviderAlias":"https://sts.windows.net/...", + "identityProviderNameId":"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "callbackUrl":"http://localhost:8080/uat" + } +} + +### +@saml_resource="" + +PUT http://localhost:8080/uat/settings/auth/saml/{{saml_resource}} +Content-Type: application/json +Authorization: Bearer {{token}} + +{ + "enabled":true, + "integrationParameters": + { + "rolesAttribute":"jobRole", + "firstNameAttribute":"givenname", + "lastNameAttribute":"surname", + "emailAttribute":"mail", + "identityProviderMetadataUrl":"https://login.microsoftonline.com/...", + "identityProviderUrl":"https://sts.windows.net/...", + "identityProviderName":"Azure", + "identityProviderAlias":"https://sts.windows.net/...", + "identityProviderNameId":"urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified", + "callbackUrl":"http://localhost:8080/uat" + } +}