Skip to content

Commit

Permalink
feat(events): org created/invited (#1634)
Browse files Browse the repository at this point in the history
Signed-off-by: Miguel Martinez <[email protected]>
  • Loading branch information
migmartri authored Dec 10, 2024
1 parent 27d69c3 commit a160509
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 43 deletions.
30 changes: 15 additions & 15 deletions app/controlplane/cmd/wire_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func TestWithCurrentAPITokenAndOrgMiddleware(t *testing.T) {
orgRepo := bizMocks.NewOrganizationRepo(t)
apiTokenUC, err := biz.NewAPITokenUseCase(apiTokenRepo, &conf.Auth{GeneratedJwsHmacSecret: "test"}, nil, nil, nil)
require.NoError(t, err)
orgUC := biz.NewOrganizationUseCase(orgRepo, nil, nil, nil, nil, nil)
orgUC := biz.NewOrganizationUseCase(orgRepo, nil, nil, nil, nil, nil, nil)
require.NoError(t, err)

ctx := context.Background()
Expand Down
126 changes: 126 additions & 0 deletions app/controlplane/pkg/auditor/events/organization.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
//
// Copyright 2024 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package events

import (
"encoding/json"
"errors"
"fmt"

"github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor"
"github.com/google/uuid"
)

var (
_ auditor.LogEntry = (*OrgUserJoined)(nil)
_ auditor.LogEntry = (*OrgUserLeft)(nil)
_ auditor.LogEntry = (*OrgCreated)(nil)
)

const (
OrgType auditor.TargetType = "Organization"
userJoinedOrgActionType string = "UserJoined"
userLeftOrgActionType string = "UserLeft"
userInvitedToOrgActionType string = "InvitationCreated"
orgCreatedActionType string = "OrganizationCreated"
)

type OrgBase struct {
OrgID *uuid.UUID `json:"org_id,omitempty"`
OrgName string `json:"org_name,omitempty"`
}

func (p *OrgBase) RequiresActor() bool {
return true
}

func (p *OrgBase) TargetType() auditor.TargetType {
return OrgType
}

func (p *OrgBase) TargetID() *uuid.UUID {
return p.OrgID
}

func (p *OrgBase) ActionInfo() (json.RawMessage, error) {
if p.OrgName == "" || p.OrgID == nil {
return nil, errors.New("user id and org name are required")
}

return json.Marshal(&p)
}

// Org created
type OrgCreated struct {
*OrgBase
}

func (p *OrgCreated) ActionType() string {
return orgCreatedActionType
}

func (p *OrgCreated) Description() string {
return fmt.Sprintf("{{ .ActorEmail }} has created the organization %s", p.OrgName)
}

// user joined the organization
type OrgUserJoined struct {
*OrgBase
}

func (p *OrgUserJoined) ActionType() string {
return userJoinedOrgActionType
}

func (p *OrgUserJoined) Description() string {
return fmt.Sprintf("{{ .ActorEmail }} has joined the organization %s", p.OrgName)
}

// user left the organization
type OrgUserLeft struct {
*OrgBase
}

func (p *OrgUserLeft) ActionType() string {
return userLeftOrgActionType
}

func (p *OrgUserLeft) Description() string {
return fmt.Sprintf("{{ .ActorEmail }} has left the organization %s", p.OrgName)
}

// user got invited to the organization
type OrgUserInvited struct {
*OrgBase
ReceiverEmail string
Role string
}

func (p *OrgUserInvited) ActionType() string {
return userInvitedToOrgActionType
}

func (p *OrgUserInvited) Description() string {
return fmt.Sprintf("{{ .ActorEmail }} has invited %s to the organization %s with role %s", p.ReceiverEmail, p.OrgName, p.Role)
}

func (p *OrgUserInvited) ActionInfo() (json.RawMessage, error) {
if p.OrgName == "" || p.ReceiverEmail == "" {
return nil, errors.New("org name and receiver emails are required")
}

return json.Marshal(&p)
}
15 changes: 9 additions & 6 deletions app/controlplane/pkg/auditor/events/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,10 @@ var (
_ auditor.LogEntry = (*UserLoggedIn)(nil)
)

const UserType auditor.TargetType = "User"

const (
userSignedUpActionType = "SignedUp"
userLoggedInActionType = "LoggedIn"
UserType auditor.TargetType = "User"
UserSignedUpActionType string = "SignedUp"
UserLoggedInActionType string = "LoggedIn"
)

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

func (p *UserBase) RequiresActor() bool {
return true
}

func (p *UserBase) TargetType() auditor.TargetType {
return UserType
}
Expand All @@ -64,7 +67,7 @@ type UserSignedUp struct {
}

func (p *UserSignedUp) ActionType() string {
return userSignedUpActionType
return UserSignedUpActionType
}

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

func (p *UserLoggedIn) ActionType() string {
return userLoggedInActionType
return UserLoggedInActionType
}

func (p *UserLoggedIn) Description() string {
Expand Down
1 change: 1 addition & 0 deletions app/controlplane/pkg/auditor/logentry.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type LogEntry interface {
TargetID() *uuid.UUID
// Description returns a templatable string, see the DescriptionVariables struct.
Description() string
RequiresActor() bool
}

type DescriptionVariables struct {
Expand Down
8 changes: 8 additions & 0 deletions app/controlplane/pkg/biz/auditor.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,20 @@ func NewAuditorUseCase(p *auditor.AuditLogPublisher, logger log.Logger) *Auditor
func (uc *AuditorUseCase) Dispatch(ctx context.Context, entry auditor.LogEntry, orgID *uuid.UUID) {
// dynamically load user information from the context
opts := []auditor.GeneratorOption{}
var gotActor bool
if user := entities.CurrentUser(ctx); user != nil {
parsedUUID, _ := uuid.Parse(user.ID)
opts = append(opts, auditor.WithActor(auditor.ActorTypeUser, parsedUUID, user.Email))
gotActor = true
} else if apiToken := entities.CurrentAPIToken(ctx); apiToken != nil {
parsedUUID, _ := uuid.Parse(apiToken.ID)
opts = append(opts, auditor.WithActor(auditor.ActorTypeAPIToken, parsedUUID, ""))
gotActor = true
}

if !gotActor && entry.RequiresActor() {
uc.log.Warn("failed to get actor information, required by the audit log entry")
return
}

if orgID != nil {
Expand Down
13 changes: 11 additions & 2 deletions app/controlplane/pkg/biz/membership.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"fmt"
"time"

"github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor/events"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
"github.com/go-kratos/kratos/v2/log"
"github.com/google/uuid"
Expand Down Expand Up @@ -51,10 +52,11 @@ type MembershipUseCase struct {
repo MembershipRepo
orgUseCase *OrganizationUseCase
logger *log.Helper
auditor *AuditorUseCase
}

func NewMembershipUseCase(repo MembershipRepo, orgUC *OrganizationUseCase, logger log.Logger) *MembershipUseCase {
return &MembershipUseCase{repo, orgUC, log.NewHelper(logger)}
func NewMembershipUseCase(repo MembershipRepo, orgUC *OrganizationUseCase, auditor *AuditorUseCase, logger log.Logger) *MembershipUseCase {
return &MembershipUseCase{repo, orgUC, log.NewHelper(logger), auditor}
}

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

uc.auditor.Dispatch(ctx, &events.OrgUserLeft{
OrgBase: &events.OrgBase{
OrgID: &m.OrganizationID,
OrgName: m.Org.Name,
},
}, &m.OrganizationID)

// Check number of members in the org
// If it's the only one, delete the org
membershipsInOrg, err := uc.repo.FindByOrg(ctx, m.OrganizationID)
Expand Down
14 changes: 13 additions & 1 deletion app/controlplane/pkg/biz/organization.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"io"
"time"

"github.com/chainloop-dev/chainloop/app/controlplane/pkg/auditor/events"
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/authz"
config "github.com/chainloop-dev/chainloop/app/controlplane/pkg/conf/controlplane/config/v1"
"github.com/chainloop-dev/chainloop/pkg/servicelogger"
Expand Down Expand Up @@ -49,9 +50,10 @@ type OrganizationUseCase struct {
integrationUC *IntegrationUseCase
membershipRepo MembershipRepo
onboardingConfig []*config.OnboardingSpec
auditor *AuditorUseCase
}

func NewOrganizationUseCase(repo OrganizationRepo, repoUC *CASBackendUseCase, iUC *IntegrationUseCase, mRepo MembershipRepo, onboardingConfig []*config.OnboardingSpec, l log.Logger) *OrganizationUseCase {
func NewOrganizationUseCase(repo OrganizationRepo, repoUC *CASBackendUseCase, auditor *AuditorUseCase, iUC *IntegrationUseCase, mRepo MembershipRepo, onboardingConfig []*config.OnboardingSpec, l log.Logger) *OrganizationUseCase {
if l == nil {
l = log.NewStdLogger(io.Discard)
}
Expand All @@ -62,6 +64,7 @@ func NewOrganizationUseCase(repo OrganizationRepo, repoUC *CASBackendUseCase, iU
integrationUC: iUC,
membershipRepo: mRepo,
onboardingConfig: onboardingConfig,
auditor: auditor,
}
}

Expand Down Expand Up @@ -147,6 +150,15 @@ func (uc *OrganizationUseCase) doCreate(ctx context.Context, name string, opts .
}
}

orgUUID, err := uuid.Parse(org.ID)
if err != nil {
return nil, NewErrInvalidUUID(err)
}

uc.auditor.Dispatch(ctx, &events.OrgCreated{
OrgBase: &events.OrgBase{OrgID: &orgUUID, OrgName: org.Name}}, &orgUUID,
)

return org, nil
}

Expand Down
6 changes: 4 additions & 2 deletions app/controlplane/pkg/biz/organization_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
repoM "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz/mocks"
"github.com/go-kratos/kratos/v2/log"
"github.com/google/uuid"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/suite"
)
Expand All @@ -33,14 +34,15 @@ type organizationTestSuite struct {

func (s *organizationTestSuite) TestCreateWithRandomName() {
repo := repoM.NewOrganizationRepo(s.T())
uc := biz.NewOrganizationUseCase(repo, nil, nil, nil, nil, log.NewStdLogger(io.Discard))
l := log.NewStdLogger(io.Discard)
uc := biz.NewOrganizationUseCase(repo, nil, biz.NewAuditorUseCase(nil, l), nil, nil, nil, l)

s.Run("the org exists, we retry", func() {
ctx := context.Background()
// the first one fails because it already exists
repo.On("Create", ctx, mock.Anything).Once().Return(nil, biz.NewErrAlreadyExistsStr("it already exists"))
// but the second call creates the org
repo.On("Create", ctx, mock.Anything).Once().Return(&biz.Organization{Name: "foobar"}, nil)
repo.On("Create", ctx, mock.Anything).Once().Return(&biz.Organization{Name: "foobar", ID: uuid.NewString()}, nil)
got, err := uc.CreateWithRandomName(ctx)
s.NoError(err)
s.Equal("foobar", got.Name)
Expand Down
Loading

0 comments on commit a160509

Please sign in to comment.