Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add legal notice URL to instance settings #4492

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading