Skip to content

Commit

Permalink
chore: improvements on org management (#546)
Browse files Browse the repository at this point in the history
Signed-off-by: Miguel Martinez Trivino <[email protected]>
  • Loading branch information
migmartri authored Feb 29, 2024
1 parent b610907 commit 30970ef
Show file tree
Hide file tree
Showing 31 changed files with 654 additions and 72 deletions.
7 changes: 1 addition & 6 deletions app/cli/cmd/organization_leave.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// 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.
Expand All @@ -17,7 +17,6 @@ package cmd

import (
"context"
"errors"
"fmt"

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

if membership.Current {
return errors.New("can't leave the `current` organization. To leave this org, please switch to another one")
}

fmt.Printf("You are about to leave the organization %q\n", membership.Org.Name)

// Ask for confirmation
Expand Down
2 changes: 1 addition & 1 deletion app/controlplane/cmd/wire_gen.go

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

37 changes: 35 additions & 2 deletions app/controlplane/internal/biz/membership.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// 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.
Expand Down Expand Up @@ -107,7 +107,17 @@ func (uc *MembershipUseCase) Create(ctx context.Context, orgID, userID string, c
return nil, NewErrInvalidUUID(err)
}

return uc.repo.Create(ctx, orgUUID, userUUID, current)
m, err := uc.repo.Create(ctx, orgUUID, userUUID, current)
if err != nil {
return nil, fmt.Errorf("failed to create membership: %w", err)
}

if !current {
return m, nil
}

// Set the current membership again to make sure we uncheck the previous ones
return uc.repo.SetCurrent(ctx, m.ID)
}

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

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

return uc.repo.SetCurrent(ctx, mUUID)
}

func (uc *MembershipUseCase) FindByOrgAndUser(ctx context.Context, orgID, userID string) (*Membership, error) {
orgUUID, err := uuid.Parse(orgID)
if err != nil {
return nil, NewErrInvalidUUID(err)
}

userUUID, err := uuid.Parse(userID)
if err != nil {
return nil, NewErrInvalidUUID(err)
}

m, err := uc.repo.FindByOrgAndUser(ctx, orgUUID, userUUID)
if err != nil {
return nil, fmt.Errorf("failed to find membership: %w", err)
} else if m == nil {
return nil, NewErrNotFound("membership")
}

return m, nil
}
35 changes: 24 additions & 11 deletions app/controlplane/internal/biz/membership_integration_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// 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.
Expand Down Expand Up @@ -99,22 +99,16 @@ func (s *membershipIntegrationTestSuite) TestCreateMembership() {
user, err := s.User.FindOrCreateByEmail(ctx, "[email protected]")
assert.NoError(err)

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

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

wantUserID, err := uuid.Parse(user.ID)
assert.NoError(err)
assert.Equal(wantUserID, m.UserID, "User ID")

wantORGID, err := uuid.Parse(org.ID)
assert.NoError(err)
assert.Equal(wantORGID, m.OrganizationID, "Organization ID")

assert.Equal(user.ID, m.UserID.String(), "User ID")
assert.Equal(org.ID, m.OrganizationID.String(), "Organization ID")
assert.EqualValues(org, m.Org, "Embedded organization")
})

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

m, err := s.Membership.Create(ctx, org.ID, user.ID, false)
assert.NoError(err)
assert.Equal(false, m.Current, "Membership should be current")
assert.Equal(false, m.Current, "Membership should not be current")
})

s.T().Run("current override", func(t *testing.T) {
org, err := s.Organization.CreateWithRandomName(ctx)
assert.NoError(err)
org2, err := s.Organization.CreateWithRandomName(ctx)
assert.NoError(err)

m, err := s.Membership.Create(ctx, org.ID, user.ID, true)
assert.NoError(err)
s.True(m.Current)
// Creating a new one will override the current status of the previous one
m, err = s.Membership.Create(ctx, org2.ID, user.ID, true)
assert.NoError(err)
s.True(m.Current)

m, err = s.Membership.FindByOrgAndUser(ctx, org.ID, user.ID)
assert.NoError(err)
s.False(m.Current)
})

