Skip to content

Commit 30970ef

Browse files
authored
chore: improvements on org management (#546)
Signed-off-by: Miguel Martinez Trivino <[email protected]>
1 parent b610907 commit 30970ef

31 files changed

+654
-72
lines changed

app/cli/cmd/organization_leave.go

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -17,7 +17,6 @@ package cmd
1717

1818
import (
1919
"context"
20-
"errors"
2120
"fmt"
2221

2322
"github.com/chainloop-dev/chainloop/app/cli/internal/action"
@@ -54,10 +53,6 @@ func newOrganizationLeaveCmd() *cobra.Command {
5453
return fmt.Errorf("organization %s not found", orgID)
5554
}
5655

57-
if membership.Current {
58-
return errors.New("can't leave the `current` organization. To leave this org, please switch to another one")
59-
}
60-
6156
fmt.Printf("You are about to leave the organization %q\n", membership.Org.Name)
6257

6358
// Ask for confirmation

app/controlplane/cmd/wire_gen.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/internal/biz/membership.go

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -107,7 +107,17 @@ func (uc *MembershipUseCase) Create(ctx context.Context, orgID, userID string, c
107107
return nil, NewErrInvalidUUID(err)
108108
}
109109

110-
return uc.repo.Create(ctx, orgUUID, userUUID, current)
110+
m, err := uc.repo.Create(ctx, orgUUID, userUUID, current)
111+
if err != nil {
112+
return nil, fmt.Errorf("failed to create membership: %w", err)
113+
}
114+
115+
if !current {
116+
return m, nil
117+
}
118+
119+
// Set the current membership again to make sure we uncheck the previous ones
120+
return uc.repo.SetCurrent(ctx, m.ID)
111121
}
112122

113123
func (uc *MembershipUseCase) ByUser(ctx context.Context, userID string) ([]*Membership, error) {
@@ -128,6 +138,8 @@ func (uc *MembershipUseCase) ByOrg(ctx context.Context, orgID string) ([]*Member
128138
return uc.repo.FindByOrg(ctx, orgUUID)
129139
}
130140

141+
// SetCurrent sets the current membership for the user
142+
// and unsets the previous one
131143
func (uc *MembershipUseCase) SetCurrent(ctx context.Context, userID, membershipID string) (*Membership, error) {
132144
userUUID, err := uuid.Parse(userID)
133145
if err != nil {
@@ -148,3 +160,24 @@ func (uc *MembershipUseCase) SetCurrent(ctx context.Context, userID, membershipI
148160

149161
return uc.repo.SetCurrent(ctx, mUUID)
150162
}
163+
164+
func (uc *MembershipUseCase) FindByOrgAndUser(ctx context.Context, orgID, userID string) (*Membership, error) {
165+
orgUUID, err := uuid.Parse(orgID)
166+
if err != nil {
167+
return nil, NewErrInvalidUUID(err)
168+
}
169+
170+
userUUID, err := uuid.Parse(userID)
171+
if err != nil {
172+
return nil, NewErrInvalidUUID(err)
173+
}
174+
175+
m, err := uc.repo.FindByOrgAndUser(ctx, orgUUID, userUUID)
176+
if err != nil {
177+
return nil, fmt.Errorf("failed to find membership: %w", err)
178+
} else if m == nil {
179+
return nil, NewErrNotFound("membership")
180+
}
181+
182+
return m, nil
183+
}

app/controlplane/internal/biz/membership_integration_test.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -99,22 +99,16 @@ func (s *membershipIntegrationTestSuite) TestCreateMembership() {
9999
user, err := s.User.FindOrCreateByEmail(ctx, "[email protected]")
100100
assert.NoError(err)
101101

102-
s.T().Run("Create default", func(t *testing.T) {
102+
s.T().Run("Create current", func(t *testing.T) {
103103
org, err := s.Organization.CreateWithRandomName(ctx)
104104
assert.NoError(err)
105105

106106
m, err := s.Membership.Create(ctx, org.ID, user.ID, true)
107107
assert.NoError(err)
108108
assert.Equal(true, m.Current, "Membership should be current")
109109

110-
wantUserID, err := uuid.Parse(user.ID)
111-
assert.NoError(err)
112-
assert.Equal(wantUserID, m.UserID, "User ID")
113-
114-
wantORGID, err := uuid.Parse(org.ID)
115-
assert.NoError(err)
116-
assert.Equal(wantORGID, m.OrganizationID, "Organization ID")
117-
110+
assert.Equal(user.ID, m.UserID.String(), "User ID")
111+
assert.Equal(org.ID, m.OrganizationID.String(), "Organization ID")
118112
assert.EqualValues(org, m.Org, "Embedded organization")
119113
})
120114

@@ -124,7 +118,26 @@ func (s *membershipIntegrationTestSuite) TestCreateMembership() {
124118

125119
m, err := s.Membership.Create(ctx, org.ID, user.ID, false)
126120
assert.NoError(err)
127-
assert.Equal(false, m.Current, "Membership should be current")
121+
assert.Equal(false, m.Current, "Membership should not be current")
122+
})
123+
124+
s.T().Run("current override", func(t *testing.T) {
125+
org, err := s.Organization.CreateWithRandomName(ctx)
126+
assert.NoError(err)
127+
org2, err := s.Organization.CreateWithRandomName(ctx)
128+
assert.NoError(err)
129+
130+
m, err := s.Membership.Create(ctx, org.ID, user.ID, true)
131+
assert.NoError(err)
132+
s.True(m.Current)
133+
// Creating a new one will override the current status of the previous one
134+
m, err = s.Membership.Create(ctx, org2.ID, user.ID, true)
135+
assert.NoError(err)
136+
s.True(m.Current)
137+
138+
m, err = s.Membership.FindByOrgAndUser(ctx, org.ID, user.ID)
139+
assert.NoError(err)
140+
s.False(m.Current)
128141
})
129142

130143
s.T().Run("Invalid ORG", func(t *testing.T) {

app/controlplane/internal/biz/organization.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -57,7 +57,20 @@ func NewOrganizationUseCase(repo OrganizationRepo, repoUC *CASBackendUseCase, iU
5757

5858
const OrganizationRandomNameMaxTries = 10
5959

60-
func (uc *OrganizationUseCase) CreateWithRandomName(ctx context.Context) (*Organization, error) {
60+
type createOptions struct {
61+
createInlineBackend bool
62+
}
63+
64+
type CreateOpt func(*createOptions)
65+
66+
// Optionally create an inline CAS-backend
67+
func WithCreateInlineBackend() CreateOpt {
68+
return func(o *createOptions) {
69+
o.createInlineBackend = true
70+
}
71+
}
72+
73+
func (uc *OrganizationUseCase) CreateWithRandomName(ctx context.Context, opts ...CreateOpt) (*Organization, error) {
6174
// Try 10 times to create a random name
6275
for i := 0; i < OrganizationRandomNameMaxTries; i++ {
6376
// Create a random name
@@ -66,7 +79,7 @@ func (uc *OrganizationUseCase) CreateWithRandomName(ctx context.Context) (*Organ
6679
return nil, fmt.Errorf("failed to generate random name: %w", err)
6780
}
6881

69-
org, err := uc.doCreate(ctx, name)
82+
org, err := uc.doCreate(ctx, name, opts...)
7083
if err != nil {
7184
// We retry if the organization already exists
7285
if errors.Is(err, ErrAlreadyExists) {
@@ -84,8 +97,8 @@ func (uc *OrganizationUseCase) CreateWithRandomName(ctx context.Context) (*Organ
8497
}
8598

8699
// Create an organization with the given name
87-
func (uc *OrganizationUseCase) Create(ctx context.Context, name string) (*Organization, error) {
88-
org, err := uc.doCreate(ctx, name)
100+
func (uc *OrganizationUseCase) Create(ctx context.Context, name string, opts ...CreateOpt) (*Organization, error) {
101+
org, err := uc.doCreate(ctx, name, opts...)
89102
if err != nil {
90103
if errors.Is(err, ErrAlreadyExists) {
91104
return nil, NewErrValidationStr("organization already exists")
@@ -99,18 +112,30 @@ func (uc *OrganizationUseCase) Create(ctx context.Context, name string) (*Organi
99112

100113
var errOrgName = errors.New("org names must only contain lowercase letters, numbers, or hyphens. Examples of valid org names are \"myorg\", \"myorg-123\"")
101114

102-
func (uc *OrganizationUseCase) doCreate(ctx context.Context, name string) (*Organization, error) {
115+
func (uc *OrganizationUseCase) doCreate(ctx context.Context, name string, opts ...CreateOpt) (*Organization, error) {
103116
uc.logger.Infow("msg", "Creating organization", "name", name)
104117

105118
if err := ValidateOrgName(name); err != nil {
106119
return nil, NewErrValidation(errOrgName)
107120
}
108121

122+
options := &createOptions{}
123+
for _, o := range opts {
124+
o(options)
125+
}
126+
109127
org, err := uc.orgRepo.Create(ctx, name)
110128
if err != nil {
111129
return nil, fmt.Errorf("failed to create organization: %w", err)
112130
}
113131

132+
if options.createInlineBackend {
133+
// Create inline CAS-backend
134+
if _, err := uc.casBackendUseCase.CreateInlineFallbackBackend(ctx, org.ID); err != nil {
135+
return nil, fmt.Errorf("failed to create fallback backend: %w", err)
136+
}
137+
}
138+
114139
return org, nil
115140
}
116141

app/controlplane/internal/biz/organization_integration_test.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -84,6 +84,28 @@ func (s *OrgIntegrationTestSuite) TestCreate() {
8484
}
8585
}
8686

87+
func (s *OrgIntegrationTestSuite) TestCreateAddsInlineCASBackend() {
88+
ctx := context.Background()
89+
s.Run("by default it does not create it", func() {
90+
org, err := s.Organization.CreateWithRandomName(ctx)
91+
s.NoError(err)
92+
// Creating an org also creates a new inline backend
93+
b, err := s.CASBackend.FindDefaultBackend(ctx, org.ID)
94+
s.Error(err)
95+
s.Nil(b)
96+
})
97+
98+
s.Run("with the option it creates it", func() {
99+
org, err := s.Organization.Create(ctx, "with-inline", biz.WithCreateInlineBackend())
100+
s.NoError(err)
101+
102+
// Creating an org also creates a new inline backend
103+
b, err := s.CASBackend.FindDefaultBackend(ctx, org.ID)
104+
s.NoError(err)
105+
s.True(b.Inline)
106+
})
107+
}
108+
87109
func (s *OrgIntegrationTestSuite) TestUpdate() {
88110
ctx := context.Background()
89111

app/controlplane/internal/biz/user.go

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -110,6 +110,7 @@ func (uc *UserUseCase) FindByID(ctx context.Context, userID string) (*User, erro
110110
}
111111

112112
// Find the organization associated with the user that's marked as current
113+
// If none is selected, it will pick the first one and set it as current
113114
func (uc *UserUseCase) CurrentOrg(ctx context.Context, userID string) (*Organization, error) {
114115
memberships, err := uc.membershipUseCase.ByUser(ctx, userID)
115116
if err != nil {
@@ -121,15 +122,25 @@ func (uc *UserUseCase) CurrentOrg(ctx context.Context, userID string) (*Organiza
121122
return nil, errors.New("user does not have any organization associated")
122123
}
123124

124-
// By default we set the first one
125-
currentOrg := memberships[0].OrganizationID
125+
var currentOrgID uuid.UUID
126126
for _, m := range memberships {
127127
// Override if it's being explicitly selected
128128
if m.Current {
129-
currentOrg = m.OrganizationID
129+
currentOrgID = m.OrganizationID
130130
break
131131
}
132132
}
133133

134-
return uc.organizationUseCase.FindByID(ctx, currentOrg.String())
134+
if currentOrgID == uuid.Nil {
135+
// If none is selected, we configure the first one
136+
_, err := uc.membershipUseCase.SetCurrent(ctx, userID, memberships[0].ID.String())
137+
if err != nil {
138+
return nil, fmt.Errorf("error setting current org: %w", err)
139+
}
140+
141+
// Call itself recursively now that we have a current org
142+
return uc.CurrentOrg(ctx, userID)
143+
}
144+
145+
return uc.organizationUseCase.FindByID(ctx, currentOrgID.String())
135146
}

app/controlplane/internal/biz/user_integration_test.go

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
//
2-
// Copyright 2023 The Chainloop Authors.
2+
// Copyright 2024 The Chainloop Authors.
33
//
44
// Licensed under the Apache License, Version 2.0 (the "License");
55
// you may not use this file except in compliance with the License.
@@ -58,6 +58,59 @@ func (s *userIntegrationTestSuite) TestDeleteUser() {
5858
s.Empty(gotMembership)
5959
}
6060

61+
func (s *userIntegrationTestSuite) TestCurrentOrg() {
62+
ctx := context.Background()
63+
s.Run("if there is an associated, default org it's returned", func() {
64+
// userOne has a default org
65+
m, err := s.Membership.FindByOrgAndUser(ctx, s.sharedOrg.ID, s.userOne.ID)
66+
s.NoError(err)
67+
s.True(m.Current)
68+
69+
// and it's returned as currentOrg
70+
got, err := s.User.CurrentOrg(ctx, s.userOne.ID)
71+
s.NoError(err)
72+
s.Equal(s.sharedOrg, got)
73+
})
74+
75+
s.Run("they have more orgs but none of them is the default, it will return the first one as default", func() {
76+
m, err := s.Membership.FindByOrgAndUser(ctx, s.sharedOrg.ID, s.userOne.ID)
77+
s.NoError(err)
78+
s.True(m.Current)
79+
// leave the current org
80+
err = s.Membership.DeleteWithOrg(ctx, s.userOne.ID, m.ID.String())
81+
s.NoError(err)
82+
83+
// none of the orgs is marked as current
84+
mems, _ := s.Membership.ByUser(ctx, s.userOne.ID)
85+
s.Len(mems, 1)
86+
s.False(mems[0].Current)
87+
88+
// asking for the current org will return the first one
89+
got, err := s.User.CurrentOrg(ctx, s.userOne.ID)
90+
s.NoError(err)
91+
s.Equal(s.userOneOrg, got)
92+
93+
// and now the membership will be set as current
94+
mems, _ = s.Membership.ByUser(ctx, s.userOne.ID)
95+
s.Len(mems, 1)
96+
s.True(mems[0].Current)
97+
})
98+
99+
s.Run("it will fail if there are no membershipts", func() {
100+
// none of the orgs is marked as current
101+
mems, _ := s.Membership.ByUser(ctx, s.userOne.ID)
102+
s.Len(mems, 1)
103+
// leave the current org
104+
err := s.Membership.DeleteWithOrg(ctx, s.userOne.ID, mems[0].ID.String())
105+
s.NoError(err)
106+
mems, _ = s.Membership.ByUser(ctx, s.userOne.ID)
107+
s.Len(mems, 0)
108+
109+
_, err = s.User.CurrentOrg(ctx, s.userOne.ID)
110+
s.ErrorContains(err, "user does not have any organization associated")
111+
})
112+
}
113+
61114
// Run the tests
62115
func TestUserUseCase(t *testing.T) {
63116
suite.Run(t, new(userIntegrationTestSuite))

app/controlplane/internal/data/ent/apitoken/apitoken.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)