diff --git a/.github/workflows/gradle-build-development.yml b/.github/workflows/gradle-build-development.yml index 59d500c301..f155759711 100644 --- a/.github/workflows/gradle-build-development.yml +++ b/.github/workflows/gradle-build-development.yml @@ -98,16 +98,7 @@ jobs: --set-env-vars "OAUTH_CLIENT_SECRET=${{ secrets.OAUTH_CLIENT_SECRET }}" \ --set-env-vars "OAUTH_CALLBACK_URI"=${{ secrets.OAUTH_CALLBACK_URI }} \ --set-env-vars "DIRECTORY_ID=${{ secrets.DIRECTORY_ID }}" \ - --set-env-vars "TYPE=${{ secrets.SA_KEY_TYPE }}" \ - --set-env-vars "PROJECT_ID=${{ secrets.RUN_PROJECT }}" \ - --set-env-vars "PRIVATE_KEY_ID=${{ secrets.SA_PRIVATE_KEY_ID }}" \ - --set-env-vars "PRIVATE_KEY=${{ secrets.SA_PRIVATE_KEY }}" \ - --set-env-vars "CLIENT_EMAIL=${{ secrets.SA_CLIENT_EMAIL }}" \ - --set-env-vars "CLIENT_ID=${{ secrets.SA_CLIENT_ID }}" \ - --set-env-vars "AUTH_URI=${{ secrets.SA_AUTH_URI }}" \ - --set-env-vars "TOKEN_URI=${{ secrets.SA_TOKEN_URI }}" \ - --set-env-vars "AUTH_PROVIDER_X509_CERT_URL=${{ secrets.SA_AUTH_PROVIDER_X509_CERT_URL }}" \ - --set-env-vars "CLIENT_X509_CERT_URL=${{ secrets.SA_CLIENT_X509_CERT_URL }}" \ + --set-env-vars "SERVICE_ACCOUNT_CREDENTIALS=${{ secrets.SERVICE_ACCOUNT_CREDENTIALS }}" \ --set-env-vars "GSUITE_SUPER_ADMIN=${{ secrets.GSUITE_SUPER_ADMIN }}" \ --set-env-vars "MJ_APIKEY_PUBLIC=${{ secrets.MJ_APIKEY_PUBLIC }}" \ --set-env-vars "MJ_APIKEY_PRIVATE=${{ secrets.MJ_APIKEY_PRIVATE }}" \ diff --git a/server/src/main/java/com/objectcomputing/checkins/security/GoogleServiceConfiguration.java b/server/src/main/java/com/objectcomputing/checkins/security/GoogleServiceConfiguration.java index 5a14105454..5a56b2ec5f 100644 --- a/server/src/main/java/com/objectcomputing/checkins/security/GoogleServiceConfiguration.java +++ b/server/src/main/java/com/objectcomputing/checkins/security/GoogleServiceConfiguration.java @@ -1,171 +1,88 @@ package com.objectcomputing.checkins.security; import io.micronaut.context.annotation.ConfigurationProperties; -import io.micronaut.core.annotation.Introspected; +import io.micronaut.context.annotation.Factory; +import io.micronaut.core.type.Argument; +import io.micronaut.json.JsonMapper; +import io.micronaut.validation.validator.constraints.ConstraintValidator; +import jakarta.inject.Singleton; +import jakarta.validation.Constraint; import jakarta.validation.constraints.NotNull; - +import lombok.Getter; +import lombok.Setter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Base64; +import java.util.Map; +import java.util.stream.Stream; + +@Getter +@Setter @ConfigurationProperties("service-account-credentials") -@Introspected public class GoogleServiceConfiguration { - @NotNull - public String directory_id; - - @NotNull - public String type; - - @NotNull - public String project_id; - - @NotNull - public String private_key_id; - - @NotNull - public String private_key; - - @NotNull - public String client_email; - - @NotNull - public String client_id; - - @NotNull - public String auth_uri; - - @NotNull - public String token_uri; - - @NotNull - public String auth_provider_x509_cert_url; + private static final Logger LOG = LoggerFactory.getLogger(GoogleServiceConfiguration.class); @NotNull - public String client_x509_cert_url; - - @NotNull - public String oauth_client_id; - - @NotNull - public String oauth_client_secret; - - public String getDirectory_id() { - return directory_id; - } - - public void setDirectory_id(String directory_id) { - this.directory_id = directory_id; - } - - public String getType() { - return type; - } - - public void setType(String type) { - this.type = type; - } - - public String getProject_id() { - return project_id; - } - - public void setProject_id(String project_id) { - this.project_id = project_id; - } - - public String getPrivate_key_id() { - return private_key_id; - } - - public void setPrivate_key_id(String private_key_id) { - this.private_key_id = private_key_id; - } - - public String getPrivate_key() { - return private_key; - } - - public void setPrivate_key(String private_key) { - this.private_key = private_key; - } - - public String getClient_email() { - return client_email; - } - - public void setClient_email(String client_email) { - this.client_email = client_email; - } - - public String getClient_id() { - return client_id; - } - - public void setClient_id(String client_id) { - this.client_id = client_id; - } - - public String getAuth_uri() { - return auth_uri; - } - - public void setAuth_uri(String auth_uri) { - this.auth_uri = auth_uri; - } - - public String getToken_uri() { - return token_uri; - } - - public void setToken_uri(String token_uri) { - this.token_uri = token_uri; - } - - public String getAuth_provider_x509_cert_url() { - return auth_provider_x509_cert_url; - } - - public void setAuth_provider_x509_cert_url(String auth_provider_x509_cert_url) { - this.auth_provider_x509_cert_url = auth_provider_x509_cert_url; - } - - public String getClient_x509_cert_url() { - return client_x509_cert_url; - } - - public void setClient_x509_cert_url(String client_x509_cert_url) { - this.client_x509_cert_url = client_x509_cert_url; - } - - public String getOauth_client_id() { - return oauth_client_id; - } - - public void setOauth_client_id(String oauth_client_id) { - this.oauth_client_id = oauth_client_id; - } - - public String getOauth_client_secret() { - return oauth_client_secret; - } - - public void setOauth_client_secret(String oauth_client_secret) { - this.oauth_client_secret = oauth_client_secret; - } - - public String toString() { - return "{" + - "\"directory_id\":\"" + directory_id + - "\", \"type\":\"" + type + - "\", \"project_id\":\"" + project_id + - "\", \"private_key_id\":\"" + private_key_id + - "\", \"private_key\":\"" + private_key + - "\", \"client_email\":\"" + client_email + - "\", \"client_id\":\"" + client_id + - "\", \"auth_uri\":\"" + auth_uri + - "\", \"token_uri\":\"" + token_uri + - "\", \"auth_provider_x509_cert_url\":\"" + auth_provider_x509_cert_url + - "\", \"client_x509_cert_url\":\"" + client_x509_cert_url + - "\", \"oauth_client_id\":\"" + oauth_client_id + - "\", \"oauth_client_secret\":\"" + oauth_client_secret + - "\"}"; + private String directoryId; + + @ValidEncodedGoogleServiceConfiguration + private String encodedValue; + + @Target(ElementType.FIELD) + @Retention(RetentionPolicy.RUNTIME) + @Documented + @Constraint(validatedBy = {}) + @interface ValidEncodedGoogleServiceConfiguration { + } + + @Factory + static class CustomValidationFactory { + + private final JsonMapper jsonMapper; + private static final Base64.Decoder DECODER = Base64.getDecoder(); + + CustomValidationFactory(JsonMapper jsonMapper) { + this.jsonMapper = jsonMapper; + } + + @Singleton + ConstraintValidator e164Validator() { + return (value, annotation, context) -> { + if (value == null || !isValid(value)) { + context.buildConstraintViolationWithTemplate("must be a valid base64 encoded Google Service Configuration") + .addConstraintViolation(); + } + return true; + }; + } + + // Check the decoded json string for the required fields + private boolean isValid(String value) { + try { + Map map = jsonMapper.readValue(DECODER.decode(value), Argument.mapOf(String.class, Object.class)); + return Stream.of( + "type", + "project_id", + "private_key_id", + "private_key", + "client_email", + "client_id", + "auth_uri", + "token_uri", + "auth_provider_x509_cert_url", + "client_x509_cert_url" + ).allMatch(map::containsKey); + } catch (Exception e) { + LOG.error("An error occurred while decoding the Google Service Configuration.", e); + } + return false; + } } } diff --git a/server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesImpl.java b/server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesImpl.java index 43883ba2c7..3e2947f24b 100644 --- a/server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesImpl.java +++ b/server/src/main/java/com/objectcomputing/checkins/services/file/FileServicesImpl.java @@ -69,7 +69,7 @@ public Set findFiles(@Nullable UUID checkInID) { Drive drive = googleApiAccess.getDrive(); validate(drive == null, "Unable to access Google Drive"); - String rootDirId = googleServiceConfiguration.getDirectory_id(); + String rootDirId = googleServiceConfiguration.getDirectoryId(); validate(rootDirId == null, "No destination folder has been configured. Contact your administrator for assistance."); if (checkInID == null && isAdmin) { @@ -166,7 +166,7 @@ public FileInfoDTO uploadFile(@NotNull UUID checkInID, @NotNull CompletedFileUpl Drive drive = googleApiAccess.getDrive(); validate(drive == null, "Unable to access Google Drive"); - String rootDirId = googleServiceConfiguration.getDirectory_id(); + String rootDirId = googleServiceConfiguration.getDirectoryId(); validate(rootDirId == null, "No destination folder has been configured. Contact your administrator for assistance."); // Check if folder already exists on google drive. If exists, return folderId and name diff --git a/server/src/main/java/com/objectcomputing/checkins/util/googleapiaccess/GoogleAuthenticator.java b/server/src/main/java/com/objectcomputing/checkins/util/googleapiaccess/GoogleAuthenticator.java index 23aba94172..ad72383a45 100644 --- a/server/src/main/java/com/objectcomputing/checkins/util/googleapiaccess/GoogleAuthenticator.java +++ b/server/src/main/java/com/objectcomputing/checkins/util/googleapiaccess/GoogleAuthenticator.java @@ -11,10 +11,9 @@ import org.slf4j.LoggerFactory; import java.io.ByteArrayInputStream; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.nio.charset.StandardCharsets; +import java.util.Base64; import java.util.List; @Requires(notEnv = Environment.TEST) @@ -22,14 +21,17 @@ public class GoogleAuthenticator { private static final Logger LOG = LoggerFactory.getLogger(GoogleAuthenticator.class); + private static final Base64.Decoder BASE64_DECODER = Base64.getDecoder(); - private GoogleServiceConfiguration gServiceConfig; + private final GoogleServiceConfiguration gServiceConfig; /** * Creates a google drive utility for quick access * @param gServiceConfig, Google Drive configuration properties */ - public GoogleAuthenticator(GoogleServiceConfiguration gServiceConfig) { + public GoogleAuthenticator( + GoogleServiceConfiguration gServiceConfig + ) { this.gServiceConfig = gServiceConfig; } @@ -40,14 +42,8 @@ public GoogleAuthenticator(GoogleServiceConfiguration gServiceConfig) { * @throws IOException If the service account configurations cannot be found. */ GoogleCredentials setupCredentials(@NotNull final List scopes) throws IOException { - InputStream in = new ByteArrayInputStream(gServiceConfig.toString().getBytes(StandardCharsets.UTF_8)); + InputStream in = gcpCredentialsStream(); GoogleCredentials credentials = GoogleCredentials.fromStream(in); - - if (credentials == null) { - credentials = GoogleCredentials.getApplicationDefault(); - throw new FileNotFoundException("Credentials not found while using Google default credentials"); - } - return scopes.isEmpty() ? credentials : credentials.createScoped(scopes); } @@ -56,12 +52,11 @@ GoogleCredentials setupCredentials(@NotNull final List scopes) throws IO * @param scopes, the scope(s) of access to request for this application * @param delegatedUser, the email of the delegated user * @return An authorized ServiceAccountCredentials object. - * @throws IOException If the service account configurations cannot be found. */ - ServiceAccountCredentials setupServiceAccountCredentials(@NotNull final List scopes, @NotNull final String delegatedUser) throws IOException { - + ServiceAccountCredentials setupServiceAccountCredentials(@NotNull final List scopes, @NotNull final String delegatedUser) { ServiceAccountCredentials sourceCredentials = null; - try(InputStream in = new ByteArrayInputStream(gServiceConfig.toString().getBytes(StandardCharsets.UTF_8))) { + try { + InputStream in = gcpCredentialsStream(); sourceCredentials = ServiceAccountCredentials.fromStream(in); sourceCredentials = (ServiceAccountCredentials) sourceCredentials.createScoped(scopes).createDelegated(delegatedUser); } catch (IOException e) { @@ -69,5 +64,9 @@ ServiceAccountCredentials setupServiceAccountCredentials(@NotNull final List> violations = validator.validate(googleServiceConfiguration); - assertEquals(13, violations.size()); - for (ConstraintViolation violation : violations) { - assertEquals("must not be null", violation.getMessage()); - } + assertEquals(2, violations.size()); + assertEquals( + List.of("directoryId:must not be null", "encodedValue:must be a valid base64 encoded Google Service Configuration"), + violations.stream().map(v -> v.getPropertyPath() + ":" + v.getMessage()).toList() + ); } @Test - void testPopulate() { + void testConstraintViolationPasses() { GoogleServiceConfiguration googleServiceConfiguration = new GoogleServiceConfiguration(); - - googleServiceConfiguration.setDirectory_id("some.directory.id"); - assertEquals("some.directory.id", googleServiceConfiguration.getDirectory_id()); - - googleServiceConfiguration.setType("some.type"); - assertEquals("some.type", googleServiceConfiguration.getType()); - - googleServiceConfiguration.setProject_id("some.project.id"); - assertEquals("some.project.id", googleServiceConfiguration.getProject_id()); - - googleServiceConfiguration.setPrivate_key_id("some.private.key.id"); - assertEquals("some.private.key.id", googleServiceConfiguration.getPrivate_key_id()); - - googleServiceConfiguration.setPrivate_key("some.private.key"); - assertEquals("some.private.key", googleServiceConfiguration.getPrivate_key()); - - googleServiceConfiguration.setClient_email("some.client.email"); - assertEquals("some.client.email", googleServiceConfiguration.getClient_email()); - - googleServiceConfiguration.setClient_id("some.client.id"); - assertEquals("some.client.id", googleServiceConfiguration.getClient_id()); - - googleServiceConfiguration.setAuth_uri("some.auth.uri"); - assertEquals("some.auth.uri", googleServiceConfiguration.getAuth_uri()); - - googleServiceConfiguration.setToken_uri("some.token.uri"); - assertEquals("some.token.uri", googleServiceConfiguration.getToken_uri()); - - googleServiceConfiguration.setAuth_provider_x509_cert_url("some.auth.provider"); - assertEquals("some.auth.provider", googleServiceConfiguration.getAuth_provider_x509_cert_url()); - - googleServiceConfiguration.setClient_x509_cert_url("some.cert.url"); - assertEquals("some.cert.url", googleServiceConfiguration.getClient_x509_cert_url()); - - googleServiceConfiguration.setOauth_client_id("some.client.id"); - assertEquals("some.client.id", googleServiceConfiguration.getClient_id()); - - googleServiceConfiguration.setOauth_client_secret("some.client.secret"); - assertEquals("some.client.secret", googleServiceConfiguration.getOauth_client_secret()); - - String toString = googleServiceConfiguration.toString(); - assertTrue(toString.contains("some.directory.id")); - assertTrue(toString.contains("some.type")); - assertTrue(toString.contains("some.project.id")); - assertTrue(toString.contains("some.private.key.id")); - assertTrue(toString.contains("some.private.key")); - assertTrue(toString.contains("some.client.email")); - assertTrue(toString.contains("some.client.id")); - assertTrue(toString.contains("some.auth.uri")); - assertTrue(toString.contains("some.token.uri")); - assertTrue(toString.contains("some.auth.provider")); - assertTrue(toString.contains("some.cert.url")); - assertTrue(toString.contains("some.client.id")); - assertTrue(toString.contains("some.client.secret")); + googleServiceConfiguration.setDirectoryId("some.directory.id"); + googleServiceConfiguration.setEncodedValue(ENCODED_EXAMPLE_GOOGLE_SERVICE_CONFIGURATION); Set> violations = validator.validate(googleServiceConfiguration); - assertTrue(violations.isEmpty()); + assertEquals(0, violations.size()); + } + + @Test + void testConfigurationLoadedCorrectlyFromConfiguration() throws IOException { + var values = Map.ofEntries( + Map.entry("directory", "some.directory.id"), + Map.entry("encoded-gcp-credentials", ENCODED_EXAMPLE_GOOGLE_SERVICE_CONFIGURATION) + ); + try (var ctx = ApplicationContext.run(Map.ofEntries( + Map.entry("datasources.enabled", false), + Map.entry("service-account-credentials.directory-id", values.get("directory")), + Map.entry("service-account-credentials.encoded-value", values.get("encoded-gcp-credentials")) + ))) { + var cfg = ctx.getBean(GoogleServiceConfiguration.class); + + assertNotNull(cfg.getDirectoryId()); + assertEquals(values.get("directory"), cfg.getDirectoryId()); + + assertNotNull(cfg.getEncodedValue()); + assertEquals(values.get("encoded-gcp-credentials"), cfg.getEncodedValue()); + } } } diff --git a/server/src/test/java/com/objectcomputing/checkins/services/file/FileServicesImplTest.java b/server/src/test/java/com/objectcomputing/checkins/services/file/FileServicesImplTest.java index 4a2498aa54..4efc3852d6 100644 --- a/server/src/test/java/com/objectcomputing/checkins/services/file/FileServicesImplTest.java +++ b/server/src/test/java/com/objectcomputing/checkins/services/file/FileServicesImplTest.java @@ -177,7 +177,7 @@ void resetMocks() { when(mockAttributes.get("email")).thenReturn(mockAttributes); when(mockAttributes.toString()).thenReturn("test.email"); when(currentUserServices.findOrSaveUser(any(), any(), any())).thenReturn(testMemberProfile); - when(googleServiceConfiguration.getDirectory_id()).thenReturn("testDirectoryId"); + when(googleServiceConfiguration.getDirectoryId()).thenReturn("testDirectoryId"); } @Test