package firewalldb import ( "bytes" "context" "database/sql" "errors" "fmt" "github.com/lightninglabs/lightning-terminal/db" "github.com/lightninglabs/lightning-terminal/db/sqlc" "github.com/lightninglabs/lightning-terminal/session" "github.com/lightningnetwork/lnd/fn" ) // SQLKVStoreQueries is a subset of the sqlc.Queries interface that can be // used to interact with the kvstore tables. // //nolint:ll type SQLKVStoreQueries interface { SQLSessionQueries DeleteFeatureKVStoreRecord(ctx context.Context, arg sqlc.DeleteFeatureKVStoreRecordParams) error DeleteGlobalKVStoreRecord(ctx context.Context, arg sqlc.DeleteGlobalKVStoreRecordParams) error DeleteGroupKVStoreRecord(ctx context.Context, arg sqlc.DeleteGroupKVStoreRecordParams) error GetFeatureKVStoreRecord(ctx context.Context, arg sqlc.GetFeatureKVStoreRecordParams) ([]byte, error) GetGlobalKVStoreRecord(ctx context.Context, arg sqlc.GetGlobalKVStoreRecordParams) ([]byte, error) GetGroupKVStoreRecord(ctx context.Context, arg sqlc.GetGroupKVStoreRecordParams) ([]byte, error) UpdateFeatureKVStoreRecord(ctx context.Context, arg sqlc.UpdateFeatureKVStoreRecordParams) error UpdateGlobalKVStoreRecord(ctx context.Context, arg sqlc.UpdateGlobalKVStoreRecordParams) error UpdateGroupKVStoreRecord(ctx context.Context, arg sqlc.UpdateGroupKVStoreRecordParams) error InsertKVStoreRecord(ctx context.Context, arg sqlc.InsertKVStoreRecordParams) error DeleteAllTempKVStores(ctx context.Context) error GetOrInsertFeatureID(ctx context.Context, name string) (int64, error) GetOrInsertRuleID(ctx context.Context, name string) (int64, error) GetFeatureID(ctx context.Context, name string) (int64, error) GetRuleID(ctx context.Context, name string) (int64, error) } // DeleteTempKVStores deletes all temporary kv stores. // // NOTE: part of the RulesDB interface. func (s *SQLDB) DeleteTempKVStores(ctx context.Context) error { var writeTxOpts db.QueriesTxOptions return s.db.ExecTx(ctx, &writeTxOpts, func(tx SQLQueries) error { return tx.DeleteAllTempKVStores(ctx) }) } // GetKVStores constructs a new rules.KVStores in a namespace defined by the // rule name, group ID and feature name. // // NOTE: part of the RulesDB interface. func (s *SQLDB) GetKVStores(rule string, groupAlias session.ID, feature string) KVStores { return &sqlExecutor[KVStoreTx]{ db: s.db, wrapTx: func(queries SQLQueries) KVStoreTx { return &sqlKVStoresTx{ queries: queries, groupAlias: groupAlias, rule: rule, feature: feature, } }, } } // sqlKVStoresTx is a SQL implementation of the KVStoreTx interface. type sqlKVStoresTx struct { queries SQLKVStoreQueries groupAlias session.ID rule string feature string } // Global returns a persisted global, rule-name indexed, kv store. A rule with a // given name will have access to this store independent of group ID or feature. // // NOTE: part of the KVStoreTx interface. func (s *sqlKVStoresTx) Global() KVStore { return &sqlKVStore{ sqlKVStoresTx: s, params: &sqlKVStoreParams{ perm: true, ruleName: s.rule, }, } } // Local returns a persisted local kv store for the rule. Depending on how the // implementation is initialised, this will either be under the group ID // namespace or the group ID _and_ feature name namespace. // // NOTE: part of the KVStoreTx interface. func (s *sqlKVStoresTx) Local() KVStore { var featureName fn.Option[string] if s.feature != "" { featureName = fn.Some(s.feature) } return &sqlKVStore{ sqlKVStoresTx: s, params: &sqlKVStoreParams{ perm: true, ruleName: s.rule, groupID: fn.Some(s.groupAlias), featureName: featureName, }, } } // GlobalTemp is similar to the Global store except that its contents is cleared // upon restart of the database. The reason persisting the temporary store // changes instead of just keeping an in-memory store is that we can then // guarantee atomicity if changes are made to both the permanent and temporary // stores. // // NOTE: part of the KVStoreTx interface. func (s *sqlKVStoresTx) GlobalTemp() KVStore { return &sqlKVStore{ sqlKVStoresTx: s, params: &sqlKVStoreParams{ perm: false, ruleName: s.rule, }, } } // LocalTemp is similar to the Local store except that its contents is cleared // upon restart of the database. The reason persisting the temporary store // changes instead of just keeping an in-memory store is that we can then // guarantee atomicity if changes are made to both the permanent and temporary // stores. // // NOTE: part of the KVStoreTx interface. func (s *sqlKVStoresTx) LocalTemp() KVStore { var featureName fn.Option[string] if s.feature != "" { featureName = fn.Some(s.feature) } return &sqlKVStore{ sqlKVStoresTx: s, params: &sqlKVStoreParams{ perm: false, ruleName: s.rule, groupID: fn.Some(s.groupAlias), featureName: featureName, }, } } // A compile-time assertion to ensure that sqlKVStoresTx implements the // KVStoreTx interface. var _ KVStoreTx = (*sqlKVStoresTx)(nil) // sqlKVStoreParams holds the various parameters that determine the namespace // that a query is accessing. type sqlKVStoreParams struct { perm bool ruleName string groupID fn.Option[session.ID] featureName fn.Option[string] } // sqlKVStore is a SQL store backed KVStore. type sqlKVStore struct { *sqlKVStoresTx params *sqlKVStoreParams } // A compile-time assertion to ensure that sqlKVStore implements the KVStore // interface. var _ KVStore = (*sqlKVStore)(nil) // Get fetches the value under the given key from the underlying kv store. If no // value is found, nil is returned. // // NOTE: part of the KVStore interface. func (s *sqlKVStore) Get(ctx context.Context, key string) ([]byte, error) { value, err := s.get(ctx, key) if errors.Is(err, sql.ErrNoRows) || errors.Is(err, session.ErrUnknownGroup) { return nil, nil } else if err != nil { return nil, err } return value, nil } // Set sets the given key-value pair in the underlying kv store. // // NOTE: part of the KVStore interface. func (s *sqlKVStore) Set(ctx context.Context, key string, value []byte) error { ruleID, groupID, featureID, err := s.genNamespaceFields(ctx, false) if err != nil { return err } // We first need to figure out if we are inserting a new record or // updating an existing one. So first do a GET with the same set of // params. oldValue, err := s.get(ctx, key) if err != nil && !errors.Is(err, sql.ErrNoRows) { return err } // No such entry. Add new record. if errors.Is(err, sql.ErrNoRows) { return s.queries.InsertKVStoreRecord( ctx, sqlc.InsertKVStoreRecordParams{ EntryKey: key, Value: value, Perm: s.params.perm, RuleID: ruleID, GroupID: groupID, FeatureID: featureID, }, ) } // If an entry exists but the value has not changed, there is nothing // left to do. if bytes.Equal(oldValue, value) { return nil } // Otherwise, the key exists but the value needs to be updated. switch { case groupID.Valid && featureID.Valid: return s.queries.UpdateFeatureKVStoreRecord( ctx, sqlc.UpdateFeatureKVStoreRecordParams{ Key: key, Value: value, Perm: s.params.perm, GroupID: groupID, RuleID: ruleID, FeatureID: featureID, }, ) case groupID.Valid: return s.queries.UpdateGroupKVStoreRecord( ctx, sqlc.UpdateGroupKVStoreRecordParams{ Key: key, Value: value, Perm: s.params.perm, GroupID: groupID, RuleID: ruleID, }, ) case featureID.Valid: return fmt.Errorf("a global feature kv store is " + "not currently supported") default: return s.queries.UpdateGlobalKVStoreRecord( ctx, sqlc.UpdateGlobalKVStoreRecordParams{ Key: key, Value: value, Perm: s.params.perm, RuleID: ruleID, }, ) } } // Del deletes the value under the given key in the underlying kv store. // // NOTE: part of the KVStore interface. func (s *sqlKVStore) Del(ctx context.Context, key string) error { // Note: we pass in true here for "read-only" since because this is a // Delete, if the record does not exist, we don't need to create one. // But no need to error out if it doesn't exist. ruleID, groupID, featureID, err := s.genNamespaceFields(ctx, true) if errors.Is(err, sql.ErrNoRows) || errors.Is(err, session.ErrUnknownGroup) { return nil } else if err != nil { return err } switch { case groupID.Valid && featureID.Valid: return s.queries.DeleteFeatureKVStoreRecord( ctx, sqlc.DeleteFeatureKVStoreRecordParams{ Key: key, Perm: s.params.perm, GroupID: groupID, RuleID: ruleID, FeatureID: featureID, }, ) case groupID.Valid: return s.queries.DeleteGroupKVStoreRecord( ctx, sqlc.DeleteGroupKVStoreRecordParams{ Key: key, Perm: s.params.perm, GroupID: groupID, RuleID: ruleID, }, ) case featureID.Valid: return fmt.Errorf("a global feature kv store is " + "not currently supported") default: return s.queries.DeleteGlobalKVStoreRecord( ctx, sqlc.DeleteGlobalKVStoreRecordParams{ Key: key, Perm: s.params.perm, RuleID: ruleID, }, ) } } // get fetches the value under the given key from the underlying kv store given // the namespace fields. func (s *sqlKVStore) get(ctx context.Context, key string) ([]byte, error) { ruleID, groupID, featureID, err := s.genNamespaceFields(ctx, true) if err != nil { return nil, err } switch { case groupID.Valid && featureID.Valid: return s.queries.GetFeatureKVStoreRecord( ctx, sqlc.GetFeatureKVStoreRecordParams{ Key: key, Perm: s.params.perm, GroupID: groupID, RuleID: ruleID, FeatureID: featureID, }, ) case groupID.Valid: return s.queries.GetGroupKVStoreRecord( ctx, sqlc.GetGroupKVStoreRecordParams{ Key: key, Perm: s.params.perm, GroupID: groupID, RuleID: ruleID, }, ) case featureID.Valid: return nil, fmt.Errorf("a global feature kv store is " + "not currently supported") default: return s.queries.GetGlobalKVStoreRecord( ctx, sqlc.GetGlobalKVStoreRecordParams{ Key: key, Perm: s.params.perm, RuleID: ruleID, }, ) } } // genNamespaceFields generates the various SQL query parameters that are // required to access the kvstore namespace determined by the sqlKVStore params. func (s *sqlKVStore) genNamespaceFields(ctx context.Context, readOnly bool) (int64, sql.NullInt64, sql.NullInt64, error) { var ( groupID sql.NullInt64 featureID sql.NullInt64 ruleID int64 err error ) // If a group ID is specified, then we first check that this group ID // is a known session alias. s.params.groupID.WhenSome(func(id session.ID) { var dbGroupID int64 dbGroupID, err = s.queries.GetSessionIDByAlias(ctx, id[:]) if errors.Is(err, sql.ErrNoRows) { err = session.ErrUnknownGroup return } else if err != nil { return } groupID = sql.NullInt64{ Int64: dbGroupID, Valid: true, } }) if err != nil { return ruleID, groupID, featureID, err } // We only insert a new rule name into the DB if this is a write call. if readOnly { ruleID, err = s.queries.GetRuleID(ctx, s.params.ruleName) if err != nil { return 0, groupID, featureID, fmt.Errorf("unable to get rule ID: %w", err) } } else { ruleID, err = s.queries.GetOrInsertRuleID( ctx, s.params.ruleName, ) if err != nil { return 0, groupID, featureID, fmt.Errorf("unable to get or insert rule "+ "ID: %w", err) } } s.params.featureName.WhenSome(func(feature string) { // We only insert a new feature name into the DB if this is a // write call. var id int64 if readOnly { id, err = s.queries.GetFeatureID(ctx, feature) if err != nil { return } } else { id, err = s.queries.GetOrInsertFeatureID(ctx, feature) if err != nil { return } } featureID = sql.NullInt64{ Int64: id, Valid: true, } }) return ruleID, groupID, featureID, err }