Skip to content

Commit 131708d

Browse files
committed
Merge remote-tracking branch 'origin/develop' into fb_sampleColors
2 parents 6d73e9b + b3d6a9a commit 131708d

35 files changed

Lines changed: 11460 additions & 28583 deletions

api/src/org/labkey/api/ApiModule.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,7 @@
173173
import org.labkey.api.util.Pair;
174174
import org.labkey.api.util.Path;
175175
import org.labkey.api.util.SessionHelper;
176+
import org.labkey.api.util.SmtpTransportProvider;
176177
import org.labkey.api.util.StringExpressionFactory;
177178
import org.labkey.api.util.StringUtilsLabKey;
178179
import org.labkey.api.util.SvgUtil;
@@ -234,6 +235,7 @@ protected void init()
234235

235236
PropertyManager.registerEncryptionMigrationHandler();
236237
AuthenticationManager.registerEncryptionMigrationHandler();
238+
MailHelper.registerProvider(new SmtpTransportProvider());
237239

238240
LabKeyManagement.register(new StandardMBean(new OperationsMXBeanImpl(), OperationsMXBean.class, true), "Operations");
239241

api/src/org/labkey/api/notification/EmailMessage.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public interface EmailMessage
4040
// TODO: Only used by tests... delete?
4141
void setFiles(List<File> files);
4242
void addContent(MimeType type, String content);
43-
void addContent(MimeType type, HttpServletRequest request, HttpView view) throws Exception;
43+
void addContent(MimeType type, HttpServletRequest request, HttpView<?> view) throws Exception;
4444

4545
/**
4646
* Sets the display name for the email sender, the actual sender email address will be the one configured via site or project settings

api/src/org/labkey/api/pipeline/PipelineJob.java

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.fasterxml.jackson.annotation.PropertyAccessor;
2121
import com.fasterxml.jackson.databind.ObjectMapper;
2222
import com.fasterxml.jackson.databind.SerializationFeature;
23+
import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator;
2324
import com.fasterxml.jackson.databind.module.SimpleModule;
2425
import datadog.trace.api.CorrelationIdentifier;
2526
import datadog.trace.api.Trace;
@@ -1897,12 +1898,30 @@ public static PipelineJob deserializeJob(@NotNull String serialized)
18971898
}
18981899

18991900
public static ObjectMapper createObjectMapper()
1901+
{
1902+
return createObjectMapper(null);
1903+
}
1904+
1905+
/**
1906+
* Build the pipeline-job ObjectMapper. Polymorphic default typing (NON_FINAL) is always active so the concrete
1907+
* PipelineJob subclass and its field graph round-trip through {@code @class} type ids on the wire.
1908+
*
1909+
* @param typeValidator when non-null, default typing is activated with this {@link PolymorphicTypeValidator}
1910+
* (a deny-by-default allowlist) instead of the deprecated, unrestricted {@code enableDefaultTyping}. It is consulted
1911+
* only on deserialization, so only the pipeline-job deserialize path passes one (see {@code PipelineJacksonTyping});
1912+
* serialization and all other callers pass null and keep the historical permissive behavior.
1913+
*/
1914+
public static ObjectMapper createObjectMapper(@Nullable PolymorphicTypeValidator typeValidator)
19001915
{
19011916
ObjectMapper mapper = JsonUtil.DEFAULT_MAPPER.copy()
19021917
.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE)
19031918
.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
1904-
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS)
1905-
.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
1919+
.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
1920+
1921+
if (typeValidator != null)
1922+
mapper.activateDefaultTyping(typeValidator, ObjectMapper.DefaultTyping.NON_FINAL);
1923+
else
1924+
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
19061925

19071926
SimpleModule module = new SimpleModule();
19081927
module.addSerializer(new SqlTimeSerialization.SqlTimeSerializer());

api/src/org/labkey/api/secrets/SecretProvider.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,14 @@ public interface SecretProvider
3030

3131
/** Human-readable name for this source, shown on the admin secrets page. */
3232
@NotNull String getDescription();
33+
34+
/**
35+
* Returns the name of the application this source is scoped to, or {@code null} if this
36+
* source has no notion of an application name (the default). Used for attributing
37+
* externally-billed resources (e.g., Vertex AI requests) back to the owning app.
38+
*/
39+
default @Nullable String getAppName()
40+
{
41+
return null;
42+
}
3343
}

