Ctrl K
ComunidadGuías…

store/configuration_state.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package store

import (
	"context"
	"database/sql"
	"log/slog"

	"github.com/pkg/errors"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"

	storepb "github.com/usememos/memos/proto/gen/store"
)

// The state record is one system_setting row. It is state, not a setting: the
// instance setting reads skip it, because convertInstanceSettingFromRaw has no
// case for its key.
var configurationStateName = storepb.InstanceSettingKey_CONFIGURATION_STATE.String()

// GetConfigurationState returns the declarative configuration state record,
// read from the database without the instance setting cache. Before any apply
// has changed something, it is an empty record with serial 0.
func (s *Store) GetConfigurationState(ctx context.Context) (*storepb.InstanceConfigurationState, error) {
	list, err := s.driver.ListInstanceSettings(ctx, &FindInstanceSetting{Name: configurationStateName})
	if err != nil {
		return nil, errors.Wrap(err, "failed to read configuration state")
	}
	if len(list) == 0 {
		return &storepb.InstanceConfigurationState{}, nil
	}
	return decodeConfigurationState(list[0])
}

// ConfigurationTransaction is one serializable transaction over declarative
// configuration, opened by RunConfigurationTransaction. Its reads see what the
// transaction has written, and its writes apply the same normalization and
// checks as the store's own writes; caches are invalidated once it commits.
type ConfigurationTransaction struct {
	store *Store
	tx    *sql.Tx
	state *storepb.InstanceConfigurationState

	settingNames     []string
	userIDs          []int32
	passwordsChanged []int32
}

// State returns the state record as read at the start of the transaction.
func (t *ConfigurationTransaction) State() *storepb.InstanceConfigurationState {
	return proto.CloneOf(t.state)
}

// GetConfigurationState is State, so a transaction can be read like a store.
func (t *ConfigurationTransaction) GetConfigurationState(context.Context) (*storepb.InstanceConfigurationState, error) {
	return t.State(), nil
}

// ListIdentityProviders reads identity providers within the transaction.
func (t *ConfigurationTransaction) ListIdentityProviders(ctx context.Context, find *FindIdentityProvider) ([]*storepb.IdentityProvider, error) {
	list, err := t.store.driver.ListIdentityProvidersTx(ctx, t.tx, find)
	if err != nil {
		return nil, err
	}
	providers := make([]*storepb.IdentityProvider, 0, len(list))
	for _, raw := range list {
		provider, err := convertIdentityProviderFromRaw(raw)
		if err != nil {
			return nil, err
		}
		providers = append(providers, provider)
	}
	return providers, nil
}

// GetUncachedInstanceSetting reads one setting within the transaction.
func (t *ConfigurationTransaction) GetUncachedInstanceSetting(ctx context.Context, find *FindInstanceSetting) (*storepb.InstanceSetting, error) {
	raw, err := t.store.driver.GetInstanceSettingTx(ctx, t.tx, find.Name)
	if err != nil || raw == nil {
		return nil, err
	}
	return convertInstanceSettingFromRaw(raw)
}

// ListUsers reads users within the transaction.
func (t *ConfigurationTransaction) ListUsers(ctx context.Context, find *FindUser) ([]*User, error) {
	return t.store.driver.ListUsersTx(ctx, t.tx, find)
}

// CreateIdentityProvider creates an identity provider within the transaction.
func (t *ConfigurationTransaction) CreateIdentityProvider(ctx context.Context, provider *storepb.IdentityProvider) error {
	raw, err := convertIdentityProviderToRaw(provider)
	if err != nil {
		return err
	}
	if _, err := t.store.driver.CreateIdentityProviderTx(ctx, t.tx, raw); err != nil {
		return errors.Wrapf(err, "failed to create identity provider %s", provider.Uid)
	}
	return nil
}

