mirror of
https://github.com/theonedev/onedev.git
synced 2025-12-08 18:26:30 +00:00
feat: Improve branch/tag protection rule to disallow specific file extensions (OD-2588)
This commit is contained in:
parent
5f5c114cdd
commit
7400902735
@ -8349,4 +8349,21 @@ public class DataMigrator {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void migrate213(File dataDir, Stack<Integer> versions) {
|
||||
for (File file : dataDir.listFiles()) {
|
||||
if (file.getName().startsWith("Projects.xml")) {
|
||||
VersionedXmlDoc dom = VersionedXmlDoc.fromFile(file);
|
||||
for (Element projectElement : dom.getRootElement().elements()) {
|
||||
for (Element branchProtectionElement : projectElement.element("branchProtections").elements()) {
|
||||
branchProtectionElement.addElement("disallowedFileTypes");
|
||||
}
|
||||
for (Element tagProtectionElement : projectElement.element("tagProtections").elements()) {
|
||||
tagProtectionElement.addElement("disallowedFileTypes");
|
||||
}
|
||||
}
|
||||
dom.writeToFile(file, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.util.QuotedString;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -43,6 +44,9 @@ public class ListChangedFilesCommand {
|
||||
Commandline git = newGit().workingDir(workingDir);
|
||||
git.environments().putAll(envs);
|
||||
|
||||
if (fromRev.equals(ObjectId.zeroId().name()))
|
||||
git.addArgs("ls-tree", "--name-only", toRev);
|
||||
else
|
||||
git.addArgs("diff", "--name-only", "--no-renames", fromRev + ".." + toRev);
|
||||
|
||||
git.execute(new LineConsumer() {
|
||||
|
||||
@ -158,6 +158,12 @@ public class GitPreReceiveCallback extends HttpServlet {
|
||||
if (commitMessageError != null)
|
||||
errorMessages.add(commitMessageError.toString());
|
||||
}
|
||||
if (errorMessages.isEmpty() && !protection.getDisallowedFileTypes().isEmpty()) {
|
||||
var violatedFileTypes = protection.getViolatedFileTypes(project, oldObjectId, newObjectId, gitEnvs);
|
||||
if (!violatedFileTypes.isEmpty()) {
|
||||
errorMessages.add("Your push contains disallowed file type(s): " + StringUtils.join(violatedFileTypes, ", "));
|
||||
}
|
||||
}
|
||||
if (errorMessages.isEmpty()
|
||||
&& !oldObjectId.equals(ObjectId.zeroId())
|
||||
&& !newObjectId.equals(ObjectId.zeroId())
|
||||
@ -202,6 +208,12 @@ public class GitPreReceiveCallback extends HttpServlet {
|
||||
errorMessages.add("Can not update this tag as tag protection setting requires "
|
||||
+ "valid tag signature");
|
||||
}
|
||||
if (errorMessages.isEmpty() && !protection.getDisallowedFileTypes().isEmpty()) {
|
||||
var violatedFileTypes = protection.getViolatedFileTypes(project, newObjectId, gitEnvs);
|
||||
if (!violatedFileTypes.isEmpty()) {
|
||||
errorMessages.add("Your push contains disallowed file type(s): " + StringUtils.join(violatedFileTypes, ", "));
|
||||
}
|
||||
}
|
||||
if (errorMessages.isEmpty()) {
|
||||
for (var preReceiveChecker : preReceiveCheckers) {
|
||||
var errorMessage = preReceiveChecker.check(project, user, refName, oldObjectId, newObjectId);
|
||||
|
||||
@ -1608,6 +1608,12 @@ public class Project extends AbstractEntity implements LabelSupport<ProjectLabel
|
||||
return getGitService().checkCommitMessages(protection, this, oldCommitId, newCommitId, gitEnvs);
|
||||
}
|
||||
|
||||
public Collection<String> getViolatedFileTypes(String branch, User user, ObjectId oldCommitId, ObjectId newCommitId,
|
||||
Map<String, String> gitEnvs) {
|
||||
var protection = getBranchProtection(branch, user);
|
||||
return protection.getViolatedFileTypes(this, oldCommitId, newCommitId, gitEnvs);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<String> readLines(BlobIdent blobIdent, WhitespaceOption whitespaceOption, boolean mustExist) {
|
||||
Blob blob = getBlob(blobIdent, mustExist);
|
||||
|
||||
@ -455,6 +455,8 @@ public class PullRequest extends ProjectBelonging
|
||||
|
||||
private transient Optional<CommitMessageError> commitMessageErrorOpt;
|
||||
|
||||
private transient Optional<Collection<String>> violatedFileTypesOpt;
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
@ -1198,6 +1200,9 @@ public class PullRequest extends ProjectBelonging
|
||||
var commitMessageError = checkCommitMessages();
|
||||
if (commitMessageError != null)
|
||||
return commitMessageError.toString();
|
||||
var violatedFileTypes = getViolatedFileTypes();
|
||||
if (!violatedFileTypes.isEmpty())
|
||||
return MessageFormat.format(_T("Disallowed file type(s): {0}"), StringUtils.join(violatedFileTypes, ", "));
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -1318,6 +1323,15 @@ public class PullRequest extends ProjectBelonging
|
||||
return commitMessageErrorOpt.orElse(null);
|
||||
}
|
||||
|
||||
public Collection<String> getViolatedFileTypes() {
|
||||
if (violatedFileTypesOpt == null) {
|
||||
var violatedFileTypes = getProject().getViolatedFileTypes(getTargetBranch(), getSubmitter(),
|
||||
getBaseCommit().copy(), getLatestUpdate().getHeadCommit().copy(), new HashMap<>());
|
||||
violatedFileTypesOpt = Optional.of(violatedFileTypes);
|
||||
}
|
||||
return violatedFileTypesOpt.get();
|
||||
}
|
||||
|
||||
public static String getSerialLockName(Long requestId) {
|
||||
return "pull-request-" + requestId + "-serial";
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import static java.util.regex.Pattern.UNICODE_CHARACTER_CLASS;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
@ -82,6 +83,8 @@ public class BranchProtection implements Serializable {
|
||||
|
||||
private Integer maxCommitMessageLineLength;
|
||||
|
||||
private List<String> disallowedFileTypes = new ArrayList<>();
|
||||
|
||||
private String reviewRequirement;
|
||||
|
||||
private transient ReviewRequirement parsedReviewRequirement;
|
||||
@ -262,6 +265,15 @@ public class BranchProtection implements Serializable {
|
||||
reviewRequirement = parsedReviewRequirement.toString();
|
||||
}
|
||||
|
||||
@Editable(order=410, placeholder = "No disallowed file types", description = "Optionally specify disallowed file types by extensions (hit ENTER to add value), for instance <code>exe</code>, <code>bin</code>. Leave empty to allow all file types")
|
||||
public List<String> getDisallowedFileTypes() {
|
||||
return disallowedFileTypes;
|
||||
}
|
||||
|
||||
public void setDisallowedFileTypes(List<String> disallowedFileTypes) {
|
||||
this.disallowedFileTypes = disallowedFileTypes;
|
||||
}
|
||||
|
||||
@Editable(order=500, name="Required Builds", placeholder="No any", description="Optionally choose required builds. You may also " +
|
||||
"input jobs not listed here, and press ENTER to add them")
|
||||
@JobChoice(tagsMode=true)
|
||||
@ -422,6 +434,18 @@ public class BranchProtection implements Serializable {
|
||||
return false;
|
||||
}
|
||||
|
||||
public Collection<String> getViolatedFileTypes(Project project, ObjectId oldObjectId, ObjectId newObjectId,
|
||||
Map<String, String> gitEnvs) {
|
||||
if (disallowedFileTypes.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
var changedFiles = getGitService().getChangedFiles(project, oldObjectId, newObjectId, gitEnvs);
|
||||
return getDisallowedFileTypes().stream()
|
||||
.filter(type -> changedFiles.stream().anyMatch(file -> file.toLowerCase().endsWith("." + type.toLowerCase())))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
public BuildRequirement getBuildRequirement(Project project, ObjectId oldObjectId, ObjectId newObjectId,
|
||||
Map<String, String> gitEnvs) {
|
||||
Collection<String> requiredJobs = new LinkedHashSet<>(getJobNames());
|
||||
|
||||
@ -1,8 +1,22 @@
|
||||
package io.onedev.server.model.support.code;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
|
||||
import io.onedev.commons.codeassist.InputSuggestion;
|
||||
import io.onedev.server.OneDev;
|
||||
import io.onedev.server.annotation.Editable;
|
||||
import io.onedev.server.annotation.Patterns;
|
||||
import io.onedev.server.git.service.GitService;
|
||||
import io.onedev.server.model.Project;
|
||||
import io.onedev.server.util.patternset.PatternSet;
|
||||
import io.onedev.server.util.usage.Usage;
|
||||
@ -10,11 +24,6 @@ import io.onedev.server.util.usermatch.Anyone;
|
||||
import io.onedev.server.util.usermatch.UserMatch;
|
||||
import io.onedev.server.web.util.SuggestionUtils;
|
||||
|
||||
import javax.validation.constraints.NotEmpty;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Editable
|
||||
public class TagProtection implements Serializable {
|
||||
|
||||
@ -34,6 +43,8 @@ public class TagProtection implements Serializable {
|
||||
|
||||
private boolean commitSignatureRequired;
|
||||
|
||||
private List<String> disallowedFileTypes = new ArrayList<>();
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
@ -115,6 +126,15 @@ public class TagProtection implements Serializable {
|
||||
this.commitSignatureRequired = commitSignatureRequired;
|
||||
}
|
||||
|
||||
@Editable(order=410, placeholder = "No disallowed file types", description = "Optionally specify disallowed file types by extensions (hit ENTER to add value), for instance <code>exe</code>, <code>bin</code>. Leave empty to allow all file types")
|
||||
public List<String> getDisallowedFileTypes() {
|
||||
return disallowedFileTypes;
|
||||
}
|
||||
|
||||
public void setDisallowedFileTypes(List<String> disallowedFileTypes) {
|
||||
this.disallowedFileTypes = disallowedFileTypes;
|
||||
}
|
||||
|
||||
public void onRenameGroup(String oldName, String newName) {
|
||||
userMatch = UserMatch.onRenameGroup(userMatch, oldName, newName);
|
||||
}
|
||||
@ -145,4 +165,19 @@ public class TagProtection implements Serializable {
|
||||
return usage;
|
||||
}
|
||||
|
||||
public Collection<String> getViolatedFileTypes(Project project, ObjectId objectId, Map<String, String> gitEnvs) {
|
||||
if (disallowedFileTypes.isEmpty()) {
|
||||
return Collections.emptySet();
|
||||
} else {
|
||||
var files = getGitService().getChangedFiles(project, ObjectId.zeroId(), objectId, gitEnvs);
|
||||
return getDisallowedFileTypes().stream()
|
||||
.filter(type -> files.stream().anyMatch(file -> file.toLowerCase().endsWith("." + type.toLowerCase())))
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
}
|
||||
|
||||
private GitService getGitService() {
|
||||
return OneDev.getInstance(GitService.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -4,8 +4,8 @@ requirement: WS* criteria (WS+ criteria)* WS* EOF;
|
||||
|
||||
criteria: userCriteria | groupCriteria;
|
||||
|
||||
userCriteria: USER WS* Value;
|
||||
groupCriteria: GROUP WS* Value (WS*':' WS* DIGIT)?;
|
||||
userCriteria: USER Value;
|
||||
groupCriteria: GROUP Value (':' DIGIT)?;
|
||||
|
||||
DIGIT: [1-9][0-9]*;
|
||||
|
||||
|
||||
@ -1539,11 +1539,17 @@ public class ProjectBlobPage extends ProjectPage implements BlobRenderContext,
|
||||
String blobPath = FilenameUtils.sanitizeFileName(FileUpload.getFileName(item));
|
||||
if (parentPath != null)
|
||||
blobPath = parentPath + "/" + blobPath;
|
||||
var blobType = StringUtils.substringAfterLast(blobPath, ".");
|
||||
|
||||
var disallowedFileTypes = getProject().getBranchProtection(blobIdent.revision, user).getDisallowedFileTypes();
|
||||
if (disallowedFileTypes.stream().anyMatch(type -> blobType.equalsIgnoreCase(type))) {
|
||||
throw new BlobEditException(MessageFormat.format(_T("Not allowed file type: {0}"), blobType));
|
||||
}
|
||||
|
||||
if (getProject().isReviewRequiredForModification(user, blobIdent.revision, blobPath))
|
||||
throw new BlobEditException("Review required for this change. Please submit pull request instead");
|
||||
throw new BlobEditException(_T("Review required for this change. Please submit pull request instead"));
|
||||
else if (getProject().isBuildRequiredForModification(user, blobIdent.revision, blobPath))
|
||||
throw new BlobEditException("Build required for this change. Please submit pull request instead");
|
||||
throw new BlobEditException(_T("Build required for this change. Please submit pull request instead"));
|
||||
else if (getProject().isCommitSignatureRequiredButNoSigningKey(user, blobIdent.revision))
|
||||
signRequired = true;
|
||||
|
||||
|
||||
@ -274,16 +274,25 @@ public class CommitOptionPanel extends Panel {
|
||||
Map<String, BlobContent> newBlobs = new HashMap<>();
|
||||
if (newContentProvider != null) {
|
||||
String newPath = context.getNewPath();
|
||||
var blobType = StringUtils.substringAfterLast(newPath, ".");
|
||||
|
||||
var disallowedFileTypes = context.getProject().getBranchProtection(revision, user).getDisallowedFileTypes();
|
||||
if (disallowedFileTypes.stream().anyMatch(type -> blobType.equalsIgnoreCase(type))) {
|
||||
form.error(MessageFormat.format(_T("Not allowed file type: {0}"), blobType));
|
||||
target.add(form);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (context.getProject().isReviewRequiredForModification(user, revision, newPath)) {
|
||||
form.error("Review required for this change. Please submit pull request instead");
|
||||
form.error(_T("Review required for this change. Please submit pull request instead"));
|
||||
target.add(form);
|
||||
return false;
|
||||
} else if (context.getProject().isBuildRequiredForModification(user, revision, newPath)) {
|
||||
form.error("Build required for this change. Please submit pull request instead");
|
||||
form.error(_T("Build required for this change. Please submit pull request instead"));
|
||||
target.add(form);
|
||||
return false;
|
||||
} else if (context.getProject().isCommitSignatureRequiredButNoSigningKey(user, revision)) {
|
||||
form.error("Signature required for this change, but no signing key is specified");
|
||||
form.error(_T("Signature required for this change, but no signing key is specified"));
|
||||
target.add(form);
|
||||
return false;
|
||||
}
|
||||
@ -307,13 +316,11 @@ public class CommitOptionPanel extends Panel {
|
||||
ExceptionUtils.find(e, ObsoleteCommitException.class);
|
||||
|
||||
if (objectAlreadyExistsException != null) {
|
||||
form.error("A path with same name already exists. "
|
||||
+ "Please choose a different name and try again.");
|
||||
form.error(_T("A path with same name already exists.Please choose a different name and try again."));
|
||||
target.add(form);
|
||||
break;
|
||||
} else if (notTreeException != null) {
|
||||
form.error("A file exists where you’re trying to create a subdirectory. "
|
||||
+ "Choose a new path and try again..");
|
||||
form.error(_T("A file exists where you’re trying to create a subdirectory. Choose a new path and try again.."));
|
||||
target.add(form);
|
||||
break;
|
||||
} else if (obsoleteCommitException != null) {
|
||||
@ -333,14 +340,14 @@ public class CommitOptionPanel extends Panel {
|
||||
if (newContentProvider != null) {
|
||||
oldPaths.clear();
|
||||
changesOfOthers = getBlobChange(path, pathChange, lastPrevCommitId, prevCommitId);
|
||||
form.warn("Someone made below change since you started editing");
|
||||
form.warn(_T("Someone made below change since you started editing"));
|
||||
break;
|
||||
} else {
|
||||
newCommitId = obsoleteCommitException.getOldCommitId();
|
||||
}
|
||||
} else {
|
||||
changesOfOthers = getBlobChange(path, pathChange, lastPrevCommitId, prevCommitId);
|
||||
form.warn("Someone made below change since you started editing");
|
||||
form.warn(_T("Someone made below change since you started editing"));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -94,6 +94,11 @@
|
||||
<wicket:svg href="times-circle" class="icon"></wicket:svg> <span wicket:id="commitMessageCheckError"></span>
|
||||
</div>
|
||||
</wicket:enclosure>
|
||||
<wicket:enclosure child="fileTypesCheckError">
|
||||
<div class="file-types-check-error text-warning">
|
||||
<wicket:svg href="times-circle" class="icon"></wicket:svg> <span wicket:id="fileTypesCheckError"></span>
|
||||
</div>
|
||||
</wicket:enclosure>
|
||||
<wicket:enclosure child="requiredJobs">
|
||||
<div class="required-jobs text-warning">
|
||||
<wicket:svg href="warning" class="icon"></wicket:svg>
|
||||
|
||||
@ -724,6 +724,27 @@ public abstract class PullRequestDetailPage extends ProjectPage implements PullR
|
||||
}
|
||||
}.setEscapeModelStrings(false));
|
||||
|
||||
summaryContainer.add(new Label("fileTypesCheckError", new LoadableDetachableModel<>() {
|
||||
@Override
|
||||
protected String load() {
|
||||
var violatedFileTypes = getPullRequest().getViolatedFileTypes();
|
||||
if (violatedFileTypes.isEmpty())
|
||||
return null;
|
||||
else
|
||||
return MessageFormat.format(_T("The change contains disallowed file type(s): {0}"), StringUtils.join(violatedFileTypes, ", "));
|
||||
}
|
||||
}) {
|
||||
@Override
|
||||
protected void onConfigure() {
|
||||
super.onConfigure();
|
||||
PullRequest request = getPullRequest();
|
||||
if (request.isOpen())
|
||||
setVisible(getDefaultModelObject() != null);
|
||||
else
|
||||
setVisible(false);
|
||||
}
|
||||
}.setEscapeModelStrings(false));
|
||||
|
||||
summaryContainer.add(new Label("requiredJobsMessage", new AbstractReadOnlyModel<String>() {
|
||||
@Override
|
||||
public String getObject() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user