api/src/org/labkey/api/secrets/SecretService.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,10 @@ static void setInstance(SecretService service)
9090
* The external provider takes priority over startup-property and environment-variable sources.
9191
*/
9292
@Nullable String getExternalProviderDescription();
93+
94+
/**
95+
* Returns the application name reported by the highest-priority provider that has one
96+
* (see {@link SecretProvider#getAppName()}), or {@code null} if no active provider reports one.
97+
*/
98+
@Nullable String getAppName();
9399
}

api/src/org/labkey/api/settings/AppProps.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.labkey.api.data.ContainerManager;
2222
import org.labkey.api.module.DefaultModule;
2323
import org.labkey.api.module.SupportedDatabase;
24+
import org.labkey.api.secrets.SecretService;
2425
import org.labkey.api.util.ExceptionReportingLevel;
2526
import org.labkey.api.util.Path;
2627
import org.labkey.api.util.UsageReportingLevel;
@@ -75,6 +76,17 @@ static WriteableAppProps getWriteableInstance()
7576
@Nullable
7677
String getEnlistmentId();
7778

79+
/**
80+
* Returns the DevOps-assigned name of the tenant/customer this deployment belongs to (e.g., "Hooli",
81+
* "WNPRC"), or {@code null} if none is configured. Not customer-facing; currently sourced from
82+
* whichever active {@link org.labkey.api.secrets.SecretProvider} reports one.
83+
* @see SecretService#getAppName()
84+
*/
85+
default @Nullable String getAppName()
86+
{
87+
return SecretService.get().getAppName();
88+
}
89+
7890
boolean isCachingAllowed();
7991

8092
boolean isRecompileJspEnabled();

api/src/org/labkey/api/util/EmailTransportProvider.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import jakarta.mail.Message;
1919
import jakarta.mail.MessagingException;
20+
import jakarta.mail.Session;
2021

2122
import java.util.Properties;
2223

@@ -31,6 +32,13 @@ public interface EmailTransportProvider
3132
*/
3233
String getName();
3334

35+
/**
36+
* @return a short, human-readable hint describing how to configure this provider, used when building the
37+
* "no email transport configured" error message. For example, {@code "SMTP (mail.smtp.*)"}. Only the hints of
38+
* registered providers are shown, so an undeployed provider (e.g. Microsoft Graph) never appears in the message.
39+
*/
40+
String getConfigurationHint();
41+
3442
/**
3543
* Load configuration from startup properties and/or ServletContext.
3644
* Called once during initialization.
@@ -50,6 +58,17 @@ public interface EmailTransportProvider
5058
*/
5159
void send(Message message) throws MessagingException;
5260

61+
/**
62+
* @return the {@link Session} to associate with newly created messages. The session travels with the message and
63+
* carries any transport-specific configuration needed at send time (e.g. SMTP host/port/auth). Transports that
64+
* don't rely on the session (e.g. Microsoft Graph, which reads the assembled MIME content) can use the default
65+
* neutral session.
66+
*/
67+
default Session getSession()
68+
{
69+
return MailHelper.getDefaultSession();
70+
}
71+
5372
/**
5473
* @return the configuration properties for this provider
5574
*/

api/src/org/labkey/api/util/MailHelper.java

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@
5353
import java.util.HashMap;
5454
import java.util.List;
5555
import java.util.Map;
56+
import java.util.Properties;
5657
import java.util.StringTokenizer;
58+
import java.util.concurrent.CopyOnWriteArrayList;
5759

