diff --git a/config/spinnaker.yml b/config/spinnaker.yml index 0f1875ed37..acfb04dc83 100644 --- a/config/spinnaker.yml +++ b/config/spinnaker.yml @@ -1,2 +1,2 @@ oes: - spinnakerVersion: "OES 1.30.1.20240300" \ No newline at end of file + spinnakerVersion: "OES 1.30.1.20240300" diff --git a/gate-basic/src/main/java/com/netflix/spinnaker/gate/security/basic/BasicAuthProvider.java b/gate-basic/src/main/java/com/netflix/spinnaker/gate/security/basic/BasicAuthProvider.java index 296b3dae4a..52b664597c 100644 --- a/gate-basic/src/main/java/com/netflix/spinnaker/gate/security/basic/BasicAuthProvider.java +++ b/gate-basic/src/main/java/com/netflix/spinnaker/gate/security/basic/BasicAuthProvider.java @@ -17,6 +17,7 @@ import com.netflix.spinnaker.gate.services.OesAuthorizationService; import com.netflix.spinnaker.gate.services.PermissionService; +import com.netflix.spinnaker.security.User; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; @@ -31,8 +32,6 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.User; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; @Slf4j @@ -84,7 +83,11 @@ public Authentication authenticate(Authentication authentication) throws Authent } else { grantedAuthorities.add(new SimpleGrantedAuthority("USER")); } - UserDetails principal = new User(name, password, grantedAuthorities); + User principal = new User(); + principal.setUsername(name); + principal.setEmail(name); + principal.setRoles(roles); + return new UsernamePasswordAuthenticationToken(principal, password, grantedAuthorities); } diff --git a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/OesAuthorizationService.groovy b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/OesAuthorizationService.groovy index 07faf0f175..759d592191 100644 --- a/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/OesAuthorizationService.groovy +++ b/gate-core/src/main/groovy/com/netflix/spinnaker/gate/services/OesAuthorizationService.groovy @@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestParam -@FeignClient(name = "OES", url = '${services.platform.baseUrl}') +@FeignClient(name = "OES", url = '${services.platform.baseUrl}', dismiss404 = true) interface OesAuthorizationService { @PutMapping(value = "/platformservice/v2/usergroups/importAndCache", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) diff --git a/gate-saml/src/main/java/com/opsmx/spinnaker/gate/security/saml/SamlSecurityConfiguration.java b/gate-saml/src/main/java/com/opsmx/spinnaker/gate/security/saml/SamlSecurityConfiguration.java index 5213afdc21..5a368eae2c 100644 --- a/gate-saml/src/main/java/com/opsmx/spinnaker/gate/security/saml/SamlSecurityConfiguration.java +++ b/gate-saml/src/main/java/com/opsmx/spinnaker/gate/security/saml/SamlSecurityConfiguration.java @@ -16,6 +16,9 @@ package com.opsmx.spinnaker.gate.security.saml; +import static org.springframework.security.saml2.core.Saml2ErrorCodes.INVALID_ASSERTION; +import static org.springframework.security.saml2.core.Saml2ErrorCodes.INVALID_IN_RESPONSE_TO; + import com.netflix.spectator.api.Registry; import com.netflix.spinnaker.fiat.shared.FiatClientConfigurationProperties; import com.netflix.spinnaker.gate.config.AuthConfig; @@ -28,7 +31,9 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.stream.Collectors; import lombok.extern.slf4j.Slf4j; import org.opensaml.saml.saml2.core.Assertion; import org.springframework.beans.factory.annotation.Autowired; @@ -46,6 +51,8 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.saml2.core.Saml2Error; +import org.springframework.security.saml2.core.Saml2ResponseValidatorResult; import org.springframework.security.saml2.provider.service.authentication.OpenSaml4AuthenticationProvider; import org.springframework.security.saml2.provider.service.authentication.Saml2AuthenticatedPrincipal; import org.springframework.security.saml2.provider.service.authentication.Saml2Authentication; @@ -90,6 +97,9 @@ public class SamlSecurityConfiguration { public static final String defaultFilterUrl = "{baseUrl}" + Saml2WebSsoAuthenticationFilter.DEFAULT_FILTER_PROCESSES_URI; + @Value("${spring.security.saml2.validation.inresponseto:false}") + private boolean ignoreInResponseToValidation; + @Bean public UserDetailsService userDetailsService() { return username -> { @@ -110,8 +120,15 @@ public RememberMeServices rememberMeServices(UserDetailsService userDetailsServi @Bean public OpenSaml4AuthenticationProvider authenticationProvider() { + var authProvider = new OpenSaml4AuthenticationProvider(); authProvider.setResponseAuthenticationConverter(extractUserDetails()); + log.debug("ignoreInResponseToValidation :{}", ignoreInResponseToValidation); + if (ignoreInResponseToValidation) { + authProvider.setAssertionValidator(removeAssertionError()); + authProvider.setResponseValidator(removeInResonseToError()); + } + return authProvider; } @@ -208,7 +225,7 @@ public SecurityFilterChain samlFilterChain( return responseToken -> { List roles = new ArrayList<>(); - log.debug("responseToken : {}", responseToken); + log.debug("responseToken : {}", responseToken.getToken().getSaml2Response()); Saml2Authentication authentication = delegate.convert(responseToken); Saml2AuthenticatedPrincipal principal = (Saml2AuthenticatedPrincipal) authentication.getPrincipal(); @@ -276,6 +293,54 @@ public SecurityFilterChain samlFilterChain( }; } + private Converter + removeAssertionError() { + log.debug("**remove assertion error from Saml2ResponseValidatorResult Errors**"); + Converter + delegate = OpenSaml4AuthenticationProvider.createDefaultAssertionValidator(); + return assertionToken -> { + log.debug("responseToken : {}", assertionToken.getToken().getSaml2Response()); + Saml2ResponseValidatorResult result = delegate.convert(assertionToken); + result + .getErrors() + .forEach( + error -> + log.debug( + " error code :{} and description :{}", + error.getErrorCode(), + error.getDescription())); + Collection errors = + result.getErrors().stream() + .filter((error) -> !error.getErrorCode().equals(INVALID_ASSERTION)) + .collect(Collectors.toList()); + return Saml2ResponseValidatorResult.failure(errors); + }; + } + + private Converter + removeInResonseToError() { + log.debug("**remove InResonseTo error from Saml2ResponseValidatorResult Errors**"); + Converter + delegate = OpenSaml4AuthenticationProvider.createDefaultResponseValidator(); + return responseToken -> { + log.debug("responseToken : {}", responseToken.getToken().getSaml2Response()); + Saml2ResponseValidatorResult result = delegate.convert(responseToken); + result + .getErrors() + .forEach( + error -> + log.debug( + " error code :{} and description :{}", + error.getErrorCode(), + error.getDescription())); + Collection errors = + result.getErrors().stream() + .filter((error) -> !error.getErrorCode().equals(INVALID_IN_RESPONSE_TO)) + .collect(Collectors.toList()); + return Saml2ResponseValidatorResult.failure(errors); + }; + } + private void loginWithRoles(String username, List roles) { var id = registry.createId("fiat.login").withTag("type", "saml"); diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy index b7442984f2..33130a4ac3 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/GateConfig.groovy @@ -121,7 +121,8 @@ class GateConfig extends RedisHttpSessionConfiguration { */ @Bean JedisPool jedis(@Value('${redis.connection:redis://localhost:6379}') String connection, - @Value('${redis.timeout:2000}') int timeout) { + @Value('${redis.timeout:2000}') int timeout, + @Value('${redis.certificate_location:#{null}}') String certFilePath) { return new JedisPool(new URI(connection), timeout) } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/PostConnectionConfiguringJedisConnectionFactory.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/PostConnectionConfiguringJedisConnectionFactory.java index 5a155601e8..262292d154 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/PostConnectionConfiguringJedisConnectionFactory.java +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/config/PostConnectionConfiguringJedisConnectionFactory.java @@ -1,13 +1,19 @@ package com.netflix.spinnaker.gate.config; import com.google.common.base.Splitter; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.net.URI; +import java.security.KeyStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateFactory; import java.util.List; import java.util.Optional; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; @@ -26,6 +32,7 @@ * *

This Redis pool is used for Spring Boot's session management, not for the rate limit storage. */ +@Slf4j @Component public class PostConnectionConfiguringJedisConnectionFactory extends JedisConnectionFactory { @@ -40,11 +47,15 @@ public class PostConnectionConfiguringJedisConnectionFactory extends JedisConnec private volatile boolean ranConfigureRedisAction; + private String password = "keyStorePass"; + @Autowired public PostConnectionConfiguringJedisConnectionFactory( @Value("${redis.connection:redis://localhost:6379}") String connectionUri, @Value("${redis.timeout:2000}") int timeout, - @ConnectionPostProcessor Optional configureRedisAction) { + @Value(value = "${redis.certificate_location:#{null}}") String certFilePath, + @ConnectionPostProcessor Optional configureRedisAction) + throws Exception { this.configureRedisAction = configureRedisAction.orElse(new ConfigureNotifyKeyspaceEventsAction()); @@ -63,6 +74,39 @@ public PostConnectionConfiguringJedisConnectionFactory( if (redisUri.getScheme().equals("rediss")) { setUseSsl(true); + String jksFilePath = "/opsmx/conf/redis-truststore.jks"; + String alias = "redis-truststore"; // An alias to identify the certificate in the keystore + char[] password = this.password.toCharArray(); // Keystore password + + FileInputStream certInputStream = null; + FileOutputStream jksOutputStream = null; + + /** + * If SSL is used then below steps add the certificate necessary for connection to redis as a + * java keystore and then add java keystore file's path as a system property for use in + * connection. + */ + try { + CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + certInputStream = new FileInputStream(certFilePath); + Certificate certificate = certificateFactory.generateCertificate(certInputStream); + + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(null, password); + keyStore.setCertificateEntry(alias, certificate); + jksOutputStream = new FileOutputStream(jksFilePath); + keyStore.store(jksOutputStream, password); + + log.info("Certificate has been added to the KeyStore successfully."); + } catch (Exception e) { + throw e; + } finally { + certInputStream.close(); + jksOutputStream.close(); + } + + System.setProperty("javax.net.ssl.trustStore", "/opsmx/conf/redis-truststore.jks"); + System.setProperty("javax.net.ssl.trustStorePassword", this.password); } } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/OpsmxOesController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/OpsmxOesController.groovy index 683ad0c40f..b77c78b3e7 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/OpsmxOesController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/OpsmxOesController.groovy @@ -551,4 +551,11 @@ class OpsmxOesController { return ResponseEntity.ok().headers(headers).body(manifestFile) } } + + @Operation(summary = "Rest api for fetching importing account environment mapping records") + @RequestMapping(value = "/acctEnvMapping/import", method = RequestMethod.POST) + Object importAccountsFromSpinnaker() { + + return opsmxOesService.importAccountEnvironmentMappings(); + } } diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineController.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineController.groovy index 6b502d186f..c8e56facb3 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineController.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/controllers/PipelineController.groovy @@ -116,6 +116,8 @@ class PipelineController { String resultStatus = result.get("status") if (!"SUCCEEDED".equalsIgnoreCase(resultStatus)) { + log.debug("Pipeline save operation failed. Result: {}", result) + String exception = result.variables.find { it.key == "exception" }?.value?.details?.errors?.getAt(0) throw new PipelineException( exception ?: "Pipeline save operation did not succeed: ${result.get("id", "unknown task id")} (status: ${resultStatus})" diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/graphql/model/Node.java b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/graphql/model/Node.java index cbc8c114ac..46b412a057 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/graphql/model/Node.java +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/graphql/model/Node.java @@ -15,7 +15,9 @@ */ package com.netflix.spinnaker.gate.graphql.model; -/** @link */ +/** + * @link + */ public interface Node { String getId(); diff --git a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/OpsmxOesService.groovy b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/OpsmxOesService.groovy index 2c997b9585..55370119cb 100644 --- a/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/OpsmxOesService.groovy +++ b/gate-web/src/main/groovy/com/netflix/spinnaker/gate/services/internal/OpsmxOesService.groovy @@ -195,4 +195,7 @@ interface OpsmxOesService { Object evaluateStaticPolicy(@Path('version') String version, @Body Object data) + @POST("/oes/acctEnvMapping/import") + Object importAccountEnvironmentMappings() + } diff --git a/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/controllers/OpsmxAuditClientServiceController.groovy b/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/controllers/OpsmxAuditClientServiceController.groovy index e4800ff8a9..bc8c0523d6 100644 --- a/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/controllers/OpsmxAuditClientServiceController.groovy +++ b/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/controllers/OpsmxAuditClientServiceController.groovy @@ -214,4 +214,22 @@ class OpsmxAuditClientServiceController { return ResponseEntity.status(response.getStatus()).build() } + @Operation(summary = "Rest api for fetching all account environment mapping records") + @RequestMapping(value = "/v3/acctEnvMapping", method = RequestMethod.GET) + Object getAllAcctEnvMappings() { + return opsmxAuditClientService.getAllAccountEnvironmentMappings(); + } + @Operation(summary = "Rest api for fetching account environment mapping record with id") + @RequestMapping(value = "/v3/acctEnvMapping/{id}", method = RequestMethod.GET) + Object getAcctEnvMappingWithId(@PathVariable("id") Integer id) { + return opsmxAuditClientService.getAccountEnvironmentMappingWithId(id); + } + @Operation(summary = "Rest api for fetching all unique environment records") + @RequestMapping(value = "/v3/env", method = RequestMethod.GET) + Object getAllUniqueEnv() { + + return opsmxAuditClientService.getAllUniqueEnvironments(); + } + + } diff --git a/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/controllers/OpsmxAuditServiceController.groovy b/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/controllers/OpsmxAuditServiceController.groovy index b9d67e8df3..90466c2dc0 100644 --- a/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/controllers/OpsmxAuditServiceController.groovy +++ b/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/controllers/OpsmxAuditServiceController.groovy @@ -16,16 +16,28 @@ package com.opsmx.spinnaker.gate.controllers +import com.netflix.spinnaker.gate.config.ServiceConfiguration +import com.netflix.spinnaker.gate.exceptions.OesRequestException +import com.netflix.spinnaker.security.AuthenticatedRequest import com.opsmx.spinnaker.gate.services.OpsmxAuditService import groovy.util.logging.Slf4j import io.swagger.v3.oas.annotations.Operation +import okhttp3.MultipartBody +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.Buffer +import okio.BufferedSink +import okio.Okio +import okio.Source import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestMethod +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile @Slf4j @RestController @@ -36,6 +48,12 @@ class OpsmxAuditServiceController { @Autowired OpsmxAuditService opsmxAuditService + @Autowired + ServiceConfiguration serviceConfiguration + + @Autowired + OkHttpClient okHttpClient + @Operation(summary = "Endpoint for audit rest services") @RequestMapping(value = "/{version}/{type}/{source}/{source1}", method = RequestMethod.POST) Object postAuditService1(@PathVariable("version") String version, @@ -47,4 +65,79 @@ class OpsmxAuditServiceController { return opsmxAuditService.postAuditService1(version, type, source, source1, data) } + @Operation(summary = "Rest api for audit save account environment mapping") + @RequestMapping(value = "/v1/acctEnvMapping", method = RequestMethod.POST) + Object saveAcctEnvMapping(@RequestBody Object data) { + return opsmxAuditService.saveAccountEnvironmentMapping(data) + } + @Operation(summary = "Rest api for updating an account environment mapping") + @RequestMapping(value = "/v1/acctEnvMapping/{id}", method = RequestMethod.PUT) + Object updateAcctEnvMapping(@PathVariable("id") Integer id, @RequestBody Object data) { + return opsmxAuditService.updateAccountEnvironmentMapping(id, data) + } + + @Operation(summary = "Rest api for deleting account environment mapping record with id") + @RequestMapping(value = "/v1/acctEnvMapping/{id}", method = RequestMethod.DELETE) + Object deleteAcctEnvMapping(@PathVariable("id") Integer id) { + return opsmxAuditService.deleteAccountEnvironmentMappingWithId(id); + } + @Operation(summary = "Rest api for bulk import of account environment mappings") + @RequestMapping(value = "/v1/acctEnvMapping/bulkimport", method = RequestMethod.POST, consumes = "multipart/form-data") + Object bulkImportAcctEnvironmentMappings(@RequestParam("file") MultipartFile data) { + try { + return uploadToAuditService(data) + } catch (Exception e) { + throw new RuntimeException("Failed to process file: ${e.message}", e) + } + } + private String uploadToAuditService(MultipartFile data) { + def obj = AuthenticatedRequest.propagate { + def request = new Request.Builder() + .url(serviceConfiguration.getServiceEndpoint("auditservice").url +"/auditservice/v1/acctEnvMapping/bulkimport") + .post(uploadFileOkHttp(data)) + .build() + def response = okHttpClient.newCall(request).execute() + return response + }.call() as okhttp3.Response + if (!obj.isSuccessful()) { + def error = obj.body().string(); + log.error("Failed to upload multipart file to audit service : {}", error) + throw new OesRequestException(error) + } else{ + return obj.body()?.string() ?: "Unknown reason: " + obj.code() as Object + } + } + private okhttp3.RequestBody uploadFileOkHttp(MultipartFile multiPartfile) throws IOException { + String fileName = multiPartfile.getOriginalFilename(); + MultipartBody.Builder builder = new MultipartBody.Builder(); + builder.setType(MultipartBody.FORM); + builder.addFormDataPart("file", fileName, new okhttp3.RequestBody() { + @Override + public okhttp3.MediaType contentType() { + return okhttp3.MediaType.parse("application/octet-stream"); + } + @Override + public void writeTo(BufferedSink sink) throws IOException { + try { + Source source = Okio.source(multiPartfile.getInputStream()); + Buffer buf = new Buffer(); + long totalRead = 0; + long totalSize = multiPartfile.getSize(); + long remaining = totalSize; + for (long readCount; (readCount = source.read(buf, 32000)) != -1;) { + totalRead += readCount; + remaining -= readCount; + sink.write(buf, readCount); + sink.flush(); + } + } catch (Exception e) { + e.printStackTrace(); + throw new OesRequestException("Failed to upload multipart file to audit service"); + } + } + }); + return builder.build(); + } + + } diff --git a/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/exception/RetrofitErrorHandler.groovy b/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/exception/RetrofitErrorHandler.groovy index 16db895466..3ccdf6d00e 100644 --- a/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/exception/RetrofitErrorHandler.groovy +++ b/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/exception/RetrofitErrorHandler.groovy @@ -22,6 +22,8 @@ import com.netflix.spinnaker.gate.controllers.OpsmxDashboardController import com.netflix.spinnaker.gate.controllers.OpsmxOesController import com.netflix.spinnaker.gate.controllers.OpsmxPlatformController import com.netflix.spinnaker.gate.controllers.OpsmxVisibilityController +import com.netflix.spinnaker.gate.controllers.PipelineController +import com.netflix.spinnaker.gate.exceptions.OesRequestException import com.opsmx.spinnaker.gate.controllers.OpsmxAuditClientServiceController import com.opsmx.spinnaker.gate.controllers.OpsmxAuditServiceController import com.opsmx.spinnaker.gate.controllers.OpsmxSaporPolicyController @@ -45,12 +47,14 @@ class RetrofitErrorHandler { @ExceptionHandler([RetrofitError.class]) @ResponseBody ResponseEntity handleRetrofitError(RetrofitError retrofitError){ if (retrofitError!=null){ - log.warn("Exception occurred in OES downstream services : {}", retrofitError.getMessage()) + log.warn("Exception occurred in OES downstream services : {}", retrofitError.getBody()) if (retrofitError.getKind() == RetrofitError.Kind.NETWORK){ + log.warn("Retrofit Exception occurred of kind Network : {}", retrofitError.getBody()) ErrorResponseModel networkErrorResponse = populateNetworkErrorResponse(retrofitError) return new ResponseEntity(networkErrorResponse, HttpStatus.INTERNAL_SERVER_ERROR) } if (retrofitError.getResponse()!=null && retrofitError.getResponse().getStatus() > 0){ + log.warn("Exception occurred in : {}", retrofitError.getBody()) if (retrofitError.getResponse().getBody() !=null){ InputStream inputStream = null try { @@ -71,6 +75,36 @@ class RetrofitErrorHandler { return new ResponseEntity(defaultErrorResponse, HttpStatus.INTERNAL_SERVER_ERROR) } + @ExceptionHandler(PipelineController.PipelineException) + @ResponseBody + ResponseEntity> handlePipelineException(PipelineController.PipelineException ex) { + log.error("PipelineException occurred: {}", ex.message, ex) + Map response = createResponseMap(ex, "Pipeline Save Error") + + if (ex.additionalAttributes) { + response.put("additionalAttributes", ex.additionalAttributes) + } + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST) + } + + @ExceptionHandler(OesRequestException) + @ResponseBody + ResponseEntity> handleOesRequestException(OesRequestException ex) { + log.error("OesRequestException occurred: {}", ex.message, ex) + Map response = createResponseMap(ex, "OES Request Exception") + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST) + } + + private Map createResponseMap(Exception ex, String errorMessage) { + Map response = [:] + response.put("error", errorMessage) + response.put("message", ex.message) + response.put("status", HttpStatus.BAD_REQUEST.value()) + response.put("timestamp", System.currentTimeMillis()) + + return response + } + private ErrorResponseModel populateDefaultErrorResponseModel() { ErrorResponseModel defaultErrorResponse = new ErrorResponseModel() diff --git a/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/services/OpsmxAuditClientService.groovy b/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/services/OpsmxAuditClientService.groovy index 8fa3f52681..fc93cab062 100644 --- a/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/services/OpsmxAuditClientService.groovy +++ b/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/services/OpsmxAuditClientService.groovy @@ -16,7 +16,6 @@ package com.opsmx.spinnaker.gate.services -import org.springframework.web.bind.annotation.RequestParam import retrofit.client.Response import retrofit.http.GET import retrofit.http.Path @@ -133,4 +132,14 @@ interface OpsmxAuditClientService { @Query('endTime') Long endTime, @Query('days') Integer days, @Query('filterBy') String filterBy) + + @GET("/auditclientservice/v3/acctEnvMapping") + Object getAllAccountEnvironmentMappings() + + @GET("/auditclientservice/v3/acctEnvMapping/{id}") + Object getAccountEnvironmentMappingWithId(@Path('id') Integer id) + + @GET("/auditclientservice/v3/env") + Object getAllUniqueEnvironments() + } diff --git a/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/services/OpsmxAuditService.groovy b/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/services/OpsmxAuditService.groovy index ebf231461e..feb0d3692d 100644 --- a/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/services/OpsmxAuditService.groovy +++ b/gate-web/src/main/groovy/com/opsmx/spinnaker/gate/services/OpsmxAuditService.groovy @@ -17,7 +17,9 @@ package com.opsmx.spinnaker.gate.services import retrofit.http.Body +import retrofit.http.DELETE import retrofit.http.POST +import retrofit.http.PUT import retrofit.http.Path interface OpsmxAuditService { @@ -29,4 +31,13 @@ interface OpsmxAuditService { @Path('source1') String source1, @Body Object data) + @POST("/auditservice/v1/acctEnvMapping") + Object saveAccountEnvironmentMapping(@Body Object data) + + @PUT("/auditservice/v1/acctEnvMapping/{id}") + Object updateAccountEnvironmentMapping(@Path('id') Integer id, + @Body Object data) + + @DELETE("/auditservice/v1/acctEnvMapping/{id}") + Object deleteAccountEnvironmentMappingWithId(@Path('id') Integer id) } diff --git a/gate-web/src/main/java/com/opsmx/spinnaker/gate/config/OESSpinnakerVersionProperties.java b/gate-web/src/main/java/com/opsmx/spinnaker/gate/config/OESSpinnakerVersionProperties.java index f20b8ac67d..734628ae33 100644 --- a/gate-web/src/main/java/com/opsmx/spinnaker/gate/config/OESSpinnakerVersionProperties.java +++ b/gate-web/src/main/java/com/opsmx/spinnaker/gate/config/OESSpinnakerVersionProperties.java @@ -26,5 +26,5 @@ @Configuration @ConfigurationProperties("oes") public class OESSpinnakerVersionProperties { - private String spinnakerVersion=""; + private String spinnakerVersion = ""; } diff --git a/gate-web/src/main/java/com/opsmx/spinnaker/gate/controller/OESSpinnakerVersionController.java b/gate-web/src/main/java/com/opsmx/spinnaker/gate/controller/OESSpinnakerVersionController.java index df6fe92dfe..b8402b3691 100644 --- a/gate-web/src/main/java/com/opsmx/spinnaker/gate/controller/OESSpinnakerVersionController.java +++ b/gate-web/src/main/java/com/opsmx/spinnaker/gate/controller/OESSpinnakerVersionController.java @@ -30,13 +30,12 @@ @RestController @RequestMapping(value = "/oes") public class OESSpinnakerVersionController { - @Autowired(required = false) - private OESSpinnakerVersionProperties oesSpinnakerVersionProperties; - - @GetMapping(value = "/spinnakerVersion", produces = MediaType.APPLICATION_JSON_VALUE) - public ResponseEntity getOESSpinnakerVersion() { - log.debug("Get OES Spinnaker Version API invoked"); - return new ResponseEntity<>(oesSpinnakerVersionProperties.getSpinnakerVersion(), HttpStatus.OK); - } + @Autowired(required = false) + private OESSpinnakerVersionProperties oesSpinnakerVersionProperties; + @GetMapping(value = "/spinnakerVersion", produces = MediaType.APPLICATION_JSON_VALUE) + public ResponseEntity getOESSpinnakerVersion() { + log.debug("Get OES Spinnaker Version API invoked"); + return new ResponseEntity<>(oesSpinnakerVersionProperties.getSpinnakerVersion(), HttpStatus.OK); + } } diff --git a/gate-web/src/main/java/com/opsmx/spinnaker/gate/rbac/ApplicationFeatureRbac.java b/gate-web/src/main/java/com/opsmx/spinnaker/gate/rbac/ApplicationFeatureRbac.java index 4598cfbe30..3046c549a0 100644 --- a/gate-web/src/main/java/com/opsmx/spinnaker/gate/rbac/ApplicationFeatureRbac.java +++ b/gate-web/src/main/java/com/opsmx/spinnaker/gate/rbac/ApplicationFeatureRbac.java @@ -18,6 +18,8 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; +import com.netflix.spinnaker.fiat.model.Authorization; +import com.netflix.spinnaker.fiat.shared.FiatPermissionEvaluator; import com.netflix.spinnaker.gate.model.PermissionModel; import com.netflix.spinnaker.gate.services.OesAuthorizationService; import com.netflix.spinnaker.gate.services.PermissionService; @@ -27,14 +29,13 @@ import com.opsmx.spinnaker.gate.exception.InvalidResourceIdException; import com.opsmx.spinnaker.gate.exception.XSpinnakerUserHeaderMissingException; import jakarta.servlet.http.HttpServletRequest; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; +import java.util.*; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; @Slf4j @@ -46,6 +47,8 @@ public class ApplicationFeatureRbac { @Autowired private PermissionService permissionService; + @Autowired private FiatPermissionEvaluator permissionEvaluator; + public static final List runtime_access = new ArrayList<>(); public static final List applicationFeatureRbacEndpoints = new ArrayList<>(); public static final List endpointsWithApplicationId = new ArrayList<>(); @@ -77,19 +80,24 @@ public void authorizeUserForFeatureVisibility(String userName) { log.debug("Start of the authorizeUserForFeatureVisibility"); log.debug("validating the user for FeatureVisibility"); if (permissionService.isAdmin(userName)) { - log.info("{} is admin, Hence not validating with ISD", userName); + log.info("{} user is admin, Hence not validating with ISD", userName); return; } - isFeatureVisibility = - Boolean.parseBoolean( - oesAuthorizationService - .isFeatureVisibility(userName, RbacFeatureType.APP.name(), userName) - .getBody() - .get("isEnabled")); - log.info("is feature visibility enabled : {}", isFeatureVisibility); - if (!isFeatureVisibility) { + + ResponseEntity> response = + oesAuthorizationService.isFeatureVisibility(userName, RbacFeatureType.APP.name(), userName); + int statusCode = response.getStatusCodeValue(); + isFeatureVisibility = Boolean.parseBoolean(response.getBody().get("isEnabled")); + + if (statusCode == HttpStatus.NOT_FOUND.value() || statusCode == HttpStatus.OK.value()) { + log.info("Feature visibility check ended with status code: {}", statusCode); + } else if (statusCode == HttpStatus.FORBIDDEN.value() || !isFeatureVisibility) { + log.warn( + "Access forbidden for user {} on feature type {}", + userName, + RbacFeatureType.APP.description); throw new AccessForbiddenException( - "You do not have permission for the feature type : " + RbacFeatureType.APP.description); + "You do not have permission for the feature type: " + RbacFeatureType.APP.description); } log.debug("End of the authorizeUserForFeatureVisibility"); } @@ -99,7 +107,7 @@ public void authorizeUserForApplicationId( log.debug("Start of the authorizeUserForApplicationId"); log.debug("validating the user for ApplicationId"); if (permissionService.isAdmin(username)) { - log.info("{} is admin, Hence not validating with ISD", username); + log.info("{} user is admin,Hence not validating with ISD", username); return; } HttpMethod method = HttpMethod.valueOf(httpMethod); @@ -110,13 +118,23 @@ public void authorizeUserForApplicationId( switch (method.name()) { case "GET": - permission = - oesAuthorizationService - .fetchPermissions(username, RbacFeatureType.APP.name(), applicationId, username) - .getBody(); - log.info("permissions for the GET API : {}", permission); - if (permission == null - || !permission.getPermissions().contains(PermissionEnum.view.name())) { + ResponseEntity response = + oesAuthorizationService.fetchPermissions( + username, RbacFeatureType.APP.name(), applicationId, username); + int statusCode = response.getStatusCodeValue(); + permission = response.getBody(); + + if ((statusCode == HttpStatus.NOT_FOUND.value() || statusCode == HttpStatus.OK.value()) + && permissionEvaluator.hasPermission( + username, applicationId, RbacFeatureType.APP.name(), Authorization.EXECUTE)) { + log.info( + "Authorized user for Application ID with status code :{}, {}", + applicationId, + statusCode); + log.info("permissions for the GET API : {}", permission); + } else if (permission == null + || !permission.getPermissions().contains(PermissionEnum.view.name()) + || statusCode == HttpStatus.FORBIDDEN.value()) { throw new AccessForbiddenException( YOU_DO_NOT_HAVE + PermissionEnum.view.name() @@ -205,24 +223,29 @@ public void authorizeUserForServiceId(String username, String endpointUrl, Strin switch (method.name()) { case "GET": - isAuthorized = - Boolean.parseBoolean( - oesAuthorizationService - .isAuthorizedUser( - username, - PermissionEnum.view.name(), - serviceId, - null, - null, - null, - null, - null, - null, - username) - .getBody() - .get("isEnabled")); - log.info("is authorized for the service Id GET API: {}, {}", serviceId, isAuthorized); - if (isAuthorized == null || !isAuthorized) { + ResponseEntity> response = + oesAuthorizationService.isAuthorizedUser( + username, + PermissionEnum.view.name(), + serviceId, + null, + null, + null, + null, + null, + null, + username); + isAuthorized = Boolean.parseBoolean(response.getBody().get("isEnabled")); + int statusCode = response.getStatusCodeValue(); + + if (statusCode == HttpStatus.NOT_FOUND.value() || statusCode == HttpStatus.OK.value()) { + log.info( + "Authorized user for the service Id GET API with status code: {}, {}", + serviceId, + statusCode); + } else if (isAuthorized == null + || !isAuthorized + || statusCode == HttpStatus.FORBIDDEN.value()) { throw new AccessForbiddenException( YOU_DO_NOT_HAVE + PermissionEnum.view.name() @@ -298,24 +321,29 @@ public void authorizeUserForPipelineId(String username, String endpointUrl, Stri switch (method.name()) { case "GET": - isAuthorized = - Boolean.parseBoolean( - oesAuthorizationService - .isAuthorizedUser( - username, - PermissionEnum.view.name(), - null, - pipelineId, - null, - null, - null, - null, - null, - username) - .getBody() - .get("isEnabled")); - log.info("is authorized for the pipeline Id GET API: {}, {}", pipelineId, isAuthorized); - if (isAuthorized == null || !isAuthorized) { + ResponseEntity> response = + oesAuthorizationService.isAuthorizedUser( + username, + PermissionEnum.view.name(), + null, + pipelineId, + null, + null, + null, + null, + null, + username); + isAuthorized = Boolean.parseBoolean(response.getBody().get("isEnabled")); + int statusCode = response.getStatusCodeValue(); + + if (statusCode == HttpStatus.NOT_FOUND.value() || statusCode == HttpStatus.OK.value()) { + log.info( + "authorized user for the pipeline Id GET API with status code: {}, {}", + pipelineId, + statusCode); + } else if (isAuthorized == null + || !isAuthorized + || statusCode == HttpStatus.FORBIDDEN.value()) { throw new AccessForbiddenException( YOU_DO_NOT_HAVE + PermissionEnum.view.name() @@ -393,24 +421,29 @@ public void authorizeUserForGateId(String username, String endpointUrl, String h switch (method.name()) { case "GET": - isAuthorized = - Boolean.parseBoolean( - oesAuthorizationService - .isAuthorizedUser( - username, - PermissionEnum.view.name(), - null, - null, - gateId, - null, - null, - null, - null, - username) - .getBody() - .get("isEnabled")); - log.info("is authorized for the gate Id GET API: {}, {}", gateId, isAuthorized); - if (isAuthorized == null || !isAuthorized) { + ResponseEntity> response = + oesAuthorizationService.isAuthorizedUser( + username, + PermissionEnum.view.name(), + null, + null, + gateId, + null, + null, + null, + null, + username); + isAuthorized = Boolean.parseBoolean(response.getBody().get("isEnabled")); + int statusCode = response.getStatusCodeValue(); + + if (statusCode == HttpStatus.NOT_FOUND.value() || statusCode == HttpStatus.OK.value()) { + log.info( + "authorized user for the gate Id GET API with staus code: {}, {}", + gateId, + statusCode); + } else if (isAuthorized == null + || !isAuthorized + || statusCode == HttpStatus.FORBIDDEN.value()) { throw new AccessForbiddenException( YOU_DO_NOT_HAVE + PermissionEnum.view.name() @@ -492,25 +525,29 @@ public void authorizeUserForApprovalGateId( switch (method.name()) { case "GET": - isAuthorized = - Boolean.parseBoolean( - oesAuthorizationService - .isAuthorizedUser( - username, - PermissionEnum.view.name(), - null, - null, - null, - approvalGateId, - null, - null, - null, - username) - .getBody() - .get("isEnabled")); - log.info( - "is authorized for the approval gate Id GET API: {}, {}", approvalGateId, isAuthorized); - if (isAuthorized == null || !isAuthorized) { + ResponseEntity> response = + oesAuthorizationService.isAuthorizedUser( + username, + PermissionEnum.view.name(), + null, + null, + null, + approvalGateId, + null, + null, + null, + username); + isAuthorized = Boolean.parseBoolean(response.getBody().get("isEnabled")); + int statusCode = response.getStatusCodeValue(); + + if (statusCode == HttpStatus.NOT_FOUND.value() || statusCode == HttpStatus.OK.value()) { + log.info( + "authorized user for the approval gate Id GET API: {}, {}", + approvalGateId, + statusCode); + } else if (isAuthorized == null + || !isAuthorized + || statusCode == HttpStatus.FORBIDDEN.value()) { throw new AccessForbiddenException( YOU_DO_NOT_HAVE + PermissionEnum.view.name() @@ -586,27 +623,29 @@ public void authorizeUserForApprovalGateInstanceId( switch (method.name()) { case "GET": - isAuthorized = - Boolean.parseBoolean( - oesAuthorizationService - .isAuthorizedUser( - username, - PermissionEnum.view.name(), - null, - null, - null, - null, - approvalGateInstanceId, - null, - null, - username) - .getBody() - .get("isEnabled")); - log.info( - "is authorized for the approval gate instance Id GET API: {}, {}", - approvalGateInstanceId, - isAuthorized); - if (isAuthorized == null || !isAuthorized) { + ResponseEntity> response = + oesAuthorizationService.isAuthorizedUser( + username, + PermissionEnum.view.name(), + null, + null, + null, + null, + approvalGateInstanceId, + null, + null, + username); + isAuthorized = Boolean.parseBoolean(response.getBody().get("isEnabled")); + int statusCode = response.getStatusCodeValue(); + + if (statusCode == HttpStatus.NOT_FOUND.value() || statusCode == HttpStatus.OK.value()) { + log.info( + "authorized user for the approval gate instance Id GET API: {}, {}", + approvalGateInstanceId, + statusCode); + } else if (isAuthorized == null + || !isAuthorized + || statusCode == HttpStatus.FORBIDDEN.value()) { throw new AccessForbiddenException( YOU_DO_NOT_HAVE + PermissionEnum.view.name() @@ -682,27 +721,29 @@ public void authorizeUserForApprovalPolicyId( switch (method.name()) { case "GET": - isAuthorized = - Boolean.parseBoolean( - oesAuthorizationService - .isAuthorizedUser( - username, - PermissionEnum.view.name(), - null, - null, - null, - null, - null, - approvalPolicyId, - null, - username) - .getBody() - .get("isEnabled")); - log.info( - "is authorized for the approval policy Id GET API: {}, {}", - approvalPolicyId, - isAuthorized); - if (isAuthorized == null || !isAuthorized) { + ResponseEntity> response = + oesAuthorizationService.isAuthorizedUser( + username, + PermissionEnum.view.name(), + null, + null, + null, + null, + null, + approvalPolicyId, + null, + username); + isAuthorized = Boolean.parseBoolean(response.getBody().get("isEnabled")); + int statusCode = response.getStatusCodeValue(); + + if (statusCode == HttpStatus.NOT_FOUND.value() || statusCode == HttpStatus.OK.value()) { + log.info( + "authorized user for the approval policy Id GET API with staus code: {}, {}", + approvalPolicyId, + statusCode); + } else if (isAuthorized == null + || !isAuthorized + || statusCode == HttpStatus.FORBIDDEN.value()) { throw new AccessForbiddenException( YOU_DO_NOT_HAVE + PermissionEnum.view.name() @@ -791,24 +832,29 @@ public void authorizeUserForApprovalGateTrigger(HttpServletRequest request) { log.info("authorizing the endpoint : {}", endpointUrl); - isAuthorized = - Boolean.parseBoolean( - oesAuthorizationService - .isAuthorizedUser( - username, - PermissionEnum.runtime_access.name(), - null, - null, - null, - approvalGateId, - null, - null, - null, - username) - .getBody() - .get("isEnabled")); - - if (isAuthorized == null || !isAuthorized) { + ResponseEntity> response = + oesAuthorizationService.isAuthorizedUser( + username, + PermissionEnum.runtime_access.name(), + null, + null, + null, + approvalGateId, + null, + null, + null, + username); + isAuthorized = Boolean.parseBoolean(response.getBody().get("isEnabled")); + int statusCode = response.getStatusCodeValue(); + + if (statusCode == HttpStatus.NOT_FOUND.value() || statusCode == HttpStatus.OK.value()) { + log.info( + "authorized user for the approval custom gate trigger Id GET API with status code: {}, {}", + approvalGateId, + statusCode); + } else if (isAuthorized == null + || !isAuthorized + || statusCode == HttpStatus.FORBIDDEN.value()) { throw new AccessForbiddenException( YOU_DO_NOT_HAVE + PermissionEnum.runtime_access.name() @@ -846,24 +892,29 @@ public void authorizeUserForPolicyGateTrigger(HttpServletRequest request, Object log.info("authorizing the endpoint : {}", endpointUrl); - isAuthorized = - Boolean.parseBoolean( - oesAuthorizationService - .isAuthorizedUser( - username, - PermissionEnum.runtime_access.name(), - null, - null, - null, - null, - null, - null, - appName, - username) - .getBody() - .get("isEnabled")); - - if (isAuthorized == null || !isAuthorized) { + ResponseEntity> response = + oesAuthorizationService.isAuthorizedUser( + username, + PermissionEnum.runtime_access.name(), + null, + null, + null, + null, + null, + null, + appName, + username); + isAuthorized = Boolean.parseBoolean(response.getBody().get("isEnabled")); + int statusCode = response.getStatusCodeValue(); + + if (statusCode == HttpStatus.NOT_FOUND.value() || statusCode == HttpStatus.OK.value()) { + log.info( + "authorized user for the policy gate Id GET API with status code: {}, {}", + isAuthorized, + statusCode); + } else if (isAuthorized == null + || !isAuthorized + || statusCode == HttpStatus.FORBIDDEN.value()) { throw new AccessForbiddenException( YOU_DO_NOT_HAVE + PermissionEnum.runtime_access.name() @@ -898,24 +949,29 @@ public void authorizeUserForVerificationAndTestVerificationGateTrigger( log.info("authorizing the endpoint : {}", endpointUrl); - isAuthorized = - Boolean.parseBoolean( - oesAuthorizationService - .isAuthorizedUser( - username, - PermissionEnum.runtime_access.name(), - null, - null, - null, - null, - null, - null, - appName, - username) - .getBody() - .get("isEnabled")); - - if (isAuthorized == null || !isAuthorized) { + ResponseEntity> response = + oesAuthorizationService.isAuthorizedUser( + username, + PermissionEnum.runtime_access.name(), + null, + null, + null, + null, + null, + null, + appName, + username); + isAuthorized = Boolean.parseBoolean(response.getBody().get("isEnabled")); + int statusCode = response.getStatusCodeValue(); + + if (statusCode == HttpStatus.NOT_FOUND.value() || statusCode == HttpStatus.OK.value()) { + log.info( + "is authorized for the verification gate Id GET API with status code: {}, {}", + isAuthorized, + statusCode); + } else if (isAuthorized == null + || !isAuthorized + || statusCode == HttpStatus.FORBIDDEN.value()) { throw new AccessForbiddenException( YOU_DO_NOT_HAVE + PermissionEnum.runtime_access.name()