|
| 1 | +package session |
| 2 | + |
| 3 | +import ( |
| 4 | + "context" |
| 5 | + "database/sql" |
| 6 | + "errors" |
| 7 | + "fmt" |
| 8 | + "reflect" |
| 9 | + "time" |
| 10 | + |
| 11 | + "github.com/davecgh/go-spew/spew" |
| 12 | + "github.com/lightninglabs/lightning-terminal/accounts" |
| 13 | + "github.com/lightninglabs/lightning-terminal/db/sqlc" |
| 14 | + "github.com/pmezard/go-difflib/difflib" |
| 15 | +) |
| 16 | + |
| 17 | +var ( |
| 18 | + // ErrMigrationMismatch is returned when the migrated session does not |
| 19 | + // match the original session. |
| 20 | + ErrMigrationMismatch = fmt.Errorf("migrated session does not match " + |
| 21 | + "original session") |
| 22 | +) |
| 23 | + |
| 24 | +// MigrateSessionStoreToSQL runs the migration of all sessions from the KV |
| 25 | +// database to the SQL database. The migration is done in a single transaction |
| 26 | +// to ensure that all sessions are migrated or none at all. |
| 27 | +// |
| 28 | +// NOTE: As sessions may contain linked accounts, the accounts sql migration |
| 29 | +// MUST be run prior to this migration. |
| 30 | +func MigrateSessionStoreToSQL(ctx context.Context, kvStore *BoltStore, |
| 31 | + tx SQLQueries) error { |
| 32 | + |
| 33 | + log.Infof("Starting migration of the KV sessions store to SQL") |
| 34 | + |
| 35 | + kvSessions, err := kvStore.ListAllSessions(ctx) |
| 36 | + if err != nil { |
| 37 | + return err |
| 38 | + } |
| 39 | + |
| 40 | + // If sessions are linked to a group, we must insert the initial session |
| 41 | + // of each group before the other sessions in that group. This ensures |
| 42 | + // we can retrieve the SQL group ID when inserting the remaining |
| 43 | + // sessions. Therefore, we first insert all initial group sessions, |
| 44 | + // allowing us to fetch the group IDs and insert the rest of the |
| 45 | + // sessions afterward. |
| 46 | + // We therefore filter out the initial sessions first, and then migrate |
| 47 | + // them prior to the rest of the sessions. |
| 48 | + var ( |
| 49 | + initialGroupSessions []*Session |
| 50 | + linkedSessions []*Session |
| 51 | + ) |
| 52 | + |
| 53 | + for _, kvSession := range kvSessions { |
| 54 | + if kvSession.GroupID == kvSession.ID { |
| 55 | + initialGroupSessions = append( |
| 56 | + initialGroupSessions, kvSession, |
| 57 | + ) |
| 58 | + } else { |
| 59 | + linkedSessions = append(linkedSessions, kvSession) |
| 60 | + } |
| 61 | + } |
| 62 | + |
| 63 | + err = migrateSessionsToSQLAndValidate(ctx, tx, initialGroupSessions) |
| 64 | + if err != nil { |
| 65 | + return fmt.Errorf("migration of non-linked session failed: %w", |
| 66 | + err) |
| 67 | + } |
| 68 | + |
| 69 | + err = migrateSessionsToSQLAndValidate(ctx, tx, linkedSessions) |
| 70 | + if err != nil { |
| 71 | + return fmt.Errorf("migration of linked session failed: %w", err) |
| 72 | + } |
| 73 | + |
| 74 | + total := len(initialGroupSessions) + len(linkedSessions) |
| 75 | + log.Infof("All sessions migrated from KV to SQL. Total number of "+ |
| 76 | + "sessions migrated: %d", total) |
| 77 | + |
| 78 | + return nil |
| 79 | +} |
| 80 | + |
| 81 | +// migrateSessionsToSQLAndValidate runs the migration for the passed sessions |
| 82 | +// from the KV database to the SQL database, and validates that the migrated |
| 83 | +// sessions match the original sessions. |
| 84 | +func migrateSessionsToSQLAndValidate(ctx context.Context, |
| 85 | + tx SQLQueries, kvSessions []*Session) error { |
| 86 | + |
| 87 | + for _, kvSession := range kvSessions { |
| 88 | + err := migrateSingleSessionToSQL(ctx, tx, kvSession) |
| 89 | + if err != nil { |
| 90 | + return fmt.Errorf("unable to migrate session(%v): %w", |
| 91 | + kvSession.ID, err) |
| 92 | + } |
| 93 | + |
| 94 | + // Validate that the session was correctly migrated and matches |
| 95 | + // the original session in the kv store. |
| 96 | + sqlSess, err := tx.GetSessionByAlias(ctx, kvSession.ID[:]) |
| 97 | + if err != nil { |
| 98 | + if errors.Is(err, sql.ErrNoRows) { |
| 99 | + err = ErrSessionNotFound |
| 100 | + } |
| 101 | + return fmt.Errorf("unable to get migrated session "+ |
| 102 | + "from sql store: %w", err) |
| 103 | + } |
| 104 | + |
| 105 | + migratedSession, err := unmarshalSession(ctx, tx, sqlSess) |
| 106 | + if err != nil { |
| 107 | + return fmt.Errorf("unable to unmarshal migrated "+ |
| 108 | + "session: %w", err) |
| 109 | + } |
| 110 | + |
| 111 | + overrideSessionTimeZone(kvSession) |
| 112 | + overrideSessionTimeZone(migratedSession) |
| 113 | + overrideMacaroonRecipe(kvSession, migratedSession) |
| 114 | + |
| 115 | + if !reflect.DeepEqual(kvSession, migratedSession) { |
| 116 | + diff := difflib.UnifiedDiff{ |
| 117 | + A: difflib.SplitLines( |
| 118 | + spew.Sdump(kvSession), |
| 119 | + ), |
| 120 | + B: difflib.SplitLines( |
| 121 | + spew.Sdump(migratedSession), |
| 122 | + ), |
| 123 | + FromFile: "Expected", |
| 124 | + FromDate: "", |
| 125 | + ToFile: "Actual", |
| 126 | + ToDate: "", |
| 127 | + Context: 3, |
| 128 | + } |
| 129 | + diffText, _ := difflib.GetUnifiedDiffString(diff) |
| 130 | + |
| 131 | + return fmt.Errorf("%w: %v.\n%v", ErrMigrationMismatch, |
| 132 | + kvSession.ID, diffText) |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + return nil |
| 137 | +} |
| 138 | + |
| 139 | +// migrateSingleSessionToSQL runs the migration for a single session from the |
| 140 | +// KV database to the SQL database. Note that if the session links to an |
| 141 | +// account, the linked accounts store MUST have been migrated before that |
| 142 | +// session is migrated. |
| 143 | +func migrateSingleSessionToSQL(ctx context.Context, |
| 144 | + tx SQLQueries, session *Session) error { |
| 145 | + |
| 146 | + var ( |
| 147 | + acctID sql.NullInt64 |
| 148 | + err error |
| 149 | + remotePubKey []byte |
| 150 | + ) |
| 151 | + |
| 152 | + session.AccountID.WhenSome(func(alias accounts.AccountID) { |
| 153 | + // Check that the account exists in the SQL store, before |
| 154 | + // linking it. |
| 155 | + var acctAlias int64 |
| 156 | + acctAlias, err = alias.ToInt64() |
| 157 | + if err != nil { |
| 158 | + return |
| 159 | + } |
| 160 | + |
| 161 | + var acctDBID int64 |
| 162 | + acctDBID, err = tx.GetAccountIDByAlias(ctx, acctAlias) |
| 163 | + if errors.Is(err, sql.ErrNoRows) { |
| 164 | + err = accounts.ErrAccNotFound |
| 165 | + return |
| 166 | + } else if err != nil { |
| 167 | + return |
| 168 | + } |
| 169 | + |
| 170 | + acctID = sql.NullInt64{ |
| 171 | + Int64: acctDBID, |
| 172 | + Valid: true, |
| 173 | + } |
| 174 | + }) |
| 175 | + if err != nil { |
| 176 | + return err |
| 177 | + } |
| 178 | + |
| 179 | + // The remote public key is currently only set for autopilot sessions, |
| 180 | + // else it's a nil byte array. |
| 181 | + if session.RemotePublicKey != nil { |
| 182 | + remotePubKey = session.RemotePublicKey.SerializeCompressed() |
| 183 | + } |
| 184 | + |
| 185 | + // Proceed to insert the session into the sql db. |
| 186 | + insertSessionParams := sqlc.InsertSessionParams{ |
| 187 | + Alias: session.ID[:], |
| 188 | + Label: session.Label, |
| 189 | + State: int16(session.State), |
| 190 | + Type: int16(session.Type), |
| 191 | + Expiry: session.Expiry.UTC(), |
| 192 | + CreatedAt: session.CreatedAt.UTC(), |
| 193 | + ServerAddress: session.ServerAddr, |
| 194 | + DevServer: session.DevServer, |
| 195 | + MacaroonRootKey: int64(session.MacaroonRootKey), |
| 196 | + PairingSecret: session.PairingSecret[:], |
| 197 | + LocalPrivateKey: session.LocalPrivateKey.Serialize(), |
| 198 | + LocalPublicKey: session.LocalPublicKey.SerializeCompressed(), |
| 199 | + RemotePublicKey: remotePubKey, |
| 200 | + Privacy: session.WithPrivacyMapper, |
| 201 | + AccountID: acctID, |
| 202 | + } |
| 203 | + |
| 204 | + sqlId, err := tx.InsertSession(ctx, insertSessionParams) |
| 205 | + if err != nil { |
| 206 | + return err |
| 207 | + } |
| 208 | + |
| 209 | + // Since the InsertSession query doesn't support that we set the revoked |
| 210 | + // field during the insert, we need to set the field after the session |
| 211 | + // has been created. |
| 212 | + if !session.RevokedAt.IsZero() { |
| 213 | + setSessionRevokedParams := sqlc.SetSessionRevokedAtParams{ |
| 214 | + ID: sqlId, |
| 215 | + RevokedAt: sql.NullTime{ |
| 216 | + Time: session.RevokedAt.UTC(), |
| 217 | + Valid: true, |
| 218 | + }, |
| 219 | + } |
| 220 | + |
| 221 | + err = tx.SetSessionRevokedAt(ctx, setSessionRevokedParams) |
| 222 | + if err != nil { |
| 223 | + return err |
| 224 | + } |
| 225 | + } |
| 226 | + |
| 227 | + // After the session has been inserted, we need to update the session |
| 228 | + // with the group ID if it is linked to a group. We need to do this |
| 229 | + // after the session has been inserted, because the group ID can be the |
| 230 | + // session itself, and therefore the SQL id for the session won't exist |
| 231 | + // prior to inserting the session. |
| 232 | + groupID, err := tx.GetSessionIDByAlias(ctx, session.GroupID[:]) |
| 233 | + if errors.Is(err, sql.ErrNoRows) { |
| 234 | + return ErrUnknownGroup |
| 235 | + } else if err != nil { |
| 236 | + return fmt.Errorf("unable to fetch group(%x): %w", |
| 237 | + session.GroupID[:], err) |
| 238 | + } |
| 239 | + |
| 240 | + // Now lets set the group ID for the session. |
| 241 | + err = tx.SetSessionGroupID(ctx, sqlc.SetSessionGroupIDParams{ |
| 242 | + ID: sqlId, |
| 243 | + GroupID: sql.NullInt64{ |
| 244 | + Int64: groupID, |
| 245 | + Valid: true, |
| 246 | + }, |
| 247 | + }) |
| 248 | + if err != nil { |
| 249 | + return fmt.Errorf("unable to set group Alias: %w", err) |
| 250 | + } |
| 251 | + |
| 252 | + // Once we have the sqlID for the session, we can proceed to insert rows |
| 253 | + // into the linked child tables. |
| 254 | + if session.MacaroonRecipe != nil { |
| 255 | + // We start by inserting the macaroon permissions. |
| 256 | + for _, sessionPerm := range session.MacaroonRecipe.Permissions { |
| 257 | + permParam := sqlc.InsertSessionMacaroonPermissionParams{ |
| 258 | + SessionID: sqlId, |
| 259 | + Entity: sessionPerm.Entity, |
| 260 | + Action: sessionPerm.Action, |
| 261 | + } |
| 262 | + |
| 263 | + err = tx.InsertSessionMacaroonPermission( |
| 264 | + ctx, permParam, |
| 265 | + ) |
| 266 | + if err != nil { |
| 267 | + return err |
| 268 | + } |
| 269 | + } |
| 270 | + |
| 271 | + // Next we insert the macaroon caveats. |
| 272 | + for _, sessCaveat := range session.MacaroonRecipe.Caveats { |
| 273 | + caveatParams := sqlc.InsertSessionMacaroonCaveatParams{ |
| 274 | + SessionID: sqlId, |
| 275 | + CaveatID: sessCaveat.Id, |
| 276 | + VerificationID: sessCaveat.VerificationId, |
| 277 | + Location: sql.NullString{ |
| 278 | + String: sessCaveat.Location, |
| 279 | + Valid: sessCaveat.Location != "", |
| 280 | + }, |
| 281 | + } |
| 282 | + |
| 283 | + err = tx.InsertSessionMacaroonCaveat( |
| 284 | + ctx, caveatParams, |
| 285 | + ) |
| 286 | + if err != nil { |
| 287 | + return err |
| 288 | + } |
| 289 | + } |
| 290 | + } |
| 291 | + |
| 292 | + // That's followed by the feature config. |
| 293 | + if session.FeatureConfig != nil { |
| 294 | + for featureName, config := range *session.FeatureConfig { |
| 295 | + fConfParams := sqlc.InsertSessionFeatureConfigParams{ |
| 296 | + SessionID: sqlId, |
| 297 | + FeatureName: featureName, |
| 298 | + Config: config, |
| 299 | + } |
| 300 | + |
| 301 | + err = tx.InsertSessionFeatureConfig(ctx, fConfParams) |
| 302 | + if err != nil { |
| 303 | + return err |
| 304 | + } |
| 305 | + } |
| 306 | + } |
| 307 | + |
| 308 | + // Finally we insert the privacy flags. |
| 309 | + for _, privacyFlag := range session.PrivacyFlags { |
| 310 | + privacyFlagParams := sqlc.InsertSessionPrivacyFlagParams{ |
| 311 | + SessionID: sqlId, |
| 312 | + Flag: int32(privacyFlag), |
| 313 | + } |
| 314 | + |
| 315 | + err = tx.InsertSessionPrivacyFlag( |
| 316 | + ctx, privacyFlagParams, |
| 317 | + ) |
| 318 | + if err != nil { |
| 319 | + return err |
| 320 | + } |
| 321 | + } |
| 322 | + |
| 323 | + return nil |
| 324 | +} |
| 325 | + |
| 326 | +// overrideSessionTimeZone overrides the time zone of the session to the local |
| 327 | +// time zone and chops off the nanosecond part for comparison. This is needed |
| 328 | +// because KV database stores times as-is which as an unwanted side effect would |
| 329 | +// fail migration due to time comparison expecting both the original and |
| 330 | +// migrated sessions to be in the same local time zone and in microsecond |
| 331 | +// precision. Note that PostgresSQL stores times in microsecond precision while |
| 332 | +// SQLite can store times in nanosecond precision if using TEXT storage class. |
| 333 | +func overrideSessionTimeZone(session *Session) { |
| 334 | + fixTime := func(t time.Time) time.Time { |
| 335 | + return t.In(time.Local).Truncate(time.Microsecond) |
| 336 | + } |
| 337 | + |
| 338 | + if !session.Expiry.IsZero() { |
| 339 | + session.Expiry = fixTime(session.Expiry) |
| 340 | + } |
| 341 | + |
| 342 | + if !session.CreatedAt.IsZero() { |
| 343 | + session.CreatedAt = fixTime(session.CreatedAt) |
| 344 | + } |
| 345 | + |
| 346 | + if !session.RevokedAt.IsZero() { |
| 347 | + session.RevokedAt = fixTime(session.RevokedAt) |
| 348 | + } |
| 349 | +} |
| 350 | + |
| 351 | +// overrideMacaroonRecipe overrides the MacaroonRecipe for the SQL session in a |
| 352 | +// certain scenario: |
| 353 | +// In the bbolt store, a session can have a non-nil macaroon struct, despite |
| 354 | +// both the permissions and caveats being nil. There is no way to represent this |
| 355 | +// in the SQL store, as the macaroon permissions and caveats are separate |
| 356 | +// tables. Therefore, in the scenario where a MacaroonRecipe exists for the |
| 357 | +// bbolt version, but both the permissions and caveats are nil, we override the |
| 358 | +// MacaroonRecipe for the SQL version and set it to a MacaroonRecipe with |
| 359 | +// nil permissions and caveats. This is needed to ensure that the deep equals |
| 360 | +// check in the migration validation does not fail in this scenario. |
| 361 | +func overrideMacaroonRecipe(kvSession *Session, migratedSession *Session) { |
| 362 | + if kvSession.MacaroonRecipe != nil { |
| 363 | + kvPerms := kvSession.MacaroonRecipe.Permissions |
| 364 | + kvCaveats := kvSession.MacaroonRecipe.Caveats |
| 365 | + |
| 366 | + if kvPerms == nil && kvCaveats == nil { |
| 367 | + migratedSession.MacaroonRecipe = &MacaroonRecipe{} |
| 368 | + } |
| 369 | + } |
| 370 | +} |
0 commit comments