5860
/**
5961
* Provides static functions for help with sending email. Supports SMTP and Microsoft Graph transport providers.
@@ -65,11 +67,15 @@ public class MailHelper
6567
private static final Logger _log = LogHelper.getLogger(MailHelper.class, "Errors sending and configuring email");
6668

6769
// Transport providers
68-
private static final SmtpTransportProvider _smtpProvider = new SmtpTransportProvider();
69-
private static final List<EmailTransportProvider> _providers = new ArrayList<>(List.of(_smtpProvider));
70+
private static final List<EmailTransportProvider> _providers = new CopyOnWriteArrayList<>();
71+
72+
// A neutral session for building MIME messages. Used when the active provider doesn't need session-based transport
73+
// config (e.g. Microsoft Graph) or when no provider is configured. Message assembly (headers, body, attachments)
74+
// doesn't depend on any transport-specific session state.
75+
private static final Session DEFAULT_SESSION = Session.getInstance(new Properties());
7076

7177
// Active provider (set during initialization)
72-
private static EmailTransportProvider _activeProvider = null;
78+
private static volatile EmailTransportProvider _activeProvider = null;
7379

7480
// Configuration conflict flag
7581
private static boolean _configurationConflict = false;
@@ -121,13 +127,24 @@ public static EmailTransportProvider getActiveProvider()
121127
return _activeProvider;
122128
}
123129

130+
/**
131+
* Directly set the active transport provider, bypassing the normal configuration-driven selection in
132+
* {@link #loadActiveProvider()}. Intended for tools that need to temporarily redirect all outgoing email, such as
133+
* the Dumbster mail recorder, which installs its own {@link SmtpTransportProvider} pointed at a local capture
134+
* server. Callers should save the previous provider (via {@link #getActiveProvider()}) and restore it when done.
135+
*/
136+
public static void setActiveProvider(@Nullable EmailTransportProvider provider)
137+
{
138+
_activeProvider = provider;
139+
}
140+
124141
public static boolean hasActiveProvider()
125142
{
126143
return null != _activeProvider;
127144
}
128145

129146
/**
130-
* Registers an optional transport provider. Must be called during module {@code init()} so that
147+
* Registers a transport provider. Must be called during module {@code init()} so that
131148
* all providers are in place before {@link #init()} calls {@link #loadActiveProvider()}.
132149
*/
133150
public static void registerProvider(EmailTransportProvider provider)
@@ -140,31 +157,37 @@ public static void init()
140157
_activeProvider = loadActiveProvider();
141158
}
142159

143-
public static void setSmtpSession(Session session)
160+
/**
161+
* @return the {@link Session} to associate with newly created messages, supplied by the active transport provider
162+
* (which decides what session state, if any, a message needs to carry to be delivered). Falls back to a neutral
163+
* session when no provider is configured. Provider-agnostic: callers should not assume this is an SMTP session.
164+
*/
165+
@NotNull
166+
public static Session getSession()
144167
{
145-
_smtpProvider.setSession(session);
168+
return null != _activeProvider ? _activeProvider.getSession() : DEFAULT_SESSION;
146169
}
147170

148171
/**
149-
* Returns the SMTP session for creating messages
172+
* @return a neutral session suitable for assembling MIME messages that don't need transport-specific session state.
150173
*/
151-
@Nullable
152-
public static Session getSmtpSession()
174+
@NotNull
175+
static Session getDefaultSession()
153176
{
154-
return _smtpProvider.getSession();
177+
return DEFAULT_SESSION;
155178
}
156179

157180
/**
158181
* Creates a blank email message. Caller must set all fields before sending.
159182
*/
160183
public static ViewMessage createMessage()
161184
{
162-
return new ViewMessage(getSmtpSession());
185+
return new ViewMessage(getSession());
163186
}
164187

165188
public static MultipartMessage createMultipartMessage()
166189
{
167-
return new MultipartMessage(getSmtpSession());
190+
return new MultipartMessage(getSession());
168191
}
169192