// ReplaceIdentityProvider writes every field of the identity provider with id.
func (t *ConfigurationTransaction) ReplaceIdentityProvider(ctx context.Context, id int32, provider *storepb.IdentityProvider) error {
	config, err := convertIdentityProviderConfigToRaw(provider.Type, provider.Config)
	if err != nil {
		return err
	}
	name, filter := provider.Name, provider.IdentifierFilter
	if _, err := t.store.driver.UpdateIdentityProviderTx(ctx, t.tx, &UpdateIdentityProvider{
		ID: id, Name: &name, IdentifierFilter: &filter, Config: &config,
	}); err != nil {
		return errors.Wrapf(err, "failed to update identity provider %s", provider.Uid)
	}
	return nil
}

// DeleteIdentityProvider deletes the identity provider with id.
func (t *ConfigurationTransaction) DeleteIdentityProvider(ctx context.Context, id int32) error {
	if err := t.store.driver.DeleteIdentityProviderTx(ctx, t.tx, &DeleteIdentityProvider{ID: id}); err != nil {
		return errors.Wrap(err, "failed to delete identity provider")
	}
	return nil
}

// UpsertInstanceSetting writes a setting within the transaction.
func (t *ConfigurationTransaction) UpsertInstanceSetting(ctx context.Context, setting *storepb.InstanceSetting) error {
	raw, err := encodeInstanceSetting(setting)
	if err != nil {
		return err
	}
	if err := t.store.driver.UpsertInstanceSettingTx(ctx, t.tx, raw); err != nil {
		return errors.Wrapf(err, "failed to write instance setting %s", raw.Name)
	}
	t.settingNames = append(t.settingNames, raw.Name)
	return nil
}

// DeleteInstanceSetting deletes a setting, so its defaults apply again.
func (t *ConfigurationTransaction) DeleteInstanceSetting(ctx context.Context, key storepb.InstanceSettingKey) error {
	if err := t.store.driver.DeleteInstanceSettingTx(ctx, t.tx, &DeleteInstanceSetting{Name: key.String()}); err != nil {
		return errors.Wrapf(err, "failed to delete instance setting %s", key)
	}
	t.settingNames = append(t.settingNames, key.String())
	return nil
}

// CreateUser creates a user within the transaction.
func (t *ConfigurationTransaction) CreateUser(ctx context.Context, create *User) (*User, error) {
	email, err := normalizeUserEmail(create.Email)
	if err != nil {
		return nil, err
	}
	create.Email = email
	user, err := t.store.driver.CreateUserTx(ctx, t.tx, create)
	if err != nil {
		if uniqueErr := classifyUserUniqueViolation(err); uniqueErr != nil {
			return nil, uniqueErr
		}
		return nil, errors.Wrapf(err, "failed to create user %s", create.Username)
	}
	t.userIDs = append(t.userIDs, user.ID)
	return user, nil
}

// UpdateUser updates a user within the transaction, keeping the last active
// administrator as the store's own update does.
func (t *ConfigurationTransaction) UpdateUser(ctx context.Context, update *UpdateUser) error {
	if update.Email != nil {
		email, err := normalizeUserEmail(*update.Email)
		if err != nil {
			return err
		}
		update.Email = &email
	}
	if _, err := t.store.driver.UpdateUserTx(ctx, t.tx, update); err != nil {
		if uniqueErr := classifyUserUniqueViolation(err); uniqueErr != nil {
			return uniqueErr
		}
		return errors.Wrap(err, "failed to update user")
	}
	t.userIDs = append(t.userIDs, update.ID)
	if update.PasswordHash != nil {
		t.passwordsChanged = append(t.passwordsChanged, update.ID)
	}
	return nil
}

// ListUserIdentities reads identity links within the transaction.
func (t *ConfigurationTransaction) ListUserIdentities(ctx context.Context, find *FindUserIdentity) ([]*UserIdentity, error) {
	return t.store.driver.ListUserIdentitiesTx(ctx, t.tx, find)
}

// CreateUserIdentity links an identity-provider subject to a user within the
// transaction. A subject already linked elsewhere is ErrUserIdentityTaken.
func (t *ConfigurationTransaction) CreateUserIdentity(ctx context.Context, create *UserIdentity) error {
	if _, err := t.store.driver.CreateUserIdentityTx(ctx, t.tx, create); err != nil {
		if uniqueErr := classifyUserUniqueViolation(err); uniqueErr != nil {
			return uniqueErr
		}
		return errors.Wrapf(err, "failed to link the %s identity of user %d", create.Provider, create.UserID)
	}
	return nil
}

