Skip to content

Commit

Permalink
b/330263707 Use CEL expression to derive email address from user ID
Browse files Browse the repository at this point in the history
Apply a CEL expression to derive an email address from a user ID. This enables multi-party approval to be used in scenarios where some (or all) users in Cloud Identity/Workspace have non-routable email addresses.

The CEL expression can be configured using a new option, SMTP_ADDRESS_MAPPING. For example:

user.email.extract('{handle}@example.com') + '@test.example.com'
When SMTP_ADDRESS_MAPPING is not configured, JIT Access uses the user ID as-is, preseving the current behavior.

This PR is inspired by, and partially supersedes #303.

Co-authored-by: mvo-dev <mvo@cvation.com>
  • Loading branch information
jpassing and mvo-dev committed Mar 20, 2024
1 parent 329eea0 commit 7f3d6ad
Show file tree
Hide file tree
Showing 15 changed files with 590 additions and 25 deletions.
37 changes: 37 additions & 0 deletions doc/site/sources/docs/configuration-options.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,43 @@ The following table lists all available configuration options.
<td></td>
<td>1.2</td>
</tr>
<tr>
<td><code>SMTP_ADDRESS_MAPPING</code></td>
<td>
<p>
<a href="https://github.com/google/cel-spec/blob/master/doc/intro.md">CEL expression</a> for deriving
a user's email address from their Cloud Identity/Workspace user ID.
</p>
<p>
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.
</p>
<p>
CEL expressions can use <a href="https://github.com/google/cel-spec/blob/master/doc/langdef.md#list-of-standard-definitions">standard functions</a>
and the <a href="https://cloud.google.com/iam/docs/conditions-attribute-reference#extract"><code>extract()</code></a> function.
</p>
<p>
For example, the following expression replaces the domain <code>example.com</code> with <code>test.example.com</code> for all users:
</p>
<p>
<code>user.email.extract('{handle}@example.com') + '@test.example.com'</code>
</p>
<p>
If you're using multiple domains and only need to substitute one of them, you can use conditional
statements. For example:
</p>
<p>
<code>user.email.endsWith('@external.example.com')
? user.email.extract('{handle}@external.example.com') + '@otherdomain.example'
: user.email
</code>
</p>
</td>
<td>Optional</td>
<td></td>
<td>1.7</td>
</tr>
</table>

## Notifications
Expand Down
Original file line number Diff line number Diff line change
@@ -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 "";
}
}
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ public SmtpClient(
}

public void sendMail(
@NotNull Collection<UserId> toRecipients,
@NotNull Collection<UserId> ccRecipients,
@NotNull Collection<EmailAddress> toRecipients,
@NotNull Collection<EmailAddress> ccRecipients,
@NotNull String subject,
@NotNull Multipart content,
@NotNull EnumSet<Flags> flags
Expand Down Expand Up @@ -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()));
}

//
Expand All @@ -127,8 +129,8 @@ protected PasswordAuthentication getPasswordAuthentication() {
}

public void sendMail(
@NotNull Collection<UserId> toRecipients,
@NotNull Collection<UserId> ccRecipients,
@NotNull Collection<EmailAddress> toRecipients,
@NotNull Collection<EmailAddress> ccRecipients,
@NotNull String subject,
@NotNull String htmlContent,
@NotNull EnumSet<Flags> flags
Expand Down Expand Up @@ -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<String, String> extraOptions
) {
Expand Down
Loading

0 comments on commit 7f3d6ad

Please sign in to comment.