| 1 | package historyreadback |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "os" |
| 6 | "slices" |
| 7 | "time" |
| 8 | |
| 9 | "github.com/carlosframework/platform/internal/hostactors" |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | ) |
| 12 | |
| 13 | // ReadReconstruction verifies the exact accepted authorization from fresh, |
| 14 | // enrolled-root readback. It does not certify fresh installation or retire |
| 15 | // independent recovery obligations carried in the returned status. |
| 16 | func ReadReconstruction(ctx context.Context, d hostactors.Directory, q Request, g release.HistoryReconstruction, now time.Time) (Receipt, error) { |
| 17 | r, err := ReadStatus(ctx, d, q, now) |
| 18 | if err != nil { |
| 19 | return Receipt{}, err |
| 20 | } |
| 21 | s := r.Status |
| 22 | accepted := digest(g) |
| 23 | if !validHex(accepted, 32) || g.Schema != 1 || g.Kind != "carlos-release-history" || g.Operation == "" || g.IssuedAt.IsZero() || g.Account != q.Scope.Binding.Account || g.App != q.Scope.Binding.App || g.Identity != s.Identity || g.Revision == 0 || g.Revision == ^uint64(0) || s.Revision != g.Revision+1 || s.Pending || s.LastReconstructionDigest != accepted || !s.AppComplete || s.AppRevision <= g.AppRevision || s.BakeComplete == 0 || s.BakeRevision <= g.BakeRevision || len(g.Hosts) == 0 || len(g.Hosts) > 256 { |
| 24 | return Receipt{}, os.ErrPermission |
| 25 | } |
| 26 | seen := map[string]bool{} |
| 27 | for _, expected := range g.Hosts { |
| 28 | if seen[expected.Host] { |
| 29 | return Receipt{}, os.ErrPermission |
| 30 | } |
| 31 | seen[expected.Host] = true |
| 32 | i := slices.IndexFunc(s.Hosts, func(h release.HistoryHostStatus) bool { return h.Host == expected.Host }) |
| 33 | if i < 0 || s.Hosts[i].Source != expected.Source || s.Hosts[i].Revision <= expected.Revision || !s.Hosts[i].RefusalStatus.Complete { |
| 34 | return Receipt{}, os.ErrPermission |
| 35 | } |
| 36 | } |
| 37 | if !seen[q.Scope.Binding.Host] { |
| 38 | return Receipt{}, os.ErrPermission |
| 39 | } |
| 40 | return r, nil |
| 41 | } |
| 42 | |
| 43 | // ReadRetirement verifies one effective retirement. Unrelated restrictions |
| 44 | // remain in the receipt and must not be treated as cleared by this result. |
| 45 | func ReadRetirement(ctx context.Context, d hostactors.Directory, q Request, g release.HistoryRecoveryRetirement, now time.Time) (Receipt, error) { |
| 46 | r, err := ReadStatus(ctx, d, q, now) |
| 47 | if err != nil { |
| 48 | return Receipt{}, err |
| 49 | } |
| 50 | s := r.Status |
| 51 | accepted := digest(g) |
| 52 | if !validHex(accepted, 32) || g.Schema != 1 || g.Kind != "carlos-release-recovery-retirement" || g.Operation == "" || g.IssuedAt.IsZero() || g.Binding != q.Scope.Binding || g.Identity != s.Identity || s.Pending || s.RecoverySources == nil || !s.RecoverySources.Known || !validHex(g.OutcomeDigest, 32) || !validHex(g.VerifiedDigest, 32) || g.AttemptID == "" || g.VerifiedID == "" || g.AttemptID == g.VerifiedID { |
| 53 | return Receipt{}, os.ErrPermission |
| 54 | } |
| 55 | for _, pending := range s.RecoverySources.Unresolved { |
| 56 | if pending.Binding == g.Binding && (pending.Attempt.ID == g.AttemptID || pending.Attempt.ID == g.VerifiedID) { |
| 57 | return Receipt{}, os.ErrPermission |
| 58 | } |
| 59 | } |
| 60 | for _, h := range s.Hosts { |
| 61 | if h.Host == g.Binding.Host && h.Source == g.Source && slices.Contains(h.RetiredRecoveries, g.AttemptID) && h.RetirementDigests[g.AttemptID] == accepted && h.RecoveryDigests[g.AttemptID] == g.OutcomeDigest && h.RecoveryDigests[g.VerifiedID] == g.VerifiedDigest { |
| 62 | return r, nil |
| 63 | } |
| 64 | } |
| 65 | return Receipt{}, os.ErrPermission |
| 66 | } |
| 67 | |