Skip to content

Commit a160509

Browse files
authored
feat(events): org created/invited (#1634)
Signed-off-by: Miguel Martinez <[email protected]>
1 parent 27d69c3 commit a160509

File tree

11 files changed

+227
-43
lines changed

11 files changed

+227
-43
lines changed

app/controlplane/cmd/wire_gen.go

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/internal/usercontext/apitoken_middleware_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func TestWithCurrentAPITokenAndOrgMiddleware(t *testing.T) {
102102
orgRepo := bizMocks.NewOrganizationRepo(t)
103103
apiTokenUC, err := biz.NewAPITokenUseCase(apiTokenRepo, &conf.Auth{GeneratedJwsHmacSecret: "test"}, nil, nil, nil)
104104
require.NoError(t, err)
105-
orgUC := biz.NewOrganizationUseCase(orgRepo, nil, nil, nil, nil, nil)
105+
orgUC := biz.NewOrganizationUseCase(orgRepo, nil, nil, nil, nil, nil, nil)
106106
require.NoError(t, err)
107107

108108
ctx := context.Background()
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//
2+
// Copyright 2024 The Chainloop Authors.
3+
//
4+
// Licensed under the Apache License, Version 2.0 (the "License");
5+
// you may not use this file except in compliance with the License.
6+
// You may obtain a copy of the License at
7+
//
8+
// http://www.apache.org/licenses/LICENSE-2.0
9+
//
10+
// Unless required by applicable law or agreed to in writing, software
11+
// distributed under the License is distributed on an "AS IS" BASIS,
12+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
// See the License for the specific language governing permissions and
14+
// limitations under the License.
15+
16+
package events
17+
18+
import (
19+
"encoding/json"
20+
"errors"
21+
"fmt"
22+
23+
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor"
24+
"github.com/google/uuid"
25+
)
26+
27+
var (
28+
_ auditor.LogEntry = (*OrgUserJoined)(nil)
29+
_ auditor.LogEntry = (*OrgUserLeft)(nil)
30+
_ auditor.LogEntry = (*OrgCreated)(nil)
31+
)
32+
33+
const (
34+
OrgType auditor.TargetType = "Organization"
35+
userJoinedOrgActionType string = "UserJoined"
36+
userLeftOrgActionType string = "UserLeft"
37+
userInvitedToOrgActionType string = "InvitationCreated"
38+
orgCreatedActionType string = "OrganizationCreated"
39+
)
40+
41+
type OrgBase struct {
42+
OrgID *uuid.UUID `json:"org_id,omitempty"`
43+
OrgName string `json:"org_name,omitempty"`
44+
}
45+
46+
func (p *OrgBase) RequiresActor() bool {
47+
return true
48+
}
49+
50+
func (p *OrgBase) TargetType() auditor.TargetType {
51+
return OrgType
52+
}
53+
54+
func (p *OrgBase) TargetID() *uuid.UUID {
55+
return p.OrgID
56+
}
57+
58+
func (p *OrgBase) ActionInfo() (json.RawMessage, error) {
59+
if p.OrgName == "" || p.OrgID == nil {
60+
return nil, errors.New("user id and org name are required")
61+
}
62+
63+
return json.Marshal(&p)
64+
}
65+
66+
// Org created
67+
type OrgCreated struct {
68+
*OrgBase
69+
}
70+
71+
func (p *OrgCreated) ActionType() string {
72+
return orgCreatedActionType
73+
}
74+
75+
func (p *OrgCreated) Description() string {
76+
return fmt.Sprintf("{{ .ActorEmail }} has created the organization %s", p.OrgName)
77+
}
78+
79+
// user joined the organization
80+
type OrgUserJoined struct {
81+
*OrgBase
82+
}
83+
84+
func (p *OrgUserJoined) ActionType() string {
85+
return userJoinedOrgActionType
86+
}
87+
88+
func (p *OrgUserJoined) Description() string {
89+
return fmt.Sprintf("{{ .ActorEmail }} has joined the organization %s", p.OrgName)
90+
}
91+
92+
// user left the organization
93+
type OrgUserLeft struct {
94+
*OrgBase
95+
}
96+
97+
func (p *OrgUserLeft) ActionType() string {
98+
return userLeftOrgActionType
99+
}
100+
101+
func (p *OrgUserLeft) Description() string {
102+
return fmt.Sprintf("{{ .ActorEmail }} has left the organization %s", p.OrgName)
103+
}
104+
105+
// user got invited to the organization
106+
type OrgUserInvited struct {
107+
*OrgBase
108+
ReceiverEmail string
109+
Role string
110+
}
111+
112+
func (p *OrgUserInvited) ActionType() string {
113+
return userInvitedToOrgActionType
114+
}
115+
116+
func (p *OrgUserInvited) Description() string {
117+
return fmt.Sprintf("{{ .ActorEmail }} has invited %s to the organization %s with role %s", p.ReceiverEmail, p.OrgName, p.Role)
118+
}
119+
120+
func (p *OrgUserInvited) ActionInfo() (json.RawMessage, error) {
121+
if p.OrgName == "" || p.ReceiverEmail == "" {
122+
return nil, errors.New("org name and receiver emails are required")
123+
}
124+
125+
return json.Marshal(&p)
126+
}

app/controlplane/pkg/auditor/events/user.go

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,10 @@ var (
3030
_ auditor.LogEntry = (*UserLoggedIn)(nil)
3131
)
3232

33-
const UserType auditor.TargetType = "User"
34-
3533
const (
36-
userSignedUpActionType = "SignedUp"
37-
userLoggedInActionType = "LoggedIn"
34+
UserType auditor.TargetType = "User"
35+
UserSignedUpActionType string = "SignedUp"
36+
UserLoggedInActionType string = "LoggedIn"
3837
)
3938

4039
// UserBase is the base struct for policy events
@@ -43,6 +42,10 @@ type UserBase struct {
4342
Email string `json:"email,omitempty"`
4443
}
4544

45+
func (p *UserBase) RequiresActor() bool {
46+
return true
47+
}
48+
4649
func (p *UserBase) TargetType() auditor.TargetType {
4750
return UserType
4851
}
@@ -64,7 +67,7 @@ type UserSignedUp struct {
6467
}
6568

6669
func (p *UserSignedUp) ActionType() string {
67-
return userSignedUpActionType
70+
return UserSignedUpActionType
6871
}
6972

7073
func (p *UserSignedUp) Description() string {
@@ -78,7 +81,7 @@ type UserLoggedIn struct {
7881
}
7982

8083
func (p *UserLoggedIn) ActionType() string {
81-
return userLoggedInActionType
84+
return UserLoggedInActionType
8285
}
8386

8487
func (p *UserLoggedIn) Description() string {

app/controlplane/pkg/auditor/logentry.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ type LogEntry interface {
5555
TargetID() *uuid.UUID
5656
// Description returns a templatable string, see the DescriptionVariables struct.
5757
Description() string
58+
RequiresActor() bool
5859
}
5960

6061
type DescriptionVariables struct {

app/controlplane/pkg/biz/auditor.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,20 @@ func NewAuditorUseCase(p *auditor.AuditLogPublisher, logger log.Logger) *Auditor
4242
func (uc *AuditorUseCase) Dispatch(ctx context.Context, entry auditor.LogEntry, orgID *uuid.UUID) {
4343
// dynamically load user information from the context
4444
opts := []auditor.GeneratorOption{}
45+
var gotActor bool
4546
if user := entities.CurrentUser(ctx); user != nil {
4647
parsedUUID, _ := uuid.Parse(user.ID)
4748
opts = append(opts, auditor.WithActor(auditor.ActorTypeUser, parsedUUID, user.Email))
49+
gotActor = true
4850
} else if apiToken := entities.CurrentAPIToken(ctx); apiToken != nil {
4951
parsedUUID, _ := uuid.Parse(apiToken.ID)
5052
opts = append(opts, auditor.WithActor(auditor.ActorTypeAPIToken, parsedUUID, ""))
53+
gotActor = true
54+
}
55+
56+
if !gotActor && entry.RequiresActor() {
57+
uc.log.Warn("failed to get actor information, required by the audit log entry")
58+
return
5159
}
5260

5361
if orgID != nil {

app/controlplane/pkg/biz/membership.go

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"fmt"
2121
"time"
2222

23+
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor/events"
2324
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
2425
"github.com/go-kratos/kratos/v2/log"
2526
"github.com/google/uuid"
@@ -51,10 +52,11 @@ type MembershipUseCase struct {
5152
repo MembershipRepo
5253
orgUseCase *OrganizationUseCase
5354
logger *log.Helper
55+
auditor *AuditorUseCase
5456
}
5557

56-
func NewMembershipUseCase(repo MembershipRepo, orgUC *OrganizationUseCase, logger log.Logger) *MembershipUseCase {
57-
return &MembershipUseCase{repo, orgUC, log.NewHelper(logger)}
58+
func NewMembershipUseCase(repo MembershipRepo, orgUC *OrganizationUseCase, auditor *AuditorUseCase, logger log.Logger) *MembershipUseCase {
59+
return &MembershipUseCase{repo, orgUC, log.NewHelper(logger), auditor}
5860
}
5961

6062
// LeaveAndDeleteOrg deletes a membership (and the org i) from the database associated with the current user
@@ -83,6 +85,13 @@ func (uc *MembershipUseCase) LeaveAndDeleteOrg(ctx context.Context, userID, memb
8385
return fmt.Errorf("failed to delete membership: %w", err)
8486
}
8587

88+
uc.auditor.Dispatch(ctx, &events.OrgUserLeft{
89+
OrgBase: &events.OrgBase{
90+
OrgID: &m.OrganizationID,
91+
OrgName: m.Org.Name,
92+
},
93+
}, &m.OrganizationID)
94+
8695
// Check number of members in the org
8796
// If it's the only one, delete the org
8897
membershipsInOrg, err := uc.repo.FindByOrg(ctx, m.OrganizationID)

app/controlplane/pkg/biz/organization.go

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"io"
2323
"time"
2424

25+
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor/events"
2526
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
2627
config "github.com/chainloop-dev/chainloop/app/controlplane/pkg/conf/controlplane/config/v1"
2728
"github.com/chainloop-dev/chainloop/pkg/servicelogger"
@@ -49,9 +50,10 @@ type OrganizationUseCase struct {
4950
integrationUC *IntegrationUseCase
5051
membershipRepo MembershipRepo
5152
onboardingConfig []*config.OnboardingSpec
53+
auditor *AuditorUseCase
5254
}
5355

54-
func NewOrganizationUseCase(repo OrganizationRepo, repoUC *CASBackendUseCase, iUC *IntegrationUseCase, mRepo MembershipRepo, onboardingConfig []*config.OnboardingSpec, l log.Logger) *OrganizationUseCase {
56+
func NewOrganizationUseCase(repo OrganizationRepo, repoUC *CASBackendUseCase, auditor *AuditorUseCase, iUC *IntegrationUseCase, mRepo MembershipRepo, onboardingConfig []*config.OnboardingSpec, l log.Logger) *OrganizationUseCase {
5557
if l == nil {
5658
l = log.NewStdLogger(io.Discard)
5759
}
@@ -62,6 +64,7 @@ func NewOrganizationUseCase(repo OrganizationRepo, repoUC *CASBackendUseCase, iU
6264
integrationUC: iUC,
6365
membershipRepo: mRepo,
6466
onboardingConfig: onboardingConfig,
67+
auditor: auditor,
6568
}
6669
}
6770

@@ -147,6 +150,15 @@ func (uc *OrganizationUseCase) doCreate(ctx context.Context, name string, opts .
147150
}
148151
}
149152

153+
orgUUID, err := uuid.Parse(org.ID)
154+
if err != nil {
155+
return nil, NewErrInvalidUUID(err)
156+
}
157+
158+
uc.auditor.Dispatch(ctx, &events.OrgCreated{
159+
OrgBase: &events.OrgBase{OrgID: &orgUUID, OrgName: org.Name}}, &orgUUID,
160+
)
161+
150162
return org, nil
151163
}
152164

app/controlplane/pkg/biz/organization_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
2424
repoM "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz/mocks"
2525
"github.com/go-kratos/kratos/v2/log"
26+
"github.com/google/uuid"
2627
"github.com/stretchr/testify/mock"
2728
"github.com/stretchr/testify/suite"
2829
)
@@ -33,14 +34,15 @@ type organizationTestSuite struct {
3334

3435
func (s *organizationTestSuite) TestCreateWithRandomName() {
3536
repo := repoM.NewOrganizationRepo(s.T())
36-
uc := biz.NewOrganizationUseCase(repo, nil, nil, nil, nil, log.NewStdLogger(io.Discard))
37+
l := log.NewStdLogger(io.Discard)
38+
uc := biz.NewOrganizationUseCase(repo, nil, biz.NewAuditorUseCase(nil, l), nil, nil, nil, l)
3739

3840
s.Run("the org exists, we retry", func() {
3941
ctx := context.Background()
4042
// the first one fails because it already exists
4143
repo.On("Create", ctx, mock.Anything).Once().Return(nil, biz.NewErrAlreadyExistsStr("it already exists"))
4244
// but the second call creates the org
43-
repo.On("Create", ctx, mock.Anything).Once().Return(&biz.Organization{Name: "foobar"}, nil)
45+
repo.On("Create", ctx, mock.Anything).Once().Return(&biz.Organization{Name: "foobar", ID: uuid.NewString()}, nil)
4446
got, err := uc.CreateWithRandomName(ctx)
4547
s.NoError(err)
4648
s.Equal("foobar", got.Name)

0 commit comments

Comments
 (0)