Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add restrictions on names of domains #6140

Merged
merged 20 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions api/src/org/labkey/api/audit/AuditHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import org.labkey.api.exp.api.ExpMaterial;
import org.labkey.api.exp.api.ExperimentService;
import org.labkey.api.gwt.client.AuditBehaviorType;
import org.labkey.api.query.QueryKey;
import org.labkey.api.query.QueryService;
import org.labkey.api.security.User;
import org.labkey.api.util.Pair;
Expand Down Expand Up @@ -72,11 +73,21 @@ static Pair<Map<String, Object>, Map<String, Object>> getOldAndNewRecordForMerge
.orElse(key);
String lcName = nameFromAlias.toLowerCase();
// Preserve casing of inputs so we can show the names properly
if (!lcName.startsWith(ExpData.DATA_INPUT_PARENT.toLowerCase()) && !lcName.startsWith(ExpMaterial.MATERIAL_INPUT_PARENT.toLowerCase()))
boolean isExpInput = false;
if (lcName.startsWith(ExpData.DATA_INPUTS_PREFIX.toLowerCase()) || lcName.startsWith(ExpMaterial.MATERIAL_INPUTS_PREFIX.toLowerCase()))
{
String[] parts = nameFromAlias.split("/", 2);
String prefix = parts[0];
String dataType = parts[1];
// MaterialInputs/datypeTypeEncoded
if (row.containsKey(prefix + "/" + QueryKey.encodePart(dataType)))
isExpInput = true;
}
else
nameFromAlias = lcName;

boolean isExtraAuditField = extraFieldsToInclude != null && extraFieldsToInclude.contains(nameFromAlias);
if (!excludedFromDetailDiff.contains(nameFromAlias) && row.containsKey(nameFromAlias))
if (!excludedFromDetailDiff.contains(nameFromAlias) && (row.containsKey(nameFromAlias) || isExpInput))
{
Object oldValue = entry.getValue();
Object newValue = row.get(nameFromAlias);
Expand Down
132 changes: 92 additions & 40 deletions api/src/org/labkey/api/data/NameGenerator.java

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions api/src/org/labkey/api/exp/api/ExperimentService.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.labkey.api.data.ContainerFilter;
import org.labkey.api.data.DbSchema;
import org.labkey.api.data.DbScope;
import org.labkey.api.data.NameGenerator;
import org.labkey.api.data.RemapCache;
import org.labkey.api.data.SQLFragment;
import org.labkey.api.data.TableInfo;
Expand Down Expand Up @@ -75,6 +76,7 @@
import org.labkey.api.security.User;
import org.labkey.api.services.ServiceRegistry;
import org.labkey.api.util.Pair;
import org.labkey.api.util.StringUtilsLabKey;
import org.labkey.api.view.HttpView;
import org.labkey.api.view.NotFoundException;
import org.labkey.api.view.ViewBackgroundInfo;
Expand Down Expand Up @@ -118,6 +120,8 @@ public interface ExperimentService extends ExperimentRunTypeSource

String LINEAGE_DEFAULT_MAXIMUM_DEPTH_PROPERTY_NAME = "lineageDefaultMaximumDepth";

String ILLEGAL_PARENT_ALIAS_CHARSET = "/:<>$[]{};,`\"~!@#$%^*=|?\\";

int SIMPLE_PROTOCOL_FIRST_STEP_SEQUENCE = 1;
int SIMPLE_PROTOCOL_CORE_STEP_SEQUENCE = 10;
int SIMPLE_PROTOCOL_EXTRA_STEP_SEQUENCE = 15;
Expand Down Expand Up @@ -539,6 +543,14 @@ static void validateParentAlias(Map<String, String> aliasMap, Set<String> reserv
throw new IllegalArgumentException(String.format("Duplicate parent alias header found: %1$s", trimmedKey));
}

String legalCharacterCheck = StringUtilsLabKey.validateLegalNames(trimmedKey, ILLEGAL_PARENT_ALIAS_CHARSET, "Parent alias");
if (legalCharacterCheck != null)
throw new IllegalArgumentException(legalCharacterCheck);

String nameSyntaxError = NameGenerator.validateFieldKeyConflict(trimmedKey);
if (nameSyntaxError != null)
throw new IllegalArgumentException("Invalid Parent alias '" + trimmedKey + "'. " + nameSyntaxError);

//Check if parent alias has correct format MaterialInput/<name> or NEW_SAMPLE_TYPE_ALIAS_VALUE, or DataInput/<name> or NEW_DATA_CLASS_ALIAS_VALUE
if (!ExperimentService.parentAliasHasCorrectFormat(trimmedValue))
throw new IllegalArgumentException(String.format("Invalid parent alias header: %1$s", trimmedValue));
Expand Down
6 changes: 6 additions & 0 deletions api/src/org/labkey/api/exp/api/SampleTypeDomainKind.java
Original file line number Diff line number Diff line change
Expand Up @@ -671,4 +671,10 @@ public boolean supportsPhiLevel()
{
return ComplianceService.get().isComplianceSupported();
}

@Override
public boolean supportsNamingPattern()
{
return true;
}
}
5 changes: 5 additions & 0 deletions api/src/org/labkey/api/exp/property/DomainKind.java
Original file line number Diff line number Diff line change
Expand Up @@ -410,4 +410,9 @@ public boolean supportsPhiLevel()
{
return false;
}

