package service import ( "context" "github.com/google/uuid" "github.com/vasyakrg/dns-autoresolver/internal/crypto" "github.com/vasyakrg/dns-autoresolver/internal/diff" "github.com/vasyakrg/dns-autoresolver/internal/provider" "github.com/vasyakrg/dns-autoresolver/internal/provider/registry" "github.com/vasyakrg/dns-autoresolver/internal/store/dto" ) // DomainRef is the minimal data the service needs about a domain. type DomainRef struct { ZoneID string Provider string SecretEnc string Template dto.TemplateDoc } type Loader interface { LoadDomain(ctx context.Context, projectID, domainID uuid.UUID) (DomainRef, error) } type Recorder interface { SaveCheckRun(ctx context.Context, domainID uuid.UUID, cs diff.Changeset) error } type ApplyRequest struct { ApplyUpdates bool ApplyPrunes bool } type DomainService struct { loader Loader rec Recorder reg *registry.Registry cipher *crypto.Cipher } func New(loader Loader, rec Recorder, reg *registry.Registry, cipher *crypto.Cipher) *DomainService { return &DomainService{loader: loader, rec: rec, reg: reg, cipher: cipher} } // resolve loads the domain, its provider and decrypted credentials, and computes the diff. // projectID scopes the lookup so a domainID belonging to another tenant's // project can never be resolved here (closes IDOR). func (s *DomainService) resolve(ctx context.Context, projectID, domainID uuid.UUID) (provider.Provider, provider.Credentials, DomainRef, diff.Changeset, error) { ref, err := s.loader.LoadDomain(ctx, projectID, domainID) if err != nil { return nil, provider.Credentials{}, ref, diff.Changeset{}, err } p, err := s.reg.ByName(ref.Provider) if err != nil { return nil, provider.Credentials{}, ref, diff.Changeset{}, err } secret, err := s.cipher.Decrypt(ref.SecretEnc) if err != nil { return nil, provider.Credentials{}, ref, diff.Changeset{}, err } creds := provider.Credentials{Secret: string(secret)} actual, err := p.GetRecords(ctx, creds, ref.ZoneID) if err != nil { return nil, provider.Credentials{}, ref, diff.Changeset{}, err } cs := diff.Diff(ref.Template.ToModel(), actual) return p, creds, ref, cs, nil } // Check computes and records the diff between template and zone. func (s *DomainService) Check(ctx context.Context, projectID, domainID uuid.UUID) (diff.Changeset, error) { _, _, _, cs, err := s.resolve(ctx, projectID, domainID) if err != nil { return diff.Changeset{}, err } if err := s.rec.SaveCheckRun(ctx, domainID, cs); err != nil { return diff.Changeset{}, err } return cs, nil } // Apply applies updates always (when ApplyUpdates) and prunes only when ApplyPrunes. func (s *DomainService) Apply(ctx context.Context, projectID, domainID uuid.UUID, req ApplyRequest) (diff.Changeset, error) { p, creds, ref, cs, err := s.resolve(ctx, projectID, domainID) if err != nil { return diff.Changeset{}, err } var toApply []diff.RecordDiff if req.ApplyUpdates { toApply = append(toApply, cs.Updates()...) } if req.ApplyPrunes { toApply = append(toApply, cs.Prunes()...) } applied := diff.Changeset{Diffs: toApply} if len(toApply) > 0 { if err := p.ApplyChanges(ctx, creds, ref.ZoneID, applied); err != nil { return diff.Changeset{}, err } } return applied, nil }