170193
/**
@@ -199,6 +222,21 @@ public static Address[] createAddressArray(String s) throws AddressException
199222
return addresses.toArray(new Address[0]);
200223
}
201224

225+
/**
226+
* Builds the "no email transport configured" message from the hints of the currently registered providers, so that
227+
* an undeployed provider (e.g. Microsoft Graph, when its module isn't present) is never mentioned.
228+
*/
229+
private static String noTransportConfiguredMessage()
230+
{
231+
List<String> hints = _providers.stream()
232+
.map(EmailTransportProvider::getConfigurationHint)
233+
.toList();
234+
if (hints.isEmpty())
235+
return "No email transport configured and no transport providers are registered.";
236+
String choices = StringUtilsLabKey.joinWithConjunction(hints, "or");
237+
return "No email transport configured. Please configure " + choices + " settings.";
238+
}
239+
202240
/**
203241
* Sends an email message using the configured transport provider. This method logs
204242
* exceptions before throwing them to the caller. The caller should avoid double-logging
@@ -222,9 +260,7 @@ public static void send(Message m, @Nullable User user, Container c)
222260
// Check if any provider is configured
223261
if (_activeProvider == null)
224262
{
225-
throw new ConfigurationException(
226-
"No email transport configured. Please configure either SMTP (mail.smtp.*) " +
227-
"or Microsoft Graph (mail.graph.*) settings.");
263+
throw new ConfigurationException(noTransportConfiguredMessage());
228264
}
229265

230266
// Send via the active provider

api/src/org/labkey/api/util/SmtpTransportProvider.java

Lines changed: 44 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ public String getName()
6666
return "SMTP";
6767
}
6868

69+
@Override
70+
public String getConfigurationHint()
71+
{
72+
return "SMTP (mail.smtp.*)";
73+
}
74+
6975
@Override
7076
public void loadConfiguration()
7177
{
@@ -99,22 +105,7 @@ public void handle(Collection<StartupPropertyEntry> entries)
99105
// Create session if configured
100106
if (isConfigured())
101107
{
102-
_session = Session.getInstance(_properties);
103-
104-
if ("true".equalsIgnoreCase(_session.getProperty("mail.smtp.ssl.enable")) ||
105-
"true".equalsIgnoreCase(_session.getProperty("mail.smtp.starttls.enable")))
106-
{
107-
String username = _session.getProperty("mail.smtp.user");
108-
String password = _session.getProperty("mail.smtp.password");
109-
_session = Session.getInstance(_session.getProperties(), new Authenticator()
110-
{
111-
@Override
112-
protected PasswordAuthentication getPasswordAuthentication()
113-
{
114-
return new PasswordAuthentication(username, password);
115-
}
116-
});
117-
}
108+
_session = createSession(_properties);
118109
LOG.info("Email configured to use SMTP transport");
119110
}
120111
}
@@ -124,6 +115,40 @@ protected PasswordAuthentication getPasswordAuthentication()
124115
}
125116
}
126117

118+
/**
119+
* Configure this provider directly from the supplied SMTP properties, replacing any previously loaded
120+
* configuration and rebuilding the session. Used to point SMTP transport at an alternate server (e.g. the
121+
* Dumbster mail recorder's local capture server) without reaching into another provider's session state.
122+
*/
123+
public void configure(Properties properties)
124+
{
125+
_properties.clear();
126+
_properties.putAll(properties);
127+
_session = createSession(_properties);
128+
}
129+
130+
private static Session createSession(Properties properties)
131+
{
132+
Session session = Session.getInstance(properties);
133+
134+
if ("true".equalsIgnoreCase(session.getProperty("mail.smtp.ssl.enable")) ||
135+
"true".equalsIgnoreCase(session.getProperty("mail.smtp.starttls.enable")))
136+
{
137+
String username = session.getProperty("mail.smtp.user");
138+
String password = session.getProperty("mail.smtp.password");
139+
session = Session.getInstance(session.getProperties(), new Authenticator()
140+
{
141+
@Override
142+
protected PasswordAuthentication getPasswordAuthentication()
143+
{
144+
return new PasswordAuthentication(username, password);
145+
}
146+
});
147+
}
148+
149+
return session;
150+
}
151+
127152
@Override
128153
public boolean isConfigured()
129154
{
@@ -141,18 +166,15 @@ public void send(Message message) throws MessagingException
141166
}
142167

143168
/**
144-
* @return the SMTP session for creating messages, or null if not configured
169+
* @return the SMTP session, which carries the host/port/auth configuration that {@code Transport.send()} needs at
170+
* send time, or null if not configured
145171
*/
172+
@Override
146173
public Session getSession()
147174
{
148175
return _session;
149176
}
150177

151-
public void setSession(Session session)
152-
{
153-
_session = session;
154-
}
155-
156178
@Override
157179
public Properties getProperties()
158180
{

0 commit comments

Comments
 (0)