diff --git a/doc/site/sources/docs/configuration-options.md b/doc/site/sources/docs/configuration-options.md index 723970978..f95dbec74 100644 --- a/doc/site/sources/docs/configuration-options.md +++ b/doc/site/sources/docs/configuration-options.md @@ -287,6 +287,43 @@ The following table lists all available configuration options. 1.2 + + SMTP_ADDRESS_MAPPING + +

+ CEL expression for deriving + a user's email address from their Cloud Identity/Workspace user ID. +

+

+ By default, JIT Accesses uses the Cloud Identity/Workspace user ID (such as alice@example.com) as + email address to deliver notifications to. If some or all of your Cloud Identity/Workspace user IDs + do not correspond to valid email addresses, use this setting to specify a CEL expression that derives a valid email address. +

+

+ CEL expressions can use standard functions + and the extract() function. +

+

+ For example, the following expression replaces the domain example.com with test.example.com for all users: +

+

+ user.email.extract('{handle}@example.com') + '@test.example.com' +

+

+ If you're using multiple domains and only need to substitute one of them, you can use conditional + statements. For example: +

+

+ user.email.endsWith('@external.example.com') + ? user.email.extract('{handle}@external.example.com') + '@otherdomain.example' + : user.email + +

+ + Optional + + 1.7 + ## Notifications diff --git a/sources/src/main/java/com/google/solutions/jitaccess/cel/ExtractFunction.java b/sources/src/main/java/com/google/solutions/jitaccess/cel/ExtractFunction.java new file mode 100644 index 000000000..fa7ded17b --- /dev/null +++ b/sources/src/main/java/com/google/solutions/jitaccess/cel/ExtractFunction.java @@ -0,0 +1,78 @@ +// +// Copyright 2024 Google LLC +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.google.solutions.jitaccess.cel; + +import dev.cel.common.CelFunctionDecl; +import dev.cel.common.CelOverloadDecl; +import dev.cel.common.types.SimpleType; +import dev.cel.runtime.CelEvaluationException; +import dev.cel.runtime.CelRuntime; + +import java.util.List; + +/** + * Extract function as documented in + * https://cloud.google.com/iam/docs/conditions-attribute-reference#extract. + */ +public class ExtractFunction { + public static final CelFunctionDecl DECLARATION = + CelFunctionDecl.newFunctionDeclaration( + "extract", + CelOverloadDecl.newMemberOverload( + "extract_string_string", + SimpleType.STRING, + List.of(SimpleType.STRING, SimpleType.STRING))); + + public static final CelRuntime.CelFunctionBinding BINDING = + CelRuntime.CelFunctionBinding.from( + "extract_string_string", + String.class, + String.class, + ExtractFunction::execute + ); + + static String execute(String value, String template) throws CelEvaluationException { + var openingBraceIndex = template.indexOf('{'); + var closingBraceIndex = template.indexOf('}'); + + if (openingBraceIndex < 0 || closingBraceIndex < 0) { + return value; + } + + var prefix = template.substring(0, openingBraceIndex); + var suffix = closingBraceIndex == template.length() - 1 + ? "" + : template.substring(closingBraceIndex + 1, template.length()); + + if (value.contains(prefix)) { + var afterPrefix = value.substring(value.indexOf(prefix) + prefix.length()); + if (suffix.length() == 0) { + return afterPrefix; + } + else if (afterPrefix.contains(suffix)) { + return afterPrefix.substring(0, afterPrefix.indexOf(suffix)); + } + } + + return ""; + } +} diff --git a/sources/src/main/java/com/google/solutions/jitaccess/core/auth/EmailMapping.java b/sources/src/main/java/com/google/solutions/jitaccess/core/auth/EmailMapping.java new file mode 100644 index 000000000..f324774e3 --- /dev/null +++ b/sources/src/main/java/com/google/solutions/jitaccess/core/auth/EmailMapping.java @@ -0,0 +1,136 @@ +// +// Copyright 2024 Google LLC +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.google.solutions.jitaccess.core.auth; + +import com.google.api.client.json.GenericJson; +import com.google.solutions.jitaccess.cel.ExtractFunction; +import com.google.solutions.jitaccess.core.clients.EmailAddress; +import dev.cel.common.CelException; +import dev.cel.common.types.CelTypes; +import dev.cel.compiler.CelCompiler; +import dev.cel.compiler.CelCompilerFactory; +import dev.cel.parser.CelStandardMacro; +import dev.cel.runtime.CelRuntime; +import dev.cel.runtime.CelRuntimeFactory; +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +/** + * Maps User IDs to email addresses using a CEL expression. + */ +public class EmailMapping { + + private static final String USER_VARIABLE_NAME = "user"; + private static final CelCompiler CEL_COMPILER = + CelCompilerFactory.standardCelCompilerBuilder() + .setStandardMacros(CelStandardMacro.ALL) + .addVar(USER_VARIABLE_NAME, CelTypes.createMap(CelTypes.STRING, CelTypes.STRING)) + .addFunctionDeclarations(ExtractFunction.DECLARATION) + .build(); + + private static final CelRuntime CEL_RUNTIME = + CelRuntimeFactory + .standardCelRuntimeBuilder() + .addFunctionBindings(ExtractFunction.BINDING) + .build(); + + private @Nullable String celExpression; + + /** + * Create mapping that uses the user's ID as email address. + */ + public EmailMapping() { + this(null); + } + + /** + * Create mapping that uses a CEL expression to derive an email address + * from a user ID. + */ + public EmailMapping(@Nullable String celExpression) { + this.celExpression = celExpression; + } + + /** + * Map a user ID to an email address. + */ + public EmailAddress emailFromUserId(UserId userId) throws MappingException { + if (this.celExpression == null || this.celExpression.isBlank()) { + // + // Use the user's ID as email address. + // + return new EmailAddress(userId.email); + } + else + { + // + // Apply a CEL mapping. + // + + // + // Expose user's email as `user.email`. + // + var userVariable = new GenericJson().set("email", userId.email); + + try { + var ast = CEL_COMPILER.compile(this.celExpression).getAst(); + var resultObject = CEL_RUNTIME + .createProgram(ast) + .eval(Map.of(USER_VARIABLE_NAME, userVariable)); + + if (resultObject == null) { + throw new MappingException( + userId, + "Result is null"); + } + else if (resultObject instanceof String result) { + return new EmailAddress(result); + } + else { + throw new MappingException( + userId, + String.format("Result is of type '%s' instead of a string", resultObject.getClass())); + } + } + catch (CelException e) { + throw new MappingException(userId, e); + } + } + } + + public static class MappingException extends RuntimeException { + public MappingException(UserId input, Exception cause) { + super( + String.format( + "The email mapping expression failed to transform the user ID '%s' into a valid email address", + input), + cause); + } + public MappingException(UserId input, String issue) { + super(String.format( + "The email mapping expression failed to transform the user ID '%s' into a valid email address: %s", + input, + issue)); + } + } +} diff --git a/sources/src/main/java/com/google/solutions/jitaccess/core/clients/EmailAddress.java b/sources/src/main/java/com/google/solutions/jitaccess/core/clients/EmailAddress.java new file mode 100644 index 000000000..b6256a7d1 --- /dev/null +++ b/sources/src/main/java/com/google/solutions/jitaccess/core/clients/EmailAddress.java @@ -0,0 +1,39 @@ +// +// Copyright 2024 Google LLC +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.google.solutions.jitaccess.core.clients; + +import jakarta.mail.internet.InternetAddress; +import org.jetbrains.annotations.NotNull; + +import java.io.UnsupportedEncodingException; + +/** + * A routable email address. + */ +public record EmailAddress( + @NotNull String value +) { + @Override + public String toString() { + return this.value; + } +} diff --git a/sources/src/main/java/com/google/solutions/jitaccess/core/clients/SmtpClient.java b/sources/src/main/java/com/google/solutions/jitaccess/core/clients/SmtpClient.java index 324cc932b..7e9f9185c 100644 --- a/sources/src/main/java/com/google/solutions/jitaccess/core/clients/SmtpClient.java +++ b/sources/src/main/java/com/google/solutions/jitaccess/core/clients/SmtpClient.java @@ -59,8 +59,8 @@ public SmtpClient( } public void sendMail( - @NotNull Collection toRecipients, - @NotNull Collection ccRecipients, + @NotNull Collection toRecipients, + @NotNull Collection ccRecipients, @NotNull String subject, @NotNull Multipart content, @NotNull EnumSet flags @@ -91,18 +91,20 @@ protected PasswordAuthentication getPasswordAuthentication() { var message = new MimeMessage(session); message.setContent(content); - message.setFrom(new InternetAddress(this.options.senderAddress, this.options.senderName)); + message.setFrom(new InternetAddress( + this.options.senderAddress.value(), + this.options.senderName)); for (var recipient : toRecipients){ message.addRecipient( Message.RecipientType.TO, - new InternetAddress(recipient.email, recipient.email)); + new InternetAddress(recipient.value(), recipient.value())); } for (var recipient : ccRecipients){ message.addRecipient( Message.RecipientType.CC, - new InternetAddress(recipient.email, recipient.email)); + new InternetAddress(recipient.value(), recipient.value())); } // @@ -127,8 +129,8 @@ protected PasswordAuthentication getPasswordAuthentication() { } public void sendMail( - @NotNull Collection toRecipients, - @NotNull Collection ccRecipients, + @NotNull Collection toRecipients, + @NotNull Collection ccRecipients, @NotNull String subject, @NotNull String htmlContent, @NotNull EnumSet flags @@ -169,17 +171,17 @@ public enum Flags { public static class Options { private @Nullable PasswordAuthentication cachedAuthentication = null; private final @NotNull String senderName; - private final @NotNull String senderAddress; + private final @NotNull EmailAddress senderAddress; private final @NotNull Properties smtpProperties; private String smtpUsername; private String smtpPassword; private String smtpSecretPath; public Options( - String smtpHost, + @NotNull String smtpHost, int smtpPort, @NotNull String senderName, - @NotNull String senderAddress, + @NotNull EmailAddress senderAddress, boolean enableStartTls, @Nullable Map extraOptions ) { diff --git a/sources/src/main/java/com/google/solutions/jitaccess/core/notifications/MailNotificationService.java b/sources/src/main/java/com/google/solutions/jitaccess/core/notifications/MailNotificationService.java index 46f92758d..414d20127 100644 --- a/sources/src/main/java/com/google/solutions/jitaccess/core/notifications/MailNotificationService.java +++ b/sources/src/main/java/com/google/solutions/jitaccess/core/notifications/MailNotificationService.java @@ -24,6 +24,7 @@ import com.google.common.base.Preconditions; import com.google.common.escape.Escaper; import com.google.common.html.HtmlEscapers; +import com.google.solutions.jitaccess.core.auth.EmailMapping; import com.google.solutions.jitaccess.core.clients.SmtpClient; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -44,6 +45,7 @@ */ public class MailNotificationService extends NotificationService { private final @NotNull Options options; + private final @NotNull EmailMapping emailMapping; private final @NotNull SmtpClient smtpClient; /** @@ -82,12 +84,15 @@ public class MailNotificationService extends NotificationService { public MailNotificationService( @NotNull SmtpClient smtpClient, + @NotNull EmailMapping emailMapping, @NotNull Options options ) { Preconditions.checkNotNull(smtpClient); + Preconditions.checkNotNull(emailMapping); Preconditions.checkNotNull(options); this.smtpClient = smtpClient; + this.emailMapping = emailMapping; this.options = options; } @@ -121,16 +126,27 @@ public void sendNotification(@NotNull Notification notification) throws Notifica try { this.smtpClient.sendMail( - notification.getToRecipients(), - notification.getCcRecipients(), + notification.getToRecipients() + .stream() + .map(id -> this.emailMapping.emailFromUserId(id)) + .collect(Collectors.toSet()), + notification.getCcRecipients() + .stream() + .map(id -> this.emailMapping.emailFromUserId(id)) + .collect(Collectors.toSet()), notification.getSubject(), formattedMessage, notification.isReply() ? EnumSet.of(SmtpClient.Flags.REPLY) : EnumSet.of(SmtpClient.Flags.NONE)); } + catch (EmailMapping.MappingException e) { + throw new NotificationException( + "The notification could not be sent because of an invalid email address", e); + } catch (SmtpClient.MailException e) { - throw new NotificationException("The notification could not be sent", e); + throw new NotificationException( + "The notification could not be sent", e); } } diff --git a/sources/src/main/java/com/google/solutions/jitaccess/web/RuntimeConfiguration.java b/sources/src/main/java/com/google/solutions/jitaccess/web/RuntimeConfiguration.java index f764b28d4..f1551f677 100644 --- a/sources/src/main/java/com/google/solutions/jitaccess/web/RuntimeConfiguration.java +++ b/sources/src/main/java/com/google/solutions/jitaccess/web/RuntimeConfiguration.java @@ -110,6 +110,7 @@ public RuntimeConfiguration(Function readSetting) { // // SMTP settings. // + this.smtpAddressMapping = new StringSetting(List.of("SMTP_ADDRESS_MAPPING"), null); this.smtpHost = new StringSetting(List.of("SMTP_HOST"), "smtp.gmail.com"); this.smtpPort = new IntSetting(List.of("SMTP_PORT"), 587); this.smtpEnableStartTls = new BooleanSetting(List.of("SMTP_ENABLE_STARTTLS"), true); @@ -190,6 +191,11 @@ public RuntimeConfiguration(Function readSetting) { */ public final @NotNull ZoneIdSetting timeZoneForNotifications; + /** + * CEL expression for mapping userIDs to email addresses. + */ + public final @NotNull StringSetting smtpAddressMapping; + /** * SMTP server for sending notifications. */ diff --git a/sources/src/main/java/com/google/solutions/jitaccess/web/RuntimeEnvironment.java b/sources/src/main/java/com/google/solutions/jitaccess/web/RuntimeEnvironment.java index 46b60c45d..745198943 100644 --- a/sources/src/main/java/com/google/solutions/jitaccess/web/RuntimeEnvironment.java +++ b/sources/src/main/java/com/google/solutions/jitaccess/web/RuntimeEnvironment.java @@ -32,6 +32,7 @@ import com.google.auth.oauth2.ImpersonatedCredentials; import com.google.auth.oauth2.ServiceAccountCredentials; import com.google.solutions.jitaccess.core.ApplicationVersion; +import com.google.solutions.jitaccess.core.auth.EmailMapping; import com.google.solutions.jitaccess.core.auth.UserId; import com.google.solutions.jitaccess.core.catalog.RegexJustificationPolicy; import com.google.solutions.jitaccess.core.catalog.TokenSigner; @@ -317,7 +318,8 @@ public GoogleCredentials getApplicationCredentials() { @Produces @Singleton public @NotNull NotificationService getEmailNotificationService( - @NotNull SecretManagerClient secretManagerClient + @NotNull SecretManagerClient secretManagerClient, + @NotNull EmailMapping emailMapping ) { // // Configure SMTP if possible, and fall back to a fail-safe @@ -328,7 +330,7 @@ public GoogleCredentials getApplicationCredentials() { this.configuration.smtpHost.getValue(), this.configuration.smtpPort.getValue(), this.configuration.smtpSenderName.getValue(), - this.configuration.smtpSenderAddress.getValue(), + new EmailAddress(this.configuration.smtpSenderAddress.getValue()), this.configuration.smtpEnableStartTls.getValue(), this.configuration.getSmtpExtraOptionsMap()); @@ -349,6 +351,7 @@ else if (this.configuration.isSmtpAuthenticationConfigured() && this.configurati return new MailNotificationService( new SmtpClient(secretManagerClient, options), + emailMapping, new MailNotificationService.Options(this.configuration.timeZoneForNotifications.getValue())); } else { @@ -356,6 +359,11 @@ else if (this.configuration.isSmtpAuthenticationConfigured() && this.configurati } } + @Produces + public @NotNull EmailMapping getEmailMapping() { + return new EmailMapping(this.configuration.smtpAddressMapping.getValue()); + } + @Produces public @NotNull ProjectRoleActivator.Options getProjectRoleActivatorOptions() { return new ProjectRoleActivator.Options( diff --git a/sources/src/main/java/com/google/solutions/jitaccess/web/actions/RequestActivationAction.java b/sources/src/main/java/com/google/solutions/jitaccess/web/actions/RequestActivationAction.java index 92f120420..4e8bfcd26 100644 --- a/sources/src/main/java/com/google/solutions/jitaccess/web/actions/RequestActivationAction.java +++ b/sources/src/main/java/com/google/solutions/jitaccess/web/actions/RequestActivationAction.java @@ -230,7 +230,7 @@ public RequestActivationAction( throw (AccessDeniedException)e.fillInStackTrace(); } else { - throw new AccessDeniedException("Requesting access failed", e); + throw new AccessDeniedException("Requesting access failed, see logs for details", e); } } } diff --git a/sources/src/main/java/com/google/solutions/jitaccess/web/actions/RequestAndSelfApproveAction.java b/sources/src/main/java/com/google/solutions/jitaccess/web/actions/RequestAndSelfApproveAction.java index 85654ec44..aec6ed61f 100644 --- a/sources/src/main/java/com/google/solutions/jitaccess/web/actions/RequestAndSelfApproveAction.java +++ b/sources/src/main/java/com/google/solutions/jitaccess/web/actions/RequestAndSelfApproveAction.java @@ -163,7 +163,7 @@ public RequestAndSelfApproveAction( throw (AccessDeniedException)e.fillInStackTrace(); } else { - throw new AccessDeniedException("Activating role failed", e); + throw new AccessDeniedException("Activating role failed, see logs for details", e); } } } diff --git a/sources/src/test/java/com/google/solutions/jitaccess/cel/TestExtractFunction.java b/sources/src/test/java/com/google/solutions/jitaccess/cel/TestExtractFunction.java new file mode 100644 index 000000000..00a58a36e --- /dev/null +++ b/sources/src/test/java/com/google/solutions/jitaccess/cel/TestExtractFunction.java @@ -0,0 +1,63 @@ +// +// Copyright 2024 Google LLC +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.google.solutions.jitaccess.cel; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestExtractFunction { + + //------------------------------------------------------------------------- + // evaluate: extract(). + //------------------------------------------------------------------------- + + @Test + public void extract() throws Exception { + var value = "projects/_/buckets/acme-orders-aaa/objects/data_lake/orders/order_date=2019-11-03/aef87g87ae0876"; + + assertEquals( + "2019-11-03", + ExtractFunction.execute(value, "/order_date={date}/")); + assertEquals( + "acme-orders-aaa", + ExtractFunction.execute(value, "buckets/{name}/")); + assertEquals( + "", + ExtractFunction.execute(value, "/orders/{empty}order_date")); + assertEquals( + "projects/_/buckets/acme-orders-aaa", + ExtractFunction.execute(value, "{start}/objects/data_lake")); + assertEquals( + "order_date=2019-11-03/aef87g87ae0876", + ExtractFunction.execute(value, "orders/{end}")); + assertEquals( + value, + ExtractFunction.execute(value, "{all}")); + assertEquals( + "", + ExtractFunction.execute(value, "/orders/{none}/order_date=")); + assertEquals( + "", + ExtractFunction.execute(value, "/orders/order_date=2019-11-03/{id}/data_lake")); + } +} diff --git a/sources/src/test/java/com/google/solutions/jitaccess/core/auth/TestEmailMapping.java b/sources/src/test/java/com/google/solutions/jitaccess/core/auth/TestEmailMapping.java new file mode 100644 index 000000000..1b6f251eb --- /dev/null +++ b/sources/src/test/java/com/google/solutions/jitaccess/core/auth/TestEmailMapping.java @@ -0,0 +1,83 @@ +// +// Copyright 2024 Google LLC +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.google.solutions.jitaccess.core.auth; + +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.*; + +public class TestEmailMapping { + @Test + public void nullExpression() { + var mapping = new EmailMapping(); + assertEquals( + "user@example.com", + mapping.emailFromUserId(new UserId("user@example.com")).value()); + } + + @Test + public void emptyExpression() { + var mapping = new EmailMapping(""); + assertEquals( + "user@example.com", + mapping.emailFromUserId(new UserId("user@example.com")).value()); + } + + @Test + public void userDotEmailExpression() { + var expression ="user.email"; + assertEquals( + "user@example.com", + new EmailMapping(expression).emailFromUserId(new UserId("user@example.com")).value()); + } + + @Test + public void substituteDomainExpression() { + var expression = "user.email.extract('{handle}@example.com') + '@test.example.com'"; + assertEquals( + "user@test.example.com", + new EmailMapping(expression).emailFromUserId(new UserId("user@example.com")).value()); + } + + @Test + public void substituteDomainConditionallyExpression() { + var expression = + "user.email.endsWith('@external.example.com') " + + "? user.email.extract('{handle}@external.example.com') + '@otherdomain.example' " + + ": user.email"; + + assertEquals( + "contractor@otherdomain.example", + new EmailMapping(expression).emailFromUserId(new UserId("contractor@external.example.com")).value()); + assertEquals( + "user@example.com", + new EmailMapping(expression).emailFromUserId(new UserId("user@example.com")).value()); + } + + @Test + public void invalidExpression() { + assertThrows( + EmailMapping.MappingException.class, + () -> new EmailMapping("user.email.extract(") + .emailFromUserId(new UserId("user@example.com")) + .value()); + } +} diff --git a/sources/src/test/java/com/google/solutions/jitaccess/core/clients/TestEmailAddress.java b/sources/src/test/java/com/google/solutions/jitaccess/core/clients/TestEmailAddress.java new file mode 100644 index 000000000..559862f93 --- /dev/null +++ b/sources/src/test/java/com/google/solutions/jitaccess/core/clients/TestEmailAddress.java @@ -0,0 +1,82 @@ +// +// Copyright 2024 Google LLC +// +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you 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.google.solutions.jitaccess.core.clients; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertFalse; + +public class TestEmailAddress { + // ------------------------------------------------------------------------- + // toString. + // ------------------------------------------------------------------------- + + @Test + public void toStringReturnsEmail() { + assertEquals("test@example.com", new EmailAddress("test@example.com").toString()); + } + + // ------------------------------------------------------------------------- + // Equality. + // ------------------------------------------------------------------------- + + @Test + public void whenObjectAreEquivalent_ThenEqualsReturnsTrue() { + EmailAddress id1 = new EmailAddress("bob@example.com"); + EmailAddress id2 = new EmailAddress("bob@example.com"); + + assertTrue(id1.equals(id2)); + assertEquals(id1.hashCode(), id2.hashCode()); + } + + @Test + public void whenObjectAreSame_ThenEqualsReturnsTrue() { + EmailAddress id1 = new EmailAddress("bob@example.com"); + + assertTrue(id1.equals(id1)); + } + + @Test + public void whenObjectAreMotEquivalent_ThenEqualsReturnsFalse() { + EmailAddress id1 = new EmailAddress("alice@example.com"); + EmailAddress id2 = new EmailAddress("bob@example.com"); + + assertFalse(id1.equals(id2)); + assertNotEquals(id1.hashCode(), id2.hashCode()); + } + + @Test + public void whenObjectIsNull_ThenEqualsReturnsFalse() { + EmailAddress id1 = new EmailAddress("bob@example.com"); + + assertFalse(id1.equals(null)); + } + + @Test + public void whenObjectIsDifferentType_ThenEqualsReturnsFalse() { + EmailAddress id1 = new EmailAddress("bob@example.com"); + + assertFalse(id1.equals("")); + } + +} diff --git a/sources/src/test/java/com/google/solutions/jitaccess/core/clients/TestSmtpClient.java b/sources/src/test/java/com/google/solutions/jitaccess/core/clients/TestSmtpClient.java index 779ab2a57..05e79e89f 100644 --- a/sources/src/test/java/com/google/solutions/jitaccess/core/clients/TestSmtpClient.java +++ b/sources/src/test/java/com/google/solutions/jitaccess/core/clients/TestSmtpClient.java @@ -37,7 +37,13 @@ public class TestSmtpClient { @Test public void whenOptionsContainPassword_ThenCreateAuthenticatorUsesPassword() throws Exception { - var options = new SmtpClient.Options("host", 2525, "sender", "sender@example.com", true, Map.of()) + var options = new SmtpClient.Options( + "host", + 2525, + "sender", + new EmailAddress("sender@example.com"), + true, + Map.of()) .setSmtpCleartextCredentials("user", "password"); var secretManager = Mockito.mock(SecretManagerClient.class); @@ -48,7 +54,13 @@ public void whenOptionsContainPassword_ThenCreateAuthenticatorUsesPassword() thr @Test public void whenOptionsContainSecretPath_ThenCreateAuthenticatorUsesPasswordFromSecret() throws Exception { - var options = new SmtpClient.Options("host", 2525, "sender", "sender@example.com", true, Map.of()) + var options = new SmtpClient.Options( + "host", + 2525, + "sender", + new EmailAddress("sender@example.com"), + true, + Map.of()) .setSmtpSecretCredentials("user", "path/to/secret"); var secretManager = Mockito.mock(SecretManagerClient.class); diff --git a/sources/src/test/java/com/google/solutions/jitaccess/core/notifications/TestMailNotificationService.java b/sources/src/test/java/com/google/solutions/jitaccess/core/notifications/TestMailNotificationService.java index 3501f1004..8127906d3 100644 --- a/sources/src/test/java/com/google/solutions/jitaccess/core/notifications/TestMailNotificationService.java +++ b/sources/src/test/java/com/google/solutions/jitaccess/core/notifications/TestMailNotificationService.java @@ -21,8 +21,11 @@ package com.google.solutions.jitaccess.core.notifications; +import com.google.solutions.jitaccess.core.auth.EmailMapping; import com.google.solutions.jitaccess.core.auth.UserId; +import com.google.solutions.jitaccess.core.clients.EmailAddress; import com.google.solutions.jitaccess.core.clients.SmtpClient; +import jakarta.validation.constraints.Email; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -70,17 +73,17 @@ public void whenTemplateNotFound_ThenSendNotificationDoesNotSendMail() throws Ex var mailAdapter = Mockito.mock(SmtpClient.class); var service = new MailNotificationService( mailAdapter, + new EmailMapping(), new MailNotificationService.Options(MailNotificationService.Options.DEFAULT_TIMEZONE)); - var to = new UserId("user@example.com"); service.sendNotification(new TestNotification( - to, + new UserId("user@example.com"), "Test email", new HashMap(), "unknown-templateid")); verify(mailAdapter, times(0)).sendMail( - eq(List.of(to)), + eq(List.of(new EmailAddress("user@example.com"))), eq(List.of()), eq("Test email"), anyString(), @@ -92,17 +95,17 @@ public void whenTemplateFound_ThenSendNotificationSendsMail() throws Exception { var mailAdapter = Mockito.mock(SmtpClient.class); var service = new MailNotificationService( mailAdapter, + new EmailMapping(), new MailNotificationService.Options(MailNotificationService.Options.DEFAULT_TIMEZONE)); - var to = new UserId("user@example.com"); service.sendNotification(new TestNotification( - to, + new UserId("user@example.com"), "Test email", new HashMap(), "RequestActivation")); verify(mailAdapter, times(1)).sendMail( - eq(List.of(to)), + eq(List.of(new EmailAddress("user@example.com"))), eq(List.of()), eq("Test email"), anyString(),