- Fix DeleteProvider skipping ownership check by adding getUserProvider call - Replace RestrictedService struct embedding with decorator pattern to prevent silent method promotion bypassing restriction checks - Make providerSettingsUsecase delegate to ProviderUsecase instead of accessing storage directly, ensuring validation and ownership are enforced - Accept ProviderValidator as constructor parameter, removing SetValidator mutator - Add instantiate() helper for consistent provider instantiation error handling - Wrap ListUserProviders storage errors in InternalError for consistency - Add Test_DeleteProvider_WrongUser test and reduce test boilerplate
293 lines
10 KiB
Go
293 lines
10 KiB
Go
// This file is part of the happyDomain (R) project.
|
|
// Copyright (c) 2020-2025 happyDomain
|
|
// Authors: Pierre-Olivier Mercier, et al.
|
|
//
|
|
// This program is offered under a commercial and under the AGPL license.
|
|
// For commercial licensing, contact us at <contact@happydomain.org>.
|
|
//
|
|
// For AGPL licensing:
|
|
// This program is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU Affero General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU Affero General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU Affero General Public License
|
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
package provider
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"git.happydns.org/happyDomain/model"
|
|
"git.happydns.org/happyDomain/providers"
|
|
)
|
|
|
|
// Service handles CRUD operations on DNS providers, with ownership enforcement.
|
|
type Service struct {
|
|
store ProviderStorage
|
|
validator ProviderValidator
|
|
}
|
|
|
|
// NewService creates a new provider Service. If validator is nil,
|
|
// the DefaultProviderValidator is used.
|
|
func NewService(store ProviderStorage, validator ProviderValidator) *Service {
|
|
if validator == nil {
|
|
validator = &DefaultProviderValidator{}
|
|
}
|
|
return &Service{
|
|
store: store,
|
|
validator: validator,
|
|
}
|
|
}
|
|
|
|
// ParseProvider converts a ProviderMessage to a Provider.
|
|
func ParseProvider(msg *happydns.ProviderMessage) (p *happydns.Provider, err error) {
|
|
p = &happydns.Provider{}
|
|
|
|
p.ProviderMeta = msg.ProviderMeta
|
|
p.Provider, err = providers.FindProvider(msg.Type)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
err = json.Unmarshal(msg.Provider, &p.Provider)
|
|
return
|
|
}
|
|
|
|
// instantiate is a helper that instantiates a provider and wraps errors consistently.
|
|
func instantiate(p *happydns.Provider) (happydns.ProviderActuator, error) {
|
|
instance, err := p.InstantiateProvider()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to instantiate provider: %w", err)
|
|
}
|
|
return instance, nil
|
|
}
|
|
|
|
// CreateProvider creates a new provider for the given user.
|
|
func (s *Service) CreateProvider(user *happydns.User, msg *happydns.ProviderMessage) (*happydns.Provider, error) {
|
|
provider, err := ParseProvider(msg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unable to parse provider: %w", err)
|
|
}
|
|
|
|
if err := s.validator.Validate(provider); err != nil {
|
|
return nil, fmt.Errorf("invalid provider: %w", err)
|
|
}
|
|
|
|
provider.Owner = user.Id
|
|
|
|
if err := s.store.CreateProvider(provider); err != nil {
|
|
return nil, happydns.InternalError{
|
|
Err: fmt.Errorf("failed to save provider: %w", err),
|
|
UserMessage: "Sorry, we are currently unable to create the given provider. Please try again later.",
|
|
}
|
|
}
|
|
|
|
return provider, nil
|
|
}
|
|
|
|
// getUserProvider retrieves a provider and verifies ownership.
|
|
func (s *Service) getUserProvider(user *happydns.User, providerID happydns.Identifier) (*happydns.ProviderMessage, error) {
|
|
p, err := s.store.GetProvider(providerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if !user.Id.Equals(p.ProviderMeta.Owner) {
|
|
return nil, happydns.ErrProviderNotFound
|
|
}
|
|
|
|
return p, err
|
|
}
|
|
|
|
// GetUserProvider retrieves a provider for the given user.
|
|
func (s *Service) GetUserProvider(user *happydns.User, providerID happydns.Identifier) (*happydns.Provider, error) {
|
|
p, err := s.getUserProvider(user, providerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return ParseProvider(p)
|
|
}
|
|
|
|
// GetUserProviderMeta retrieves provider metadata for the given user.
|
|
func (s *Service) GetUserProviderMeta(user *happydns.User, providerID happydns.Identifier) (*happydns.ProviderMeta, error) {
|
|
p, err := s.getUserProvider(user, providerID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p.Meta(), nil
|
|
}
|
|
|
|
// ListUserProviders retrieves all providers for the given user.
|
|
func (s *Service) ListUserProviders(user *happydns.User) ([]*happydns.ProviderMeta, error) {
|
|
items, err := s.store.ListProviders(user)
|
|
if err != nil {
|
|
return nil, happydns.InternalError{
|
|
Err: fmt.Errorf("failed to list providers: %w", err),
|
|
UserMessage: "Sorry, we are currently unable to list your providers. Please try again later.",
|
|
}
|
|
}
|
|
|
|
metas := make([]*happydns.ProviderMeta, 0, len(items))
|
|
for _, p := range items {
|
|
metas = append(metas, &p.ProviderMeta)
|
|
}
|
|
|
|
return metas, nil
|
|
}
|
|
|
|
// UpdateProvider updates a provider using the provided update function.
|
|
func (s *Service) UpdateProvider(providerID happydns.Identifier, user *happydns.User, updateFn func(*happydns.Provider)) error {
|
|
provider, err := s.GetUserProvider(user, providerID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
updateFn(provider)
|
|
|
|
if !provider.Id.Equals(providerID) {
|
|
return happydns.ValidationError{Msg: "you cannot change the provider identifier"}
|
|
}
|
|
|
|
err = s.validator.Validate(provider)
|
|
if err != nil {
|
|
return happydns.ValidationError{Msg: fmt.Sprintf("unable to validate provider attributes: %s", err.Error())}
|
|
}
|
|
|
|
err = s.store.UpdateProvider(provider)
|
|
if err != nil {
|
|
return happydns.InternalError{
|
|
Err: fmt.Errorf("unable to UpdateProvider in UpdateProvider: %w", err),
|
|
UserMessage: "Sorry, we are currently unable to update your provider. Please retry later.",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// UpdateProviderFromMessage updates a provider from a ProviderMessage.
|
|
func (s *Service) UpdateProviderFromMessage(providerID happydns.Identifier, user *happydns.User, p *happydns.ProviderMessage) error {
|
|
newprovider, err := ParseProvider(p)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return s.UpdateProvider(providerID, user, func(provider *happydns.Provider) {
|
|
*provider = *newprovider
|
|
})
|
|
}
|
|
|
|
// DeleteProvider deletes a provider for the given user.
|
|
func (s *Service) DeleteProvider(user *happydns.User, providerID happydns.Identifier) error {
|
|
// Verify ownership before deleting
|
|
if _, err := s.getUserProvider(user, providerID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := s.store.DeleteProvider(providerID); err != nil {
|
|
return happydns.InternalError{
|
|
Err: fmt.Errorf("failed to delete provider %s: %w", providerID.String(), err),
|
|
UserMessage: "Sorry, we are currently unable to delete your provider. Please try again later.",
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// RestrictedService wraps a ProviderUsecase with configuration-based restrictions.
|
|
type RestrictedService struct {
|
|
inner happydns.ProviderUsecase
|
|
config *happydns.Options
|
|
}
|
|
|
|
// NewRestrictedService creates a RestrictedService backed by the given configuration and storage.
|
|
func NewRestrictedService(cfg *happydns.Options, store ProviderStorage) *RestrictedService {
|
|
return &RestrictedService{
|
|
inner: NewService(store, nil),
|
|
config: cfg,
|
|
}
|
|
}
|
|
|
|
// CreateProvider refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
|
func (s *RestrictedService) CreateProvider(user *happydns.User, msg *happydns.ProviderMessage) (*happydns.Provider, error) {
|
|
if s.config.DisableProviders {
|
|
return nil, happydns.ForbiddenError{Msg: "cannot add provider as DisableProviders parameter is set."}
|
|
}
|
|
|
|
return s.inner.CreateProvider(user, msg)
|
|
}
|
|
|
|
// DeleteProvider refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
|
func (s *RestrictedService) DeleteProvider(user *happydns.User, providerID happydns.Identifier) error {
|
|
if s.config.DisableProviders {
|
|
return happydns.ForbiddenError{Msg: "cannot delete provider as DisableProviders parameter is set."}
|
|
}
|
|
|
|
return s.inner.DeleteProvider(user, providerID)
|
|
}
|
|
|
|
// UpdateProvider refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
|
func (s *RestrictedService) UpdateProvider(providerID happydns.Identifier, user *happydns.User, updateFn func(*happydns.Provider)) error {
|
|
if s.config.DisableProviders {
|
|
return happydns.ForbiddenError{Msg: "cannot update provider as DisableProviders parameter is set."}
|
|
}
|
|
|
|
return s.inner.UpdateProvider(providerID, user, updateFn)
|
|
}
|
|
|
|
// UpdateProviderFromMessage refuses the operation when DisableProviders is set, otherwise delegates to Service.
|
|
func (s *RestrictedService) UpdateProviderFromMessage(providerID happydns.Identifier, user *happydns.User, p *happydns.ProviderMessage) error {
|
|
if s.config.DisableProviders {
|
|
return happydns.ForbiddenError{Msg: "cannot update provider as DisableProviders parameter is set."}
|
|
}
|
|
|
|
return s.inner.UpdateProviderFromMessage(providerID, user, p)
|
|
}
|
|
|
|
func (s *RestrictedService) CreateDomainOnProvider(provider *happydns.Provider, fqdn string) error {
|
|
if s.config.DisableProviders {
|
|
return happydns.ForbiddenError{Msg: "cannot create domain on provider as DisableProviders parameter is set."}
|
|
}
|
|
|
|
return s.inner.CreateDomainOnProvider(provider, fqdn)
|
|
}
|
|
|
|
// Read-only operations delegate directly.
|
|
|
|
func (s *RestrictedService) GetUserProvider(user *happydns.User, providerID happydns.Identifier) (*happydns.Provider, error) {
|
|
return s.inner.GetUserProvider(user, providerID)
|
|
}
|
|
|
|
func (s *RestrictedService) GetUserProviderMeta(user *happydns.User, providerID happydns.Identifier) (*happydns.ProviderMeta, error) {
|
|
return s.inner.GetUserProviderMeta(user, providerID)
|
|
}
|
|
|
|
func (s *RestrictedService) ListUserProviders(user *happydns.User) ([]*happydns.ProviderMeta, error) {
|
|
return s.inner.ListUserProviders(user)
|
|
}
|
|
|
|
func (s *RestrictedService) ListHostedDomains(provider *happydns.Provider) ([]string, error) {
|
|
return s.inner.ListHostedDomains(provider)
|
|
}
|
|
|
|
func (s *RestrictedService) ListZoneCorrections(ctx context.Context, provider *happydns.Provider, domain *happydns.Domain, records []happydns.Record) ([]*happydns.Correction, int, error) {
|
|
return s.inner.ListZoneCorrections(ctx, provider, domain, records)
|
|
}
|
|
|
|
func (s *RestrictedService) RetrieveZone(ctx context.Context, provider *happydns.Provider, name string) ([]happydns.Record, error) {
|
|
return s.inner.RetrieveZone(ctx, provider, name)
|
|
}
|
|
|
|
func (s *RestrictedService) TestDomainExistence(provider *happydns.Provider, name string) error {
|
|
return s.inner.TestDomainExistence(provider, name)
|
|
}
|