s.T().Run("Invalid ORG", func(t *testing.T) {
Expand Down
37 changes: 31 additions & 6 deletions app/controlplane/internal/biz/organization.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// 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.
Expand Down Expand Up @@ -57,7 +57,20 @@ func NewOrganizationUseCase(repo OrganizationRepo, repoUC *CASBackendUseCase, iU

const OrganizationRandomNameMaxTries = 10

func (uc *OrganizationUseCase) CreateWithRandomName(ctx context.Context) (*Organization, error) {
type createOptions struct {
createInlineBackend bool
}

type CreateOpt func(*createOptions)

// Optionally create an inline CAS-backend
func WithCreateInlineBackend() CreateOpt {
return func(o *createOptions) {
o.createInlineBackend = true
}
}

func (uc *OrganizationUseCase) CreateWithRandomName(ctx context.Context, opts ...CreateOpt) (*Organization, error) {
// Try 10 times to create a random name
for i := 0; i < OrganizationRandomNameMaxTries; i++ {
// Create a random name
Expand All @@ -66,7 +79,7 @@ func (uc *OrganizationUseCase) CreateWithRandomName(ctx context.Context) (*Organ
return nil, fmt.Errorf("failed to generate random name: %w", err)
}

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

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

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

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

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

options := &createOptions{}
for _, o := range opts {
o(options)
}

org, err := uc.orgRepo.Create(ctx, name)
if err != nil {
return nil, fmt.Errorf("failed to create organization: %w", err)
}

if options.createInlineBackend {
// Create inline CAS-backend
if _, err := uc.casBackendUseCase.CreateInlineFallbackBackend(ctx, org.ID); err != nil {
return nil, fmt.Errorf("failed to create fallback backend: %w", err)
}
}

return org, nil
}

Expand Down
24 changes: 23 additions & 1 deletion app/controlplane/internal/biz/organization_integration_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// 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.
Expand Down Expand Up @@ -84,6 +84,28 @@ func (s *OrgIntegrationTestSuite) TestCreate() {
}
}

func (s *OrgIntegrationTestSuite) TestCreateAddsInlineCASBackend() {
ctx := context.Background()
s.Run("by default it does not create it", func() {
org, err := s.Organization.CreateWithRandomName(ctx)
s.NoError(err)
// Creating an org also creates a new inline backend
b, err := s.CASBackend.FindDefaultBackend(ctx, org.ID)
s.Error(err)
s.Nil(b)
})

s.Run("with the option it creates it", func() {
org, err := s.Organization.Create(ctx, "with-inline", biz.WithCreateInlineBackend())
s.NoError(err)

// Creating an org also creates a new inline backend
b, err := s.CASBackend.FindDefaultBackend(ctx, org.ID)
s.NoError(err)
s.True(b.Inline)
})
}

func (s *OrgIntegrationTestSuite) TestUpdate() {
ctx := context.Background()

Expand Down
21 changes: 16 additions & 5 deletions app/controlplane/internal/biz/user.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// 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.
Expand Down Expand Up @@ -110,6 +110,7 @@ func (uc *UserUseCase) FindByID(ctx context.Context, userID string) (*User, erro
}

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

// By default we set the first one
currentOrg := memberships[0].OrganizationID
var currentOrgID uuid.UUID
for _, m := range memberships {
// Override if it's being explicitly selected
if m.Current {
currentOrg = m.OrganizationID
currentOrgID = m.OrganizationID
break
}
}

return uc.organizationUseCase.FindByID(ctx, currentOrg.String())
if currentOrgID == uuid.Nil {
// If none is selected, we configure the first one
_, err := uc.membershipUseCase.SetCurrent(ctx, userID, memberships[0].ID.String())
if err != nil {
return nil, fmt.Errorf("error setting current org: %w", err)
}

// Call itself recursively now that we have a current org
return uc.CurrentOrg(ctx, userID)
}

return uc.organizationUseCase.FindByID(ctx, currentOrgID.String())
}
55 changes: 54 additions & 1 deletion app/controlplane/internal/biz/user_integration_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2023 The Chainloop Authors.
// 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.
Expand Down Expand Up @@ -58,6 +58,59 @@ func (s *userIntegrationTestSuite) TestDeleteUser() {
s.Empty(gotMembership)
}

func (s *userIntegrationTestSuite) TestCurrentOrg() {
ctx := context.Background()
s.Run("if there is an associated, default org it's returned", func() {
// userOne has a default org
m, err := s.Membership.FindByOrgAndUser(ctx, s.sharedOrg.ID, s.userOne.ID)
s.NoError(err)
s.True(m.Current)

// and it's returned as currentOrg
got, err := s.User.CurrentOrg(ctx, s.userOne.ID)
s.NoError(err)
s.Equal(s.sharedOrg, got)
})

s.Run("they have more orgs but none of them is the default, it will return the first one as default", func() {
m, err := s.Membership.FindByOrgAndUser(ctx, s.sharedOrg.ID, s.userOne.ID)
s.NoError(err)
s.True(m.Current)
// leave the current org
err = s.Membership.DeleteWithOrg(ctx, s.userOne.ID, m.ID.String())
s.NoError(err)

// none of the orgs is marked as current
mems, _ := s.Membership.ByUser(ctx, s.userOne.ID)
s.Len(mems, 1)
s.False(mems[0].Current)

// asking for the current org will return the first one
got, err := s.User.CurrentOrg(ctx, s.userOne.ID)
s.NoError(err)
s.Equal(s.userOneOrg, got)

// and now the membership will be set as current
mems, _ = s.Membership.ByUser(ctx, s.userOne.ID)
s.Len(mems, 1)
s.True(mems[0].Current)
})

s.Run("it will fail if there are no membershipts", func() {
// none of the orgs is marked as current
mems, _ := s.Membership.ByUser(ctx, s.userOne.ID)
s.Len(mems, 1)
// leave the current org
err := s.Membership.DeleteWithOrg(ctx, s.userOne.ID, mems[0].ID.String())
s.NoError(err)
mems, _ = s.Membership.ByUser(ctx, s.userOne.ID)
s.Len(mems, 0)

_, err = s.User.CurrentOrg(ctx, s.userOne.ID)
s.ErrorContains(err, "user does not have any organization associated")
})
}

// Run the tests
func TestUserUseCase(t *testing.T) {
suite.Run(t, new(userIntegrationTestSuite))
Expand Down
2 changes: 1 addition & 1 deletion app/controlplane/internal/data/ent/apitoken/apitoken.go

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

Loading

0 comments on commit 30970ef

Please sign in to comment.