Line data Source code
1 : // Copyright (C) 2017 The Android Open Source Project 2 : // 3 : // Licensed under the Apache License, Version 2.0 (the "License"); 4 : // you may not use this file except in compliance with the License. 5 : // You may obtain a copy of the License at 6 : // 7 : // http://www.apache.org/licenses/LICENSE-2.0 8 : // 9 : // Unless required by applicable law or agreed to in writing, software 10 : // distributed under the License is distributed on an "AS IS" BASIS, 11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 : // See the License for the specific language governing permissions and 13 : // limitations under the License. 14 : 15 : package com.google.gerrit.server.index.account; 16 : 17 : import static com.google.common.base.Preconditions.checkState; 18 : import static java.nio.charset.StandardCharsets.UTF_8; 19 : import static java.util.Objects.requireNonNull; 20 : 21 : import com.google.common.base.Splitter; 22 : import com.google.common.collect.ImmutableSet; 23 : import com.google.common.collect.ListMultimap; 24 : import com.google.common.collect.MultimapBuilder; 25 : import com.google.gerrit.entities.Account; 26 : import com.google.gerrit.entities.Project; 27 : import com.google.gerrit.entities.RefNames; 28 : import com.google.gerrit.index.IndexConfig; 29 : import com.google.gerrit.index.QueryOptions; 30 : import com.google.gerrit.index.RefState; 31 : import com.google.gerrit.index.query.FieldBundle; 32 : import com.google.gerrit.server.account.externalids.ExternalId; 33 : import com.google.gerrit.server.account.externalids.ExternalIds; 34 : import com.google.gerrit.server.config.AllUsersName; 35 : import com.google.gerrit.server.config.AllUsersNameProvider; 36 : import com.google.gerrit.server.git.GitRepositoryManager; 37 : import com.google.gerrit.server.index.IndexUtils; 38 : import com.google.gerrit.server.index.StalenessCheckResult; 39 : import com.google.inject.Inject; 40 : import com.google.inject.Singleton; 41 : import java.io.IOException; 42 : import java.util.List; 43 : import java.util.Map; 44 : import java.util.Optional; 45 : import java.util.Set; 46 : import org.eclipse.jgit.lib.ObjectId; 47 : import org.eclipse.jgit.lib.Ref; 48 : import org.eclipse.jgit.lib.Repository; 49 : 50 : /** 51 : * Checks if documents in the account index are stale. 52 : * 53 : * <p>An index document is considered stale if the stored ref state differs from the SHA1 of the 54 : * user branch or if the stored external ID states don't match with the external IDs of the account 55 : * from the refs/meta/external-ids branch. 56 : */ 57 : @Singleton 58 : public class StalenessChecker { 59 151 : public static final ImmutableSet<String> FIELDS = 60 151 : ImmutableSet.of( 61 151 : AccountField.ID_FIELD_SPEC.getName(), 62 151 : AccountField.REF_STATE_SPEC.getName(), 63 151 : AccountField.EXTERNAL_ID_STATE_SPEC.getName()); 64 : 65 151 : public static final ImmutableSet<String> FIELDS2 = 66 151 : ImmutableSet.of( 67 151 : AccountField.ID_STR_FIELD_SPEC.getName(), 68 151 : AccountField.REF_STATE_SPEC.getName(), 69 151 : AccountField.EXTERNAL_ID_STATE_SPEC.getName()); 70 : 71 : private final AccountIndexCollection indexes; 72 : private final GitRepositoryManager repoManager; 73 : private final AllUsersName allUsersName; 74 : private final ExternalIds externalIds; 75 : private final IndexConfig indexConfig; 76 : 77 : @Inject 78 : StalenessChecker( 79 : AccountIndexCollection indexes, 80 : GitRepositoryManager repoManager, 81 : AllUsersName allUsersName, 82 : ExternalIds externalIds, 83 151 : IndexConfig indexConfig) { 84 151 : this.indexes = indexes; 85 151 : this.repoManager = repoManager; 86 151 : this.allUsersName = allUsersName; 87 151 : this.externalIds = externalIds; 88 151 : this.indexConfig = indexConfig; 89 151 : } 90 : 91 : public StalenessCheckResult check(Account.Id id) throws IOException { 92 1 : AccountIndex i = indexes.getSearchIndex(); 93 1 : if (i == null) { 94 : // No index; caller couldn't do anything if it is stale. 95 0 : return StalenessCheckResult.notStale(); 96 : } 97 1 : if (!i.getSchema().hasField(AccountField.REF_STATE_SPEC) 98 1 : || !i.getSchema().hasField(AccountField.EXTERNAL_ID_STATE_SPEC)) { 99 : // Index version not new enough for this check. 100 0 : return StalenessCheckResult.notStale(); 101 : } 102 : 103 1 : boolean useLegacyNumericFields = i.getSchema().hasField(AccountField.ID_FIELD_SPEC); 104 1 : ImmutableSet<String> fields = useLegacyNumericFields ? FIELDS : FIELDS2; 105 1 : Optional<FieldBundle> result = 106 1 : i.getRaw( 107 : id, 108 1 : QueryOptions.create( 109 1 : indexConfig, 0, 1, IndexUtils.accountFields(fields, useLegacyNumericFields))); 110 1 : if (!result.isPresent()) { 111 : // The document is missing in the index. 112 1 : try (Repository repo = repoManager.openRepository(allUsersName)) { 113 1 : Ref ref = repo.exactRef(RefNames.refsUsers(id)); 114 : 115 : // Stale if the account actually exists. 116 1 : if (ref == null) { 117 1 : return StalenessCheckResult.notStale(); 118 : } 119 0 : return StalenessCheckResult.stale( 120 : "Document missing in index, but found %s in the repo", ref); 121 1 : } 122 : } 123 : 124 1 : Iterable<byte[]> refStates = 125 1 : result.get().<Iterable<byte[]>>getValue(AccountField.REF_STATE_SPEC); 126 1 : for (Map.Entry<Project.NameKey, RefState> e : RefState.parseStates(refStates).entries()) { 127 : // Custom All-Users repository names are not indexed. Instead, the default name is used. 128 : // Therefore, defer to the currently configured All-Users name. 129 : Project.NameKey repoName = 130 1 : e.getKey().get().equals(AllUsersNameProvider.DEFAULT) ? allUsersName : e.getKey(); 131 1 : try (Repository repo = repoManager.openRepository(repoName)) { 132 1 : if (!e.getValue().match(repo)) { 133 1 : return StalenessCheckResult.stale( 134 : "Ref was modified since the account was indexed (%s != %s)", 135 1 : e.getValue(), repo.exactRef(e.getValue().ref())); 136 : } 137 1 : } 138 1 : } 139 : 140 1 : Set<ExternalId> extIds = externalIds.byAccount(id); 141 : 142 1 : ListMultimap<ObjectId, ObjectId> extIdStates = 143 1 : parseExternalIdStates( 144 1 : result.get().<Iterable<byte[]>>getValue(AccountField.EXTERNAL_ID_STATE_SPEC)); 145 1 : if (extIdStates.size() != extIds.size()) { 146 1 : return StalenessCheckResult.stale( 147 : "External IDs of the account were modified since the account was indexed. (%s != %s)", 148 1 : extIdStates.size(), extIds.size()); 149 : } 150 1 : for (ExternalId extId : extIds) { 151 1 : if (!extIdStates.containsKey(extId.key().sha1())) { 152 0 : return StalenessCheckResult.stale("External ID missing: %s", extId.key().sha1()); 153 : } 154 1 : if (!extIdStates.containsEntry(extId.key().sha1(), extId.blobId())) { 155 1 : return StalenessCheckResult.stale( 156 : "External ID has unexpected value. (%s != %s)", 157 1 : extIdStates.get(extId.key().sha1()), extId.blobId()); 158 : } 159 1 : } 160 : 161 1 : return StalenessCheckResult.notStale(); 162 : } 163 : 164 : public static ListMultimap<ObjectId, ObjectId> parseExternalIdStates( 165 : Iterable<byte[]> extIdStates) { 166 1 : ListMultimap<ObjectId, ObjectId> result = MultimapBuilder.hashKeys().arrayListValues().build(); 167 : 168 1 : if (extIdStates == null) { 169 0 : return result; 170 : } 171 : 172 1 : for (byte[] b : extIdStates) { 173 1 : requireNonNull(b, "invalid external ID state"); 174 1 : String s = new String(b, UTF_8); 175 1 : List<String> parts = Splitter.on(':').splitToList(s); 176 1 : checkState(parts.size() == 2, "invalid external ID state: %s", s); 177 1 : result.put(ObjectId.fromString(parts.get(0)), ObjectId.fromString(parts.get(1))); 178 1 : } 179 1 : return result; 180 : } 181 : }