Skip to content

Commit

Permalink
feat: Add legal notice URL to instance settings (#4492)
Browse files Browse the repository at this point in the history
When an instance's context has an associated cloudery and the
instance's partner on the cloudery has defined a legal notice, the URL
to this notice will be present in the instance settings returned by
`GET /settings/instance` calls.
  • Loading branch information
taratatach authored Nov 21, 2024
2 parents 7d74936 + 0b8cc2d commit 29d1c86
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 98 deletions.
11 changes: 9 additions & 2 deletions docs/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -690,13 +690,14 @@ Cookie: sessionid=xxxx
"auth_mode": "basic",
"default_redirection": "drive/#/folder",
"context": "dev",
"sponsorships": ["springfield"]
"sponsorships": ["springfield"],
"legal_notice_url": "https://manager.cozycloud.cc/e96388a5-8eed-44cc-81e6-40aad273f0d4.pdf"
}
}
}
```

#### Note about `password_defined`
##### Note about `password_defined`

There are a few fields that are persisted on the instance its-self, not on its
settings document. When they are updated, it won't be reflected in the realtime
Expand All @@ -706,6 +707,12 @@ For `password_defined`, it is possible to be notified when the password is
defined by watching a synthetic document with the doctype `io.cozy.settings`,
and the id `io.cozy.settings.passphrase`.

##### Note about `legal_notice_url`

This attribute will only be present if a manager is associated with the
instance and the instance was created on behalf of a partner with a defined
legal notice.

### POST /settings/instance/deletion

The settings application can use this route if the user wants to delete their
Expand Down
1 change: 1 addition & 0 deletions model/cloudery/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var service Service
type Service interface {
SaveInstance(inst *instance.Instance, cmd *SaveCmd) error
BlockingSubscription(inst *instance.Instance) (*BlockingSubscription, error)
LegalNoticeUrl(inst *instance.Instance) (string, error)
}

func Init(contexts map[string]config.ClouderyConfig) Service {
Expand Down
41 changes: 26 additions & 15 deletions model/cloudery/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (

"github.com/cozy/cozy-stack/model/instance"
"github.com/cozy/cozy-stack/pkg/config/config"
"github.com/cozy/cozy-stack/pkg/manager"
)

var (
Expand Down Expand Up @@ -38,9 +37,9 @@ type SaveCmd struct {

// SaveInstance data into the cloudery matching the instance context.
func (s *ClouderyService) SaveInstance(inst *instance.Instance, cmd *SaveCmd) error {
client, err := s.getClient(inst)
if err != nil {
return err
client := instance.APIManagerClient(inst)
if client == nil {
return nil
}

url := fmt.Sprintf("/api/v1/instances/%s?source=stack", url.PathEscape(inst.UUID))
Expand All @@ -60,9 +59,9 @@ type BlockingSubscription struct {
}

func (s *ClouderyService) BlockingSubscription(inst *instance.Instance) (*BlockingSubscription, error) {
client, err := s.getClient(inst)
if err != nil {
return nil, err
client := instance.APIManagerClient(inst)
if client == nil {
return nil, nil
}

url := fmt.Sprintf("/api/v1/instances/%s", url.PathEscape(inst.UUID))
Expand Down Expand Up @@ -97,17 +96,29 @@ func blockingSubscriptionVendor(clouderyInstance map[string]interface{}) (string
return "", fmt.Errorf("invalid blocking subscription vendor")
}

func (s *ClouderyService) getClient(inst *instance.Instance) (*manager.APIClient, error) {
cfg, ok := s.contexts[inst.ContextName]
if !ok {
cfg, ok = s.contexts[config.DefaultInstanceContext]
func (s *ClouderyService) LegalNoticeUrl(inst *instance.Instance) (string, error) {
client := instance.APIManagerClient(inst)
if client == nil {
return "", nil
}

if !ok {
return nil, fmt.Errorf("%w: tried %q and %q", ErrInvalidContext, inst.ContextName, config.DefaultInstanceContext)
url := fmt.Sprintf("/api/v1/instances/%s", url.PathEscape(inst.UUID))
res, err := client.Get(url)
if err != nil {
return "", fmt.Errorf("request failed: %w", err)
}

client := manager.NewAPIClient(cfg.API.URL, cfg.API.Token)
return legalNoticeUrl(res)
}

func legalNoticeUrl(clouderyInstance map[string]interface{}) (string, error) {
if str, ok := clouderyInstance["legal_notice_url"]; ok {
if url, ok := str.(string); ok {
return url, nil
}

return "", fmt.Errorf("invalid legal notice url")
}

return client, nil
return "", nil
}
10 changes: 10 additions & 0 deletions model/cloudery/service_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,13 @@ func (m *Mock) BlockingSubscription(inst *instance.Instance) (*BlockingSubscript

return args.Get(0).(*BlockingSubscription), args.Error(1)
}

func (m *Mock) LegalNoticeUrl(inst *instance.Instance) (string, error) {
args := m.Called(inst)

if args.Get(0) == "" {
return "", args.Error(1)
}

return args.Get(0).(string), args.Error(1)
}
4 changes: 4 additions & 0 deletions model/cloudery/service_noop.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ func (s *NoopService) SaveInstance(inst *instance.Instance, cmd *SaveCmd) error
func (s *NoopService) BlockingSubscription(inst *instance.Instance) (*BlockingSubscription, error) {
return nil, nil
}

func (s *NoopService) LegalNoticeUrl(inst *instance.Instance) (string, error) {
return "", nil
}
1 change: 1 addition & 0 deletions model/settings/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Service interface {
ConfirmEmailUpdate(inst *instance.Instance, tok string) error
CancelEmailUpdate(inst *instance.Instance) error
GetExternalTies(inst *instance.Instance) (*ExternalTies, error)
GetLegalNoticeUrl(inst *instance.Instance) (string, error)
}

func Init(
Expand Down
4 changes: 4 additions & 0 deletions model/settings/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,7 @@ func (s *SettingsService) GetExternalTies(inst *instance.Instance) (*ExternalTie

return &ties, nil
}

func (s *SettingsService) GetLegalNoticeUrl(inst *instance.Instance) (string, error) {
return s.cloudery.LegalNoticeUrl(inst)
}
10 changes: 10 additions & 0 deletions model/settings/service_mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,13 @@ func (m *Mock) GetExternalTies(inst *instance.Instance) (*ExternalTies, error) {

return args.Get(0).(*ExternalTies), args.Error(1)
}

func (m *Mock) GetLegalNoticeUrl(inst *instance.Instance) (string, error) {
args := m.Called(inst)

if args.Get(0) == "" {
return "", args.Error(1)
}

return args.Get(0).(string), args.Error(1)
}
136 changes: 59 additions & 77 deletions model/settings/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,28 @@ import (
"github.com/stretchr/testify/assert"
)

func TestServiceImplems(t *testing.T) {
assert.Implements(t, (*Service)(nil), new(SettingsService))
assert.Implements(t, (*Service)(nil), new(Mock))
}

func Test_StartEmailUpdate_success(t *testing.T) {
func setupTest(t *testing.T) (*emailer.Mock, *instance.Mock, *token.Mock, *cloudery.Mock, *storageMock, Service) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
return emailerSvc,
instSvc,
tokenSvc,
clouderySvc,
storage,
NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
}

func TestServiceImplems(t *testing.T) {
assert.Implements(t, (*Service)(nil), new(SettingsService))
assert.Implements(t, (*Service)(nil), new(Mock))
}

func Test_StartEmailUpdate_success(t *testing.T) {
emailerSvc, instSvc, tokenSvc, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand Down Expand Up @@ -64,13 +73,7 @@ func Test_StartEmailUpdate_success(t *testing.T) {
}

func Test_StartEmailUpdate_with_an_invalid_password(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, instSvc, _, _, _, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -88,13 +91,7 @@ func Test_StartEmailUpdate_with_an_invalid_password(t *testing.T) {
}

func Test_StartEmailUpdate_with_a_missing_public_name(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
emailerSvc, instSvc, tokenSvc, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand Down Expand Up @@ -135,13 +132,7 @@ func Test_StartEmailUpdate_with_a_missing_public_name(t *testing.T) {
}

func TestConfirmEmailUpdate_success(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, tokenSvc, clouderySvc, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand Down Expand Up @@ -178,13 +169,7 @@ func TestConfirmEmailUpdate_success(t *testing.T) {
}

func TestConfirmEmailUpdate_with_an_invalid_token(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, tokenSvc, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -206,13 +191,7 @@ func TestConfirmEmailUpdate_with_an_invalid_token(t *testing.T) {
}

func TestConfirmEmailUpdate_without_a_pending_email(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, _, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -231,13 +210,7 @@ func TestConfirmEmailUpdate_without_a_pending_email(t *testing.T) {
}

func Test_CancelEmailUpdate_success(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, _, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -264,13 +237,7 @@ func Test_CancelEmailUpdate_success(t *testing.T) {
}

func Test_CancelEmailUpdate_without_pending_email(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, _, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -288,13 +255,7 @@ func Test_CancelEmailUpdate_without_pending_email(t *testing.T) {
}

func Test_ResendEmailUpdate_success(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
emailerSvc, _, tokenSvc, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand Down Expand Up @@ -323,13 +284,7 @@ func Test_ResendEmailUpdate_success(t *testing.T) {
}

func Test_ResendEmailUpdate_with_no_pending_email(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, _, _, storage, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand All @@ -347,13 +302,7 @@ func Test_ResendEmailUpdate_with_no_pending_email(t *testing.T) {
}

func Test_GetExternalTies(t *testing.T) {
emailerSvc := emailer.NewMock(t)
instSvc := instance.NewMock(t)
tokenSvc := token.NewMock(t)
clouderySvc := cloudery.NewMock(t)
storage := newStorageMock(t)

svc := NewService(emailerSvc, instSvc, tokenSvc, clouderySvc, storage)
_, _, _, clouderySvc, _, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
Expand Down Expand Up @@ -386,3 +335,36 @@ func Test_GetExternalTies(t *testing.T) {
assert.Nil(t, ties)
})
}

func Test_GetLegalNoticeUrl(t *testing.T) {
_, _, _, clouderySvc, _, svc := setupTest(t)

inst := instance.Instance{
Domain: "foo.mycozy.cloud",
}

t.Run("with a legal notice", func(t *testing.T) {
clouderySvc.On("LegalNoticeUrl", &inst).Return("https://testmanager.cozycloud.cc", nil).Once()

url, err := svc.GetLegalNoticeUrl(&inst)
assert.NoError(t, err)
assert.Equal(t, "https://testmanager.cozycloud.cc", url)
})

t.Run("without a legal notice", func(t *testing.T) {
clouderySvc.On("LegalNoticeUrl", &inst).Return("", nil).Once()

url, err := svc.GetLegalNoticeUrl(&inst)
assert.NoError(t, err)
assert.Equal(t, "", url)
})

t.Run("with error from cloudery", func(t *testing.T) {
unauthorizedError := errors.New("unauthorized")
clouderySvc.On("LegalNoticeUrl", &inst).Return("", unauthorizedError).Once()

url, err := svc.GetLegalNoticeUrl(&inst)
assert.ErrorIs(t, err, unauthorizedError)
assert.Equal(t, "", url)
})
}
8 changes: 8 additions & 0 deletions web/settings/instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,14 @@ func (h *HTTPHandler) getInstance(c echo.Context) error {
return err
}

url, err := h.svc.GetLegalNoticeUrl(inst)
if err != nil {
return err
}
if url != "" {
doc.M["legal_notice_url"] = url
}

return jsonapi.Data(c, http.StatusOK, &apiInstance{doc}, nil)
}

Expand Down
Loading

0 comments on commit 29d1c86

Please sign in to comment.