org.projectnessie.services.impl.TreeApiImpl Maven / Gradle / Ivy
The newest version!
/*
* Copyright (C) 2020 Dremio
*
* Licensed 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 org.projectnessie.services.impl;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.Sets.newHashSetWithExpectedSize;
import static java.util.Collections.emptyList;
import static java.util.Collections.emptyMap;
import static java.util.Collections.singleton;
import static java.util.function.Function.identity;
import static org.projectnessie.model.CommitResponse.AddedContent.addedContent;
import static org.projectnessie.services.authz.Check.canReadContentKey;
import static org.projectnessie.services.authz.Check.canReadEntries;
import static org.projectnessie.services.authz.Check.canViewReference;
import static org.projectnessie.services.cel.CELUtil.COMMIT_LOG_DECLARATIONS;
import static org.projectnessie.services.cel.CELUtil.COMMIT_LOG_TYPES;
import static org.projectnessie.services.cel.CELUtil.CONTAINER;
import static org.projectnessie.services.cel.CELUtil.ENTRIES_DECLARATIONS;
import static org.projectnessie.services.cel.CELUtil.ENTRIES_TYPES;
import static org.projectnessie.services.cel.CELUtil.REFERENCES_DECLARATIONS;
import static org.projectnessie.services.cel.CELUtil.REFERENCES_TYPES;
import static org.projectnessie.services.cel.CELUtil.SCRIPT_HOST;
import static org.projectnessie.services.cel.CELUtil.VAR_COMMIT;
import static org.projectnessie.services.cel.CELUtil.VAR_ENTRY;
import static org.projectnessie.services.cel.CELUtil.VAR_OPERATIONS;
import static org.projectnessie.services.cel.CELUtil.VAR_REF;
import static org.projectnessie.services.cel.CELUtil.VAR_REF_META;
import static org.projectnessie.services.cel.CELUtil.VAR_REF_TYPE;
import static org.projectnessie.services.impl.RefUtil.toNamedRef;
import static org.projectnessie.versioned.RequestMeta.API_WRITE;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import jakarta.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.projectnessie.cel.tools.Script;
import org.projectnessie.cel.tools.ScriptException;
import org.projectnessie.error.NessieConflictException;
import org.projectnessie.error.NessieNotFoundException;
import org.projectnessie.error.NessieReferenceAlreadyExistsException;
import org.projectnessie.error.NessieReferenceConflictException;
import org.projectnessie.error.NessieReferenceNotFoundException;
import org.projectnessie.model.Branch;
import org.projectnessie.model.CommitMeta;
import org.projectnessie.model.CommitResponse;
import org.projectnessie.model.Content;
import org.projectnessie.model.ContentKey;
import org.projectnessie.model.EntriesResponse.Entry;
import org.projectnessie.model.FetchOption;
import org.projectnessie.model.IdentifiedContentKey;
import org.projectnessie.model.ImmutableCommitResponse;
import org.projectnessie.model.ImmutableContentKeyDetails;
import org.projectnessie.model.ImmutableLogEntry;
import org.projectnessie.model.ImmutableMergeResponse;
import org.projectnessie.model.ImmutableReferenceHistoryResponse;
import org.projectnessie.model.ImmutableReferenceMetadata;
import org.projectnessie.model.LogResponse.LogEntry;
import org.projectnessie.model.MergeBehavior;
import org.projectnessie.model.MergeKeyBehavior;
import org.projectnessie.model.MergeResponse;
import org.projectnessie.model.MergeResponse.ContentKeyConflict;
import org.projectnessie.model.Operation;
import org.projectnessie.model.Operation.Delete;
import org.projectnessie.model.Operation.Put;
import org.projectnessie.model.Operations;
import org.projectnessie.model.Reference;
import org.projectnessie.model.Reference.ReferenceType;
import org.projectnessie.model.ReferenceHistoryResponse;
import org.projectnessie.model.ReferenceHistoryState;
import org.projectnessie.model.ReferenceMetadata;
import org.projectnessie.model.Tag;
import org.projectnessie.model.Validation;
import org.projectnessie.services.authz.AccessContext;
import org.projectnessie.services.authz.ApiContext;
import org.projectnessie.services.authz.Authorizer;
import org.projectnessie.services.authz.AuthzPaginationIterator;
import org.projectnessie.services.authz.BatchAccessChecker;
import org.projectnessie.services.authz.Check;
import org.projectnessie.services.authz.RetriableAccessChecker;
import org.projectnessie.services.cel.CELUtil;
import org.projectnessie.services.config.ServerConfig;
import org.projectnessie.services.hash.HashValidator;
import org.projectnessie.services.hash.ResolvedHash;
import org.projectnessie.services.spi.PagedResponseHandler;
import org.projectnessie.services.spi.TreeService;
import org.projectnessie.versioned.BranchName;
import org.projectnessie.versioned.Commit;
import org.projectnessie.versioned.GetNamedRefsParams;
import org.projectnessie.versioned.GetNamedRefsParams.RetrieveOptions;
import org.projectnessie.versioned.Hash;
import org.projectnessie.versioned.KeyEntry;
import org.projectnessie.versioned.MergeConflictException;
import org.projectnessie.versioned.MergeResult;
import org.projectnessie.versioned.MergeTransplantResultBase;
import org.projectnessie.versioned.NamedRef;
import org.projectnessie.versioned.ReferenceAlreadyExistsException;
import org.projectnessie.versioned.ReferenceConflictException;
import org.projectnessie.versioned.ReferenceHistory;
import org.projectnessie.versioned.ReferenceInfo;
import org.projectnessie.versioned.ReferenceNotFoundException;
import org.projectnessie.versioned.RequestMeta;
import org.projectnessie.versioned.TagName;
import org.projectnessie.versioned.TransplantResult;
import org.projectnessie.versioned.VersionStore;
import org.projectnessie.versioned.VersionStore.CommitValidator;
import org.projectnessie.versioned.VersionStore.MergeOp;
import org.projectnessie.versioned.VersionStore.TransplantOp;
import org.projectnessie.versioned.WithHash;
import org.projectnessie.versioned.paging.PaginationIterator;
public class TreeApiImpl extends BaseApiImpl implements TreeService {
public TreeApiImpl(
ServerConfig config,
VersionStore store,
Authorizer authorizer,
AccessContext accessContext,
ApiContext apiContext) {
super(config, store, authorizer, accessContext, apiContext);
}
@Override
public R getAllReferences(
FetchOption fetchOption,
String filter,
String pagingToken,
PagedResponseHandler pagedResponseHandler) {
boolean fetchAll = FetchOption.isFetchAll(fetchOption);
try (PaginationIterator> references =
getStore().getNamedRefs(getGetNamedRefsParams(fetchAll), pagingToken)) {
AuthzPaginationIterator> authz =
new AuthzPaginationIterator>(
references, super::startAccessCheck, getServerConfig().accessChecksBatchSize()) {
@Override
protected Set checksForEntry(ReferenceInfo entry) {
return singleton(canViewReference(entry.getNamedRef()));
}
};
Predicate filterPredicate = filterReferences(filter);
while (authz.hasNext()) {
ReferenceInfo refInfo = authz.next();
Reference ref = makeReference(refInfo, fetchAll);
if (!filterPredicate.test(ref)) {
continue;
}
if (!pagedResponseHandler.addEntry(ref)) {
pagedResponseHandler.hasMore(authz.tokenForCurrent());
break;
}
}
} catch (ReferenceNotFoundException e) {
throw new IllegalArgumentException(
String.format(
"Could not find default branch '%s'.", this.getServerConfig().getDefaultBranch()));
}
return pagedResponseHandler.build();
}
private GetNamedRefsParams getGetNamedRefsParams(boolean fetchMetadata) {
return fetchMetadata
? GetNamedRefsParams.builder()
.baseReference(BranchName.of(this.getServerConfig().getDefaultBranch()))
.branchRetrieveOptions(RetrieveOptions.BASE_REFERENCE_RELATED_AND_COMMIT_META)
.tagRetrieveOptions(RetrieveOptions.COMMIT_META)
.build()
: GetNamedRefsParams.DEFAULT;
}
/**
* Produces the filter predicate for reference-filtering.
*
* @param filter The filter to filter by
*/
private static Predicate filterReferences(String filter) {
if (Strings.isNullOrEmpty(filter)) {
return r -> true;
}
final Script script;
try {
script =
SCRIPT_HOST
.buildScript(filter)
.withContainer(CONTAINER)
.withDeclarations(REFERENCES_DECLARATIONS)
.withTypes(REFERENCES_TYPES)
.build();
} catch (ScriptException e) {
throw new IllegalArgumentException(e);
}
return reference -> {
try {
ReferenceMetadata refMeta = reference.getMetadata();
if (refMeta == null) {
refMeta = CELUtil.EMPTY_REFERENCE_METADATA;
}
CommitMeta commit = refMeta.getCommitMetaOfHEAD();
if (commit == null) {
commit = CELUtil.EMPTY_COMMIT_META;
}
return script.execute(
Boolean.class,
ImmutableMap.of(
VAR_REF,
reference,
VAR_REF_TYPE,
reference.getType().name(),
VAR_COMMIT,
commit,
VAR_REF_META,
refMeta));
} catch (ScriptException e) {
throw new RuntimeException(e);
}
};
}
@Override
public Reference getReferenceByName(String refName, FetchOption fetchOption)
throws NessieNotFoundException {
try {
boolean fetchAll = FetchOption.isFetchAll(fetchOption);
Reference ref =
makeReference(getStore().getNamedRef(refName, getGetNamedRefsParams(fetchAll)), fetchAll);
startAccessCheck().canViewReference(RefUtil.toNamedRef(ref)).checkAndThrow();
return ref;
} catch (ReferenceNotFoundException e) {
throw new NessieReferenceNotFoundException(e.getMessage(), e);
}
}
@Override
public ReferenceHistoryResponse getReferenceHistory(String refName, Integer headCommitsToScan)
throws NessieNotFoundException {
Reference ref;
ReferenceHistory history;
try {
ref = makeReference(getStore().getNamedRef(refName, getGetNamedRefsParams(false)), false);
startAccessCheck().canViewReference(RefUtil.toNamedRef(ref)).checkAndThrow();
history = getStore().getReferenceHistory(ref.getName(), headCommitsToScan);
} catch (ReferenceNotFoundException e) {
throw new NessieReferenceNotFoundException(e.getMessage(), e);
}
ImmutableReferenceHistoryResponse.Builder response =
ReferenceHistoryResponse.builder()
.reference(ref)
.commitLogConsistency(history.commitLogConsistency());
response.current(convertStoreHistoryEntry(history.current()));
history.previous().stream().map(this::convertStoreHistoryEntry).forEach(response::addPrevious);
return response.build();
}
private ReferenceHistoryState convertStoreHistoryEntry(
ReferenceHistory.ReferenceHistoryElement element) {
return ReferenceHistoryState.referenceHistoryElement(
element.pointer().asString(), element.commitConsistency(), element.meta());
}
@Override
public Reference createReference(
String refName, ReferenceType type, String targetHash, String sourceRefName)
throws NessieNotFoundException, NessieConflictException {
Validation.validateForbiddenReferenceName(refName);
NamedRef namedReference = toNamedRef(type, refName);
BatchAccessChecker check = startAccessCheck().canCreateReference(namedReference);
Optional targetHashObj;
try {
ResolvedHash targetRef =
getHashResolver()
.resolveHashOnRef(
sourceRefName,
targetHash,
new HashValidator("Target hash").hashMustBeUnambiguous());
check.canViewReference(targetRef.getNamedRef());
targetHashObj = Optional.of(targetRef.getHash());
} catch (ReferenceNotFoundException e) {
// If the default-branch does not exist and hashOnRef points to the "beginning of time",
// then do not throw a NessieNotFoundException, but re-create the default branch. In all
// cases, re-throw the exception.
if (!(ReferenceType.BRANCH.equals(type)
&& refName.equals(getServerConfig().getDefaultBranch())
&& (null == targetHash || getStore().noAncestorHash().asString().equals(targetHash)))) {
throw new NessieReferenceNotFoundException(e.getMessage(), e);
}
targetHashObj = Optional.empty();
}
check.checkAndThrow();
try {
Hash hash = getStore().create(namedReference, targetHashObj).getHash();
return RefUtil.toReference(namedReference, hash);
} catch (ReferenceNotFoundException e) {
throw new NessieReferenceNotFoundException(e.getMessage(), e);
} catch (ReferenceAlreadyExistsException e) {
throw new NessieReferenceAlreadyExistsException(e.getMessage(), e);
}
}
@Override
public Branch getDefaultBranch() throws NessieNotFoundException {
Reference r = getReferenceByName(getServerConfig().getDefaultBranch(), FetchOption.MINIMAL);
checkState(r instanceof Branch, "Default branch isn't a branch");
return (Branch) r;
}
@Override
public Reference assignReference(
ReferenceType referenceType, String referenceName, String expectedHash, Reference assignTo)
throws NessieNotFoundException, NessieConflictException {
try {
ResolvedHash oldRef =
getHashResolver()
.resolveHashOnRef(
referenceName,
expectedHash,
new HashValidator("Assignment target", "Expected hash")
.refMustBeBranchOrTag()
.hashMustBeUnambiguous());
ResolvedHash newRef =
getHashResolver()
.resolveHashOnRef(
assignTo.getName(),
assignTo.getHash(),
new HashValidator("Target hash").hashMustBeUnambiguous());
checkArgument(
referenceType == null || referenceType == RefUtil.referenceType(oldRef.getNamedRef()),
"Expected reference type %s does not match existing reference %s",
referenceType,
oldRef.getNamedRef());
startAccessCheck()
.canViewReference(newRef.getNamedRef())
.canAssignRefToHash(oldRef.getNamedRef())
.checkAndThrow();
getStore().assign(oldRef.getNamedRef(), oldRef.getHash(), newRef.getHash());
return RefUtil.toReference(oldRef.getNamedRef(), newRef.getHash());
} catch (ReferenceNotFoundException e) {
throw new NessieReferenceNotFoundException(e.getMessage(), e);
} catch (ReferenceConflictException e) {
throw new NessieReferenceConflictException(e.getReferenceConflicts(), e.getMessage(), e);
}
}
@Override
public Reference deleteReference(
ReferenceType referenceType, String referenceName, String expectedHash)
throws NessieConflictException, NessieNotFoundException {
try {
ReferenceInfo resolved =
getStore().getNamedRef(referenceName, GetNamedRefsParams.DEFAULT);
NamedRef ref = resolved.getNamedRef();
checkArgument(
referenceType == null || referenceType == RefUtil.referenceType(ref),
"Expected reference type %s does not match existing reference %s",
referenceType,
ref);
checkArgument(
!(ref instanceof BranchName
&& getServerConfig().getDefaultBranch().equals(ref.getName())),
"Default branch '%s' cannot be deleted.",
ref.getName());
startAccessCheck().canDeleteReference(ref).checkAndThrow();
ResolvedHash refToDelete =
getHashResolver()
.resolveHashOnRef(
resolved.getNamedRef(),
resolved.getHash(),
expectedHash,
new HashValidator("Expected hash").hashMustBeUnambiguous());
Hash deletedAthash = getStore().delete(ref, refToDelete.getHash()).getHash();
return RefUtil.toReference(ref, deletedAthash);
} catch (ReferenceNotFoundException e) {
throw new NessieReferenceNotFoundException(e.getMessage(), e);
} catch (ReferenceConflictException e) {
throw new NessieReferenceConflictException(e.getReferenceConflicts(), e.getMessage(), e);
}
}
@Override
public R getCommitLog(
String namedRef,
FetchOption fetchOption,
String oldestHashLimit,
String youngestHash,
String filter,
String pageToken,
PagedResponseHandler pagedResponseHandler)
throws NessieNotFoundException {
try {
ResolvedHash endRef =
getHashResolver()
.resolveHashOnRef(
namedRef,
null == pageToken ? youngestHash : pageToken,
new HashValidator(null == pageToken ? "Youngest hash" : "Token pagination hash"));
startAccessCheck().canListCommitLog(endRef.getNamedRef()).checkAndThrow();
String stopHash =
oldestHashLimit == null
? null
: getHashResolver()
.resolveHashOnRef(endRef, oldestHashLimit, new HashValidator("Oldest hash"))
.getHash()
.asString();
boolean fetchAll = FetchOption.isFetchAll(fetchOption);
Set successfulChecks = new HashSet<>();
Set failedChecks = new HashSet<>();
try (PaginationIterator commits = getStore().getCommits(endRef.getHash(), fetchAll)) {
Predicate predicate = filterCommitLog(filter);
while (commits.hasNext()) {
Commit commit = commits.next();
LogEntry logEntry = commitToLogEntry(fetchAll, commit);
String hash = logEntry.getCommitMeta().getHash();
if (!predicate.test(logEntry)) {
continue;
}
boolean stop = Objects.equals(hash, stopHash);
logEntry =
logEntryOperationsAccessCheck(successfulChecks, failedChecks, endRef, logEntry);
if (!pagedResponseHandler.addEntry(logEntry)) {
if (!stop) {
pagedResponseHandler.hasMore(commits.tokenForCurrent());
}
break;
}
if (stop) {
break;
}
}
}
return pagedResponseHandler.build();
} catch (ReferenceNotFoundException e) {
throw new NessieReferenceNotFoundException(e.getMessage(), e);
}
}
private LogEntry logEntryOperationsAccessCheck(
Set successfulChecks,
Set failedChecks,
WithHash endRef,
LogEntry logEntry) {
List operations = logEntry.getOperations();
if (operations == null || operations.isEmpty()) {
return logEntry;
}
ImmutableLogEntry.Builder newLogEntry =
ImmutableLogEntry.builder().from(logEntry).operations(emptyList());
Map identifiedKeys = new HashMap<>();
try {
getStore()
.getIdentifiedKeys(
endRef.getHash(),
operations.stream().map(Operation::getKey).collect(Collectors.toList()))
.forEach(entry -> identifiedKeys.put(entry.contentKey(), entry));
} catch (ReferenceNotFoundException e) {
throw new RuntimeException(e);
}
Set checks = newHashSetWithExpectedSize(operations.size());
for (Operation op : operations) {
if (!(op instanceof Operation.Put) && !(op instanceof Operation.Delete)) {
throw new IllegalStateException("Unknown operation " + op);
}
IdentifiedContentKey identifiedKey = identifiedKeys.get(op.getKey());
if (identifiedKey != null) {
checks.add(canReadContentKey(endRef.getValue(), identifiedKey));
} else {
checks.add(canReadContentKey(endRef.getValue(), op.getKey()));
}
}
BatchAccessChecker accessCheck = startAccessCheck();
boolean anyCheck = false;
for (Check check : checks) {
if (!successfulChecks.contains(check) && !failedChecks.contains(check)) {
accessCheck.can(check);
anyCheck = true;
}
}
Map failures = anyCheck ? accessCheck.check() : emptyMap();
for (Operation op : operations) {
if (!(op instanceof Operation.Put) && !(op instanceof Operation.Delete)) {
throw new IllegalStateException("Unknown operation " + op);
}
IdentifiedContentKey identifiedKey = identifiedKeys.get(op.getKey());
Check check;
if (identifiedKey != null) {
check = canReadContentKey(endRef.getValue(), identifiedKey);
} else {
check = canReadContentKey(endRef.getValue(), op.getKey());
}
if (failures.containsKey(check)) {
failedChecks.add(check);
} else if (!failedChecks.contains(check)) {
newLogEntry.addOperations(op);
successfulChecks.add(check);
}
}
return newLogEntry.build();
}
private ImmutableLogEntry commitToLogEntry(boolean fetchAll, Commit commit) {
CommitMeta commitMeta = commit.getCommitMeta();
ImmutableLogEntry.Builder logEntry = LogEntry.builder();
logEntry.commitMeta(commitMeta);
if (commit.getParentHash() != null) {
logEntry.parentCommitHash(commit.getParentHash().asString());
}
if (fetchAll) {
if (commit.getOperations() != null) {
commit
.getOperations()
.forEach(
op -> {
ContentKey key = op.getKey();
if (op instanceof Put) {
Content content = ((Put) op).getContent();
logEntry.addOperations(Operation.Put.of(key, content));
}
if (op instanceof Delete) {
logEntry.addOperations(Operation.Delete.of(key));
}
});
}
}
return logEntry.build();
}
/**
* Produces the filter predicate for commit-log filtering.
*
* @param filter The filter to filter by
*/
private static Predicate filterCommitLog(String filter) {
if (Strings.isNullOrEmpty(filter)) {
return x -> true;
}
final Script script;
try {
script =
SCRIPT_HOST
.buildScript(filter)
.withContainer(CONTAINER)
.withDeclarations(COMMIT_LOG_DECLARATIONS)
.withTypes(COMMIT_LOG_TYPES)
.build();
} catch (ScriptException e) {
throw new IllegalArgumentException(e);
}
return logEntry -> {
try {
List operations = logEntry.getOperations();
if (operations == null) {
operations = Collections.emptyList();
}
// ContentKey has some @JsonIgnore attributes, which would otherwise not be accessible.
List