mirror of
https://github.com/go-vikunja/vikunja.git
synced 2026-07-16 14:52:35 -05:00
222 lines
7.7 KiB
Go
222 lines
7.7 KiB
Go
// Vikunja is a to-do list application to facilitate your life.
|
|
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package openid
|
|
|
|
import (
|
|
"slices"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"code.vikunja.io/api/pkg/config"
|
|
"code.vikunja.io/api/pkg/cron"
|
|
"code.vikunja.io/api/pkg/log"
|
|
"code.vikunja.io/api/pkg/utils"
|
|
)
|
|
|
|
// ProviderStatus reports whether one configured OpenID Connect provider is
|
|
// available for login. A configured but unavailable provider was unreachable
|
|
// when Vikunja last initialized its providers and would stay broken until a
|
|
// restart (vikunja#3135) — initialization is retried automatically instead,
|
|
// see RegisterProviderAvailabilityCron.
|
|
type ProviderStatus struct {
|
|
Key string `json:"key" doc:"The config key of the provider."`
|
|
Available bool `json:"available" doc:"True when the provider is initialized and offered for login. This reflects the last initialization attempt, not the provider's current reachability. A configured but unavailable provider was unreachable or misconfigured when Vikunja last initialized its providers; initialization is retried automatically with exponential backoff, after at most 15 minutes."`
|
|
}
|
|
|
|
func availableProviderKeys() map[string]bool {
|
|
providers, err := GetAllProviders()
|
|
if err != nil {
|
|
log.Errorf("Could not get available openid providers: %s", err)
|
|
return nil
|
|
}
|
|
keys := make(map[string]bool, len(providers))
|
|
for _, p := range providers {
|
|
keys[p.Key] = true
|
|
}
|
|
return keys
|
|
}
|
|
|
|
// GetProvidersStatus returns the availability of every configured OpenID
|
|
// Connect provider. It returns nil when OpenID Connect auth is disabled, no
|
|
// providers are configured, or the provider list has not been initialized
|
|
// yet (right after startup, or while a rebuild is in flight).
|
|
//
|
|
// It enumerates providers from the raw config instead of the provider list
|
|
// because the latter silently drops providers whose initialization failed —
|
|
// exactly the ones a healthcheck needs to report. It reads only the cached
|
|
// list and never builds it: this runs on the /health request path, where
|
|
// dialing providers could block for seconds per down provider (or exit the
|
|
// process for requireavailability ones). The availability cron keeps the
|
|
// cache fresh.
|
|
func GetProvidersStatus() []ProviderStatus {
|
|
if !config.AuthOpenIDEnabled.GetBool() {
|
|
return nil
|
|
}
|
|
|
|
configured := rawProviderConfigs()
|
|
if len(configured) == 0 {
|
|
return nil
|
|
}
|
|
|
|
cached, initialized := getCachedProviders()
|
|
if !initialized {
|
|
return nil
|
|
}
|
|
available := make(map[string]bool, len(cached))
|
|
for _, p := range cached {
|
|
available[p.Key] = true
|
|
}
|
|
|
|
statuses := make([]ProviderStatus, 0, len(configured))
|
|
for key := range configured {
|
|
statuses = append(statuses, ProviderStatus{
|
|
Key: key,
|
|
Available: available[key],
|
|
})
|
|
}
|
|
sort.Slice(statuses, func(i, j int) bool { return statuses[i].Key < statuses[j].Key })
|
|
return statuses
|
|
}
|
|
|
|
func unavailableProviderKeys() []string {
|
|
var unavailable []string
|
|
for _, p := range GetProvidersStatus() {
|
|
if !p.Available {
|
|
unavailable = append(unavailable, p.Key)
|
|
}
|
|
}
|
|
return unavailable
|
|
}
|
|
|
|
// initializeUnavailableProviders re-runs provider initialization for the
|
|
// given configured but unavailable providers. This heals the state from
|
|
// vikunja#3135: a provider that was down while Vikunja started stayed
|
|
// unusable for login until a manual restart.
|
|
func initializeUnavailableProviders(unavailable []string) {
|
|
log.Infof("Openid providers %v are configured but not available, retrying initialization", unavailable)
|
|
CleanupSavedOpenIDProviders()
|
|
|
|
available := availableProviderKeys()
|
|
for _, key := range unavailable {
|
|
if available[key] {
|
|
log.Infof("Openid provider %s is now available", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
const (
|
|
providerRetryBaseInterval = time.Minute
|
|
providerRetryMaxInterval = 15 * time.Minute
|
|
)
|
|
|
|
// providerRetryState paces the initialization retries with capped
|
|
// exponential backoff so a long-unavailable provider is not hammered on
|
|
// every cron tick. The state is global rather than per provider because an
|
|
// initialization attempt always rebuilds the whole provider list; the backoff
|
|
// resets whenever the set of unavailable providers changes so a newly failing
|
|
// provider is retried promptly instead of inheriting an old long delay.
|
|
var providerRetryState struct {
|
|
sync.Mutex
|
|
failures int
|
|
nextAttempt time.Time
|
|
lastUnavailable []string
|
|
}
|
|
|
|
func resetProviderRetryBackoff(unavailable []string) {
|
|
providerRetryState.failures = 0
|
|
providerRetryState.nextAttempt = time.Time{}
|
|
providerRetryState.lastUnavailable = unavailable
|
|
}
|
|
|
|
// retryUnavailableProviders runs on every cron tick but only attempts
|
|
// initialization once the backoff delay has passed. The delay doubles per
|
|
// failed attempt from providerRetryBaseInterval up to
|
|
// providerRetryMaxInterval, with equal jitter (delay/2 + random(delay/2)) so
|
|
// many instances don't retry in lockstep. Any success resets the backoff.
|
|
func retryUnavailableProviders() {
|
|
providerRetryState.Lock()
|
|
defer providerRetryState.Unlock()
|
|
|
|
if !config.AuthOpenIDEnabled.GetBool() || len(rawProviderConfigs()) == 0 {
|
|
resetProviderRetryBackoff(nil)
|
|
return
|
|
}
|
|
|
|
_, initialized := getCachedProviders()
|
|
if initialized {
|
|
unavailable := unavailableProviderKeys()
|
|
if len(unavailable) == 0 {
|
|
resetProviderRetryBackoff(nil)
|
|
return
|
|
}
|
|
if !slices.Equal(unavailable, providerRetryState.lastUnavailable) {
|
|
resetProviderRetryBackoff(unavailable)
|
|
}
|
|
}
|
|
|
|
now := time.Now()
|
|
if now.Before(providerRetryState.nextAttempt) {
|
|
return
|
|
}
|
|
|
|
if initialized {
|
|
initializeUnavailableProviders(unavailableProviderKeys())
|
|
} else if _, err := GetAllProviders(); err != nil {
|
|
// The list was never built in this keyvalue store, or the last build
|
|
// failed (e.g. duplicate issuers) — GetAllProviders builds it so the
|
|
// healthcheck has cached state to serve.
|
|
log.Errorf("Error while initializing openid providers: %s", err)
|
|
}
|
|
|
|
unavailable := unavailableProviderKeys()
|
|
if _, initialized = getCachedProviders(); initialized && len(unavailable) == 0 {
|
|
resetProviderRetryBackoff(nil)
|
|
return
|
|
}
|
|
|
|
backoff := providerRetryBaseInterval << min(providerRetryState.failures, 10)
|
|
if backoff > providerRetryMaxInterval {
|
|
backoff = providerRetryMaxInterval
|
|
}
|
|
providerRetryState.failures++
|
|
providerRetryState.lastUnavailable = unavailable
|
|
|
|
delay := backoff/2 + randomJitter(backoff/2)
|
|
providerRetryState.nextAttempt = now.Add(delay)
|
|
log.Debugf("Openid providers %v are still not available, retrying initialization after %s", unavailable, delay)
|
|
}
|
|
|
|
func randomJitter(limit time.Duration) time.Duration {
|
|
n, err := utils.CryptoRandomInt(int64(limit))
|
|
if err != nil {
|
|
return limit / 2
|
|
}
|
|
return time.Duration(n)
|
|
}
|
|
|
|
// RegisterProviderAvailabilityCron periodically retries initializing
|
|
// configured openid providers which are not available, typically because
|
|
// they were unreachable while Vikunja started. Retries are paced with
|
|
// capped exponential backoff.
|
|
func RegisterProviderAvailabilityCron() {
|
|
err := cron.Schedule("* * * * *", retryUnavailableProviders)
|
|
if err != nil {
|
|
log.Fatalf("Could not register openid provider availability cron: %s", err)
|
|
}
|
|
}
|