public boolean supportsNamingPattern()
{
return false;
}
}
65 changes: 59 additions & 6 deletions api/src/org/labkey/api/exp/property/DomainUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import org.labkey.api.data.ContainerFilter;
import org.labkey.api.data.ContainerManager;
import org.labkey.api.data.ContainerService;
import org.labkey.api.data.NameGenerator;
import org.labkey.api.data.PHI;
import org.labkey.api.data.SchemaTableInfo;
import org.labkey.api.data.SimpleFilter;
Expand Down Expand Up @@ -76,6 +77,7 @@
import org.labkey.api.util.JsonUtil;
import org.labkey.api.util.Pair;
import org.labkey.api.util.StringExpression;
import org.labkey.api.util.StringUtilsLabKey;
import org.labkey.api.view.UnauthorizedException;
import org.labkey.data.xml.ColumnType;
import org.labkey.data.xml.ConditionalFormatFilterType;
Expand Down Expand Up @@ -104,6 +106,8 @@
public class DomainUtil
{
private static final Logger LOG = LogManager.getLogger(DomainUtil.class);
public static final String ILLEGAL_DOMAIN_NAME_CHARSET = "<>[]{};,`\"~!@#$%^*=|?\\";

private DomainUtil()
{
}
Expand Down Expand Up @@ -688,10 +692,6 @@ public static Domain createDomain(
{
// Create a copy of the GWTDomain to ensure the template's Domain is not modified
domain = new GWTDomain(domain);
if (domainName != null)
{
domain.setName(domainName);
}

DomainKind kind = PropertyService.get().getDomainKindByName(kindName);
if (kind == null)
Expand All @@ -700,6 +700,17 @@ public static Domain createDomain(
if (!kind.canCreateDefinition(user, container))
throw new UnauthorizedException("You don't have permission to create a new domain");


if (domainName != null)
domain.setName(domainName);

if (domain.getName() != null)
{
String domainNameError = validateDomainName(domain.getName(), kind.getKindName(), kind.supportsNamingPattern());
if (!StringUtils.isEmpty(domainNameError))
throw new IllegalArgumentException(domainNameError);
}

// Issue 48810: if not creating from templateInfo, validate reserved field names based on domainKind
boolean strictFieldValidation = arguments != null ? (Boolean) arguments.getOrDefault("strictFieldValidation", true) : true;
DomainKind validateDomainKind = templateInfo == null && strictFieldValidation ? kind : null;
Expand Down Expand Up @@ -754,10 +765,17 @@ public static ValidationException updateDomainDescriptor(GWTDomain<? extends GWT
return validationException;
}

if (updateDomainName && !d.getName().equals(update.getName()))
String updatedName = update.getName();
if (updateDomainName && !d.getName().equals(updatedName))
{
String domainNameError = validateDomainName(updatedName, kind.getKindName(), kind.supportsNamingPattern());
if (!StringUtils.isEmpty(domainNameError))
{
validationException.addError(new SimpleValidationError(domainNameError));
return validationException;
}
DefaultValueService.get().clearDefaultValues(d.getContainer(), d); // default values exp.objects will be re-created
d.setName(update.getName());
d.setName(updatedName);
}

d.setDisabledSystemFields(kind.getDisabledSystemFields(update.getDisabledSystemFields()));
Expand Down Expand Up @@ -917,6 +935,31 @@ public static ValidationException updateDomainDescriptor(GWTDomain<? extends GWT
return validationException;
}

public static @Nullable String validateDomainName(@NotNull String domainName, String kindName, boolean supportsNamingPattern)
{
String prefix = "Invalid " + kindName + " name \"" + domainName + "\". ";

if (StringUtils.isBlank(domainName))
return kindName + " name must not be blank.";

char start = domainName.charAt(0);
if (!Character.isLetterOrDigit(start))
return prefix + kindName + " name must start with a letter or a number.";

String legalCharacterCheck = StringUtilsLabKey.validateLegalNames(domainName, ILLEGAL_DOMAIN_NAME_CHARSET, kindName + " name");
if (legalCharacterCheck != null)
return prefix + legalCharacterCheck;

if (supportsNamingPattern)
{
String invalidPattenError = NameGenerator.validateFieldKeyConflict(domainName);
if (invalidPattenError != null)
return prefix + invalidPattenError;
}

return null;
}

private static String getMissingMandatoryField(List<? extends GWTPropertyDescriptor> updatedFields,
List<? extends GWTPropertyDescriptor> origFields)
{
Expand Down Expand Up @@ -1333,6 +1376,16 @@ public static ValidationException validateProperties(@Nullable Domain domain, @N
continue;
}

if (domainKind != null && domainKind.supportsNamingPattern())
{
String nameSyntaxError = NameGenerator.validateFieldKeyConflict(name);
if (nameSyntaxError != null)
{
exception.addFieldError(name, nameSyntaxError);
continue;
}
}

if (namePropertyIdMap.containsKey(name))
{
String errorMsg = getDomainErrorMessage(updates,"The field name '" + name + "' is already taken. Please provide a unique name for each field.");
Expand Down
15 changes: 1 addition & 14 deletions api/src/org/labkey/api/util/FileUtil.java
Original file line number Diff line number Diff line change
Expand Up @@ -334,20 +334,7 @@ static String isAllowedFileName(String s, boolean checkFileExtension, AppProps a

private static @Nullable String validateFileName(String s)
{
if (StringUtils.isBlank(s))
return "Filename must not be blank";
if (!ViewServlet.validChars(s))
return "Filename must contain only valid unicode characters.";
if (StringUtils.containsAny(s, restrictedPrintable))
return "Filename may not contain any of these characters: " + restrictedPrintable;
if (StringUtils.containsAny(s, "\t\n\r"))
return "Filename may not contain 'tab', 'new line', or 'return' characters.";
if (StringUtils.contains("-$", s.charAt(0)))
return "Filename may not begin with any of these characters: -$";
if (Pattern.matches("(.*\\s--[^ ].*)|(.*\\s-[^- ].*)", s))
return "Filename may not contain space followed by dash.";

return null;
return StringUtilsLabKey.validateLegalNames(s, restrictedPrintable, "Filename");
}

private static String checkExtension(String filename, AppProps appProps)
Expand Down
21 changes: 21 additions & 0 deletions api/src/org/labkey/api/util/StringUtilsLabKey.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import org.junit.Test;
import org.labkey.api.data.Container;
import org.labkey.api.exp.Identifiable;
import org.labkey.api.view.ViewServlet;

import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
Expand Down Expand Up @@ -52,6 +53,26 @@ public class StringUtilsLabKey
private static final Random RANDOM = new Random();
private static final int MAX_LONG_LENGTH = String.valueOf(Long.MAX_VALUE).length() - 1;



public static @Nullable String validateLegalNames(String s, @NotNull String illegalCharset, String type)
{
if (StringUtils.isBlank(s))
return type + " must not be blank.";
if (!ViewServlet.validChars(s))
return type + " must contain only valid unicode characters.";
if (StringUtils.containsAny(s, illegalCharset))
return type + " may not contain any of these characters: " + illegalCharset;
if (StringUtils.containsAny(s, "\t\n\r"))
return type + " may not contain 'tab', 'new line', or 'return' characters.";
if (StringUtils.contains("-$", s.charAt(0)))
return type + " may not begin with any of these characters: -$";
if (Pattern.matches("(.*\\s--[^ ].*)|(.*\\s-[^- ].*)", s))
return type + " may not contain space followed by dash.";

return null;
}

public static void append(StringBuilder sb, @Nullable Identifiable identifiable)
{
if (null != identifiable)
Expand Down
10 changes: 10 additions & 0 deletions assay/src/org/labkey/assay/AssayDomainServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@ public GWTProtocol saveChanges(GWTProtocol assay, boolean replaceIfExisting) thr
if (AssayManager.get().getAssayProtocolByName(getContainer(), assay.getName()) != null)
throw new ValidationException("Assay protocol already exists for this name.");

String nameError = DomainUtil.validateDomainName(assay.getName(), "Assay Design", false);
if (nameError != null)
throw new ValidationException(nameError);

XarContext context = new XarContext("Domains", getContainer(), getUser());
context.addSubstitution("AssayName", PageFlowUtil.encode(assay.getName()));

Expand Down Expand Up @@ -437,6 +441,12 @@ public GWTProtocol saveChanges(GWTProtocol assay, boolean replaceIfExisting) thr
"This assay was created in folder " + protocol.getContainer().getPath());
oldAssayName = protocol.getName();
hasNameChange = !assay.getName().equals(oldAssayName);
if (hasNameChange)
{
String nameError = DomainUtil.validateDomainName(assay.getName(), "Assay Design", false);
if (nameError != null)
throw new ValidationException(nameError);
}
protocol.setName(assay.getName());
protocol.setProtocolDescription(assay.getDescription());
if (assay.getStatus() != null)
Expand Down
8 changes: 4 additions & 4 deletions core/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
}
},
"dependencies": {
"@labkey/components": "6.4.0",
"@labkey/components": "6.5.3",
"@labkey/themes": "1.4.0"
},
"devDependencies": {
Expand Down
8 changes: 4 additions & 4 deletions experiment/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion experiment/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"test-integration": "cross-env NODE_ENV=test jest --ci --runInBand -c test/js/jest.config.integration.js"
},
"dependencies": {
"@labkey/components": "6.4.0"
"@labkey/components": "6.5.3"
},
"devDependencies": {
"@labkey/build": "8.3.0",
Expand Down
Loading