// DeleteUserIdentities removes identity links within the transaction.
func (t *ConfigurationTransaction) DeleteUserIdentities(ctx context.Context, delete *DeleteUserIdentity) error {
	if err := t.store.driver.DeleteUserIdentitiesTx(ctx, t.tx, delete); err != nil {
		return errors.Wrap(err, "failed to delete identity links")
	}
	return nil
}

// afterCommit drops what the committed writes made stale. A new password ends
// every session of that user, as an administrator's password reset does.
func (t *ConfigurationTransaction) afterCommit(ctx context.Context) {
	s := t.store
	for _, name := range t.settingNames {
		s.instanceSettingCache.Delete(ctx, name)
		if name == storepb.InstanceSettingKey_STORAGE.String() {
			s.resetStorageDriverCache()
		}
	}
	for _, userID := range t.userIDs {
		s.userCache.Delete(ctx, userCacheKey(userID))
	}
	for _, userID := range t.passwordsChanged {
		if err := s.RemoveUserRefreshTokensExcept(ctx, userID, ""); err != nil {
			slog.Warn("failed to revoke sessions after a configured password change", "user_id", userID, "error", err)
		}
	}
}

// RunConfigurationTransaction runs fn in one serializable transaction that
// starts by reading, and where the database supports it locking, the state
// record. fn returns the record to store, or nil when nothing changed; a
// returned record is written with the next serial, whatever serial it carries.
// fn runs again when the transaction conflicts with a concurrent one, so it
// must derive what it writes from what it reads through tx. The result is the
// record as stored when the transaction commits.
func (s *Store) RunConfigurationTransaction(
	ctx context.Context,
	fn func(ctx context.Context, tx *ConfigurationTransaction) (*storepb.InstanceConfigurationState, error),
) (*storepb.InstanceConfigurationState, error) {
	var result *storepb.InstanceConfigurationState
	var committed *ConfigurationTransaction
	err := s.runSerializable(ctx, func(tx *sql.Tx) error {
		raw, err := s.driver.GetInstanceSettingTx(ctx, tx, configurationStateName)
		if err != nil {
			return errors.Wrap(err, "failed to read configuration state")
		}
		current := &storepb.InstanceConfigurationState{}
		if raw != nil {
			if current, err = decodeConfigurationState(raw); err != nil {
				return err
			}
		}

		transaction := &ConfigurationTransaction{store: s, tx: tx, state: current}
		committed = transaction
		next, err := fn(ctx, transaction)
		if err != nil {
			return err
		}
		if next == nil {
			result = current
			return nil
		}
		next = proto.CloneOf(next)
		next.Serial = current.Serial + 1
		value, err := protojson.Marshal(next)
		if err != nil {
			return errors.Wrap(err, "failed to encode configuration state")
		}

		// The write is conditional on the row read above, so a concurrent apply
		// that slipped past the database's isolation still cannot be overwritten.
		var written bool
		if raw == nil {
			written, err = s.driver.CreateInstanceSettingIfNotExistsTx(ctx, tx, &InstanceSetting{Name: configurationStateName, Value: string(value)})
		} else {
			written, err = s.driver.UpdateInstanceSettingIfUnchangedTx(ctx, tx, configurationStateName, raw.Value, string(value))
		}
		if err != nil {
			return errors.Wrap(err, "failed to write configuration state")
		}
		if !written {
			return errTransactionConflict
		}
		result = next
		return nil
	})
	if err != nil {
		return nil, err
	}
	committed.afterCommit(ctx)
	return result, nil
}

func decodeConfigurationState(raw *InstanceSetting) (*storepb.InstanceConfigurationState, error) {
	state := &storepb.InstanceConfigurationState{}
	if err := protojsonUnmarshaler.Unmarshal([]byte(raw.Value), state); err != nil {
		return nil, errors.Wrap(err, "failed to decode configuration state")
	}
	return state, nil
}