blob: 68ebb431977e05a5321147d4f019bf0a580e204e [file] [log] [blame]
// Copyright (C) 2024 The Android Open Source Project
//
// 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.
import com.google.common.cache.*
import com.google.common.flogger.*
import com.google.gerrit.common.*
import com.google.gerrit.entities.*
import com.google.gerrit.extensions.annotations.*
import com.google.gerrit.extensions.events.*
import com.google.gerrit.extensions.registration.*
import com.google.gerrit.lifecycle.*
import com.google.gerrit.server.*
import com.google.gerrit.server.account.*
import com.google.gerrit.server.cache.*
import com.google.gerrit.server.config.*
import com.google.gerrit.server.project.*
import com.google.inject.*
import com.google.inject.name.*
import java.time.*
import java.util.function.*
import java.util.stream.Collectors
import static java.util.concurrent.TimeUnit.*
class TrackActiveUsersCache extends CacheModule {
static final NAME = "users_cache"
static final DEFAULT_CACHE_TTL = Duration.ofDays(90)
static final MAX_SIZE = 300_000
@Override
protected void configure() {
persist(NAME, Integer, Long)
.diskLimit(MAX_SIZE)
.maximumWeight(MAX_SIZE)
.expireAfterWrite(DEFAULT_CACHE_TTL)
}
}
@Singleton
class TrackingGroupBackend implements GroupBackend {
@Inject
@Named(TrackActiveUsersCache.NAME)
Cache<Integer, Long> trackActiveUsersCache
@Override
boolean handles(AccountGroup.UUID uuid) {
return true
}
@Override
GroupDescription.Basic get(AccountGroup.UUID uuid) {
return null
}
@Override
Collection<GroupReference> suggest(String name, @Nullable ProjectState project) {
return Collections.emptyList()
}
@Override
GroupMembership membershipsOf(CurrentUser user) {
if (user.identifiedUser) {
def accountId = user.accountId.get()
def currentMinutes = MILLISECONDS.toMinutes(System.currentTimeMillis())
if (trackActiveUsersCache.getIfPresent(accountId) != currentMinutes) {
trackActiveUsersCache.put(accountId, currentMinutes)
}
}
return GroupMembership.EMPTY
}
@Override
boolean isVisibleToAll(AccountGroup.UUID uuid) {
return false
}
}
class AutoDisableInactiveUsersConfig {
final Set<Account.Id> ignoreAccountIds
private final PluginConfig config
@Inject
AutoDisableInactiveUsersConfig(
PluginConfigFactory configFactory,
@PluginName String pluginName
) {
config = configFactory.getFromGerritConfig(pluginName)
ignoreAccountIds = ignoreAccountIdsFromConfig("ignoreAccountId")
}
private Set<Account.Id> ignoreAccountIdsFromConfig(String name) {
def strings = config.getStringList(name) as Set
strings.stream()
.map(Account.Id.&tryParse)
.filter { it.isPresent() }
.map { it.get() }
.collect(Collectors.toSet())
}
}
class AutoDisableInactiveUsersEvictionListener implements CacheRemovalListener<Integer, Long> {
static final FluentLogger logger = FluentLogger.forEnclosingClass()
private final String pluginName
private final String fullCacheName
private final Cache<Integer, Long> trackActiveUsersCache
private final Provider<AccountsUpdate> accountsUpdate
private final AutoDisableInactiveUsersConfig autoDisableConfig
@Inject
AutoDisableInactiveUsersEvictionListener(
@PluginName String pluginName,
@ServerInitiated Provider<AccountsUpdate> accountsUpdate,
@Named(TrackActiveUsersCache.NAME) Cache<Integer, Long> trackActiveUsersCache,
AutoDisableInactiveUsersConfig autoDisableConfig
) {
this.pluginName = pluginName
this.accountsUpdate = accountsUpdate
this.autoDisableConfig = autoDisableConfig
this.trackActiveUsersCache = trackActiveUsersCache
fullCacheName = "${pluginName}.${TrackActiveUsersCache.NAME}"
}
@Override
void onRemoval(String pluginName, String cacheName, RemovalNotification<Integer, Long> notification) {
if (fullCacheName != cacheName) {
return
}
if (notification.cause == RemovalCause.EXPIRED) {
disableAccount(Account.id(notification.key))
} else if (notification.cause == RemovalCause.EXPLICIT) {
logger.atWarning().log(
"cache %s do not support eviction, entry for user %d will be added back", fullCacheName, notification.key)
trackActiveUsersCache.put(notification.key, notification.value)
}
}
private void disableAccount(Account.Id accountId) {
if (autoDisableConfig.ignoreAccountIds.contains(accountId)) {
return
}
logger.atInfo().log("Automatically disabling user id: %d", accountId.get())
accountsUpdate.get().update(
"""Automatically disabling after inactivity
Disabled by ${pluginName}""",
accountId,
new Consumer<AccountDelta.Builder>() {
@Override
void accept(AccountDelta.Builder builder) {
builder.setActive(false)
}
})
}
}
@Singleton
class AutoDisableInactiveUsersListener implements LifecycleListener {
static final FluentLogger logger = FluentLogger.forEnclosingClass()
@Inject
Accounts accounts
@Inject
ServiceUserClassifier serviceUserClassifier
@Inject
@Named(TrackActiveUsersCache.NAME)
Cache<Integer, Long> trackActiveUsersCache
@Override
void start() {
def currentMinutes = MILLISECONDS.toMinutes(System.currentTimeMillis())
accounts.all()
.findAll {
it.account().isActive()
&& !serviceUserClassifier.isServiceUser(it.account().id()
&& !trackActiveUsersCache.getIfPresent(it.account().id().get())
}
.each { trackActiveUsersCache.put(it.account().id().get(), currentMinutes) }
}
@Override
void stop() {
// no-op
}
}
class TrackAndDisableInactiveUsersModule extends LifecycleModule {
@Override
void configure() {
install(new TrackActiveUsersCache())
listener().to(AutoDisableInactiveUsersListener)
DynamicSet.bind(binder(), CacheRemovalListener).to(AutoDisableInactiveUsersEvictionListener)
DynamicSet.bind(binder(), GroupBackend).to(TrackingGroupBackend)
}
}
modules = [TrackAndDisableInactiveUsersModule]