Repository navigation
Expand file tree
/
Copy pathModuleRuntimeConfigResolver.java
More file actions
289 lines (250 loc) · 12.3 KB
/
Copy pathModuleRuntimeConfigResolver.java
File metadata and controls
289 lines (250 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
package org.lucee.lucli.modules;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.lucee.lucli.LuCLI;
import org.lucee.lucli.Settings;
import org.lucee.lucli.secrets.LocalSecretStore;
import org.lucee.lucli.secrets.LucliSecretProviderSupport;
import org.lucee.lucli.secrets.SecretStoreException;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Resolves runtime env and secret inputs for module execution.
*/
public class ModuleRuntimeConfigResolver {
private static final String MODULE_ENV_FILE = ".env.lucli";
private static final String DOT_ENV_FILE = ".env";
private static final Pattern HASH_SECRET_PATTERN = Pattern.compile("#secret:([^#]+)#");
private final boolean strictMode;
private final boolean allowDotEnvFallback;
private final char[] passphraseOverride;
private LocalSecretStore localSecretStore;
private String selectedProviderName;
public ModuleRuntimeConfigResolver(boolean strictMode, boolean allowDotEnvFallback) {
this(strictMode, allowDotEnvFallback, null);
}
ModuleRuntimeConfigResolver(boolean strictMode, boolean allowDotEnvFallback, char[] passphraseOverride) {
this.strictMode = strictMode;
this.allowDotEnvFallback = allowDotEnvFallback;
this.passphraseOverride = passphraseOverride;
}
public static ModuleRuntimeConfigResolver fromSettings() {
Settings settings = new Settings();
JsonNode strictNode = settings.getNestedSetting("moduleRuntime", "strictEnv");
JsonNode fallbackNode = settings.getNestedSetting("moduleRuntime", "allowDotEnvFallback");
boolean strict = strictNode != null && !strictNode.isMissingNode() ? strictNode.asBoolean(false) : false;
boolean allowFallback = fallbackNode != null && !fallbackNode.isMissingNode() ? fallbackNode.asBoolean(false) : false;
// Env vars/system props can override settings for one-off runs.
String strictOverride = System.getProperty("lucli.modules.strictEnv");
if (strictOverride == null || strictOverride.isBlank()) {
strictOverride = System.getenv("LUCLI_MODULES_STRICT_ENV");
}
if (strictOverride != null && !strictOverride.isBlank()) {
strict = Boolean.parseBoolean(strictOverride);
}
String fallbackOverride = System.getProperty("lucli.modules.allowDotEnvFallback");
if (fallbackOverride == null || fallbackOverride.isBlank()) {
fallbackOverride = System.getenv("LUCLI_MODULES_ALLOW_DOTENV_FALLBACK");
}
if (fallbackOverride != null && !fallbackOverride.isBlank()) {
allowFallback = Boolean.parseBoolean(fallbackOverride);
}
return new ModuleRuntimeConfigResolver(strict, allowFallback);
}
public ResolutionResult resolve(String moduleName, ModuleConfig moduleConfig, Path projectDir) throws Exception {
selectedProviderName = LucliSecretProviderSupport.getSelectedProviderName();
Path moduleEnvPath = projectDir.resolve(MODULE_ENV_FILE);
Path dotEnvPath = projectDir.resolve(DOT_ENV_FILE);
Map<String, String> moduleEnvMap = Files.exists(moduleEnvPath)
? LuCLI.loadEnvFileToMap(moduleEnvPath)
: new HashMap<>();
Map<String, String> dotEnvMap = allowDotEnvFallback && Files.exists(dotEnvPath)
? LuCLI.loadEnvFileToMap(dotEnvPath)
: new HashMap<>();
Map<String, String> scriptEnv = new HashMap<>(LuCLI.scriptEnvironment);
Map<String, String> osEnv = System.getenv();
Map<String, String> resolvedEnv = new LinkedHashMap<>();
Map<String, String> resolvedSecrets = new LinkedHashMap<>();
List<String> warnings = new ArrayList<>();
for (ModuleConfig.PermissionRequirement req : moduleConfig.getEnvPermissions()) {
String value = resolveAlias(req.getAlias(), moduleEnvMap, dotEnvMap, scriptEnv, osEnv);
if (value != null && value.contains("#secret:")) {
value = resolveSecretPlaceholders(value);
}
if ((value == null || value.isBlank()) && req.isRequired()) {
throw new IllegalStateException("Missing required env permission '" + req.getAlias()
+ "' for module '" + moduleName + "'");
}
if (value != null) {
resolvedEnv.put(req.getAlias(), value);
}
}
for (ModuleConfig.PermissionRequirement req : moduleConfig.getSecretPermissions()) {
String binding = resolveAlias(req.getAlias(), moduleEnvMap, dotEnvMap, scriptEnv, osEnv);
String value = null;
if (binding != null && !binding.isBlank()) {
if (binding.startsWith("#secret:")) {
value = resolveSecretPlaceholders(binding);
} else if (binding.startsWith("secret:")) {
String secretName = binding.substring("secret:".length()).trim();
value = resolveSecretByName(secretName, req.isRequired());
} else {
value = binding;
}
} else {
value = resolveSecretByName(req.getAlias(), false);
}
if ((value == null || value.isBlank()) && req.isRequired()) {
throw new IllegalStateException("Missing required secret permission '" + req.getAlias()
+ "' for module '" + moduleName + "'");
}
if (value != null) {
resolvedSecrets.put(req.getAlias(), value);
}
}
Map<String, String> compatibilityEnv = new HashMap<>(scriptEnv);
if (allowDotEnvFallback) {
compatibilityEnv.putAll(dotEnvMap);
}
compatibilityEnv.putAll(moduleEnvMap);
compatibilityEnv.putAll(resolvedEnv);
compatibilityEnv.putAll(resolvedSecrets);
Map<String, String> serverEnv = strictMode
? buildStrictServerEnv(resolvedEnv, resolvedSecrets)
: compatibilityEnv;
if (strictMode && !moduleConfig.hasDeclaredPermissions()) {
warnings.add("Strict mode is enabled but module has no declared permissions; ambient env is not injected.");
}
Map<String, Object> runtimeContext = new HashMap<>();
runtimeContext.put("moduleName", moduleName);
runtimeContext.put("strictMode", strictMode);
runtimeContext.put("allowDotEnvFallback", allowDotEnvFallback);
runtimeContext.put("moduleEnvFile", Files.exists(moduleEnvPath) ? moduleEnvPath.toString() : "");
runtimeContext.put("dotEnvFile", allowDotEnvFallback && Files.exists(dotEnvPath) ? dotEnvPath.toString() : "");
return new ResolutionResult(serverEnv, resolvedEnv, resolvedSecrets, runtimeContext, warnings);
}
private String resolveAlias(String alias,
Map<String, String> moduleEnvMap,
Map<String, String> dotEnvMap,
Map<String, String> scriptEnv,
Map<String, String> osEnv) {
if (scriptEnv.containsKey(alias) && scriptEnv.get(alias) != null && !scriptEnv.get(alias).isBlank()) {
return scriptEnv.get(alias);
}
if (moduleEnvMap.containsKey(alias) && moduleEnvMap.get(alias) != null && !moduleEnvMap.get(alias).isBlank()) {
return moduleEnvMap.get(alias);
}
if (allowDotEnvFallback && dotEnvMap.containsKey(alias) && dotEnvMap.get(alias) != null && !dotEnvMap.get(alias).isBlank()) {
return dotEnvMap.get(alias);
}
if (osEnv.containsKey(alias) && osEnv.get(alias) != null && !osEnv.get(alias).isBlank()) {
return osEnv.get(alias);
}
return null;
}
private String resolveSecretPlaceholders(String value) throws Exception {
if (value == null) {
return null;
}
Matcher matcher = HASH_SECRET_PATTERN.matcher(value);
StringBuffer sb = new StringBuffer();
while (matcher.find()) {
String name = matcher.group(1).trim();
String replacement = resolveSecretByName(name, true);
matcher.appendReplacement(sb, Matcher.quoteReplacement(replacement != null ? replacement : ""));
}
matcher.appendTail(sb);
return sb.toString();
}
private String resolveSecretByName(String name, boolean required) throws Exception {
if (name == null || name.isBlank()) {
return null;
}
if (LucliSecretProviderSupport.isLocalProviderName(selectedProviderName)) {
LocalSecretStore store = getLocalSecretStore(required);
if (store == null) {
return null;
}
Optional<char[]> value = store.get(name);
if (value.isEmpty()) {
if (required) {
throw new IllegalStateException("Secret '" + name + "' not found in local secret store");
}
return null;
}
return new String(value.get());
}
String providerValue = LucliSecretProviderSupport.readSecretViaLuceeProvider(selectedProviderName, name);
if ((providerValue == null || providerValue.isBlank()) && required) {
throw new IllegalStateException(
"Secret '" + name + "' was not found for provider '" + selectedProviderName + "'."
);
}
return providerValue;
}
private LocalSecretStore getLocalSecretStore(boolean required) throws Exception {
if (localSecretStore != null) {
return localSecretStore;
}
Path storePath = LucliSecretProviderSupport.getLocalStorePath();
if (!Files.exists(storePath)) {
if (required) {
throw new IllegalStateException(
"Local secret store not found at " + storePath +
". Run '" + org.lucee.lucli.LuCLI.cliName() + " secrets init' to create it."
);
}
return null;
}
char[] passphrase = LucliSecretProviderSupport.resolvePassphrase(
passphraseOverride,
"Enter secrets passphrase to unlock module secrets: "
);
if (passphrase == null || passphrase.length == 0) {
throw new IllegalStateException("Module execution requires secrets but no passphrase is available. "
+ "Set " + LucliSecretProviderSupport.DEFAULT_PASSPHRASE_ENV + " or run in interactive mode.");
}
try {
localSecretStore = new LocalSecretStore(storePath, passphrase);
return localSecretStore;
} catch (SecretStoreException e) {
throw new IllegalStateException("Failed to open local secret store: " + e.getMessage(), e);
}
}
private Map<String, String> buildStrictServerEnv(Map<String, String> resolvedEnv, Map<String, String> resolvedSecrets) {
Map<String, String> strictEnv = new HashMap<>();
strictEnv.putAll(resolvedEnv);
strictEnv.putAll(resolvedSecrets);
return strictEnv;
}
public static class ResolutionResult {
private final Map<String, String> serverEnv;
private final Map<String, String> envVars;
private final Map<String, String> secrets;
private final Map<String, Object> runtimeContext;
private final List<String> warnings;
ResolutionResult(Map<String, String> serverEnv,
Map<String, String> envVars,
Map<String, String> secrets,
Map<String, Object> runtimeContext,
List<String> warnings) {
this.serverEnv = serverEnv;
this.envVars = envVars;
this.secrets = secrets;
this.runtimeContext = runtimeContext;
this.warnings = warnings;
}
public Map<String, String> getServerEnv() { return serverEnv; }
public Map<String, String> getEnvVars() { return envVars; }
public Map<String, String> getSecrets() { return secrets; }
public Map<String, Object> getRuntimeContext() { return runtimeContext; }
public List<String> getWarnings() { return warnings; }
}
}