Skip to content
1 change: 1 addition & 0 deletions internal/authz/interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
RolePermissionsManager Role = "permissions_manager"
)


Check failure on line 35 in internal/authz/interface.go

View workflow job for this annotation

GitHub Actions / lint / Run golangci-lint

File is not properly formatted (gci)
Comment thread
ShantKhatri marked this conversation as resolved.
Outdated
// nolint:lll
var (
// AllRolesDescriptions is a list of all roles
Expand Down
18 changes: 1 addition & 17 deletions internal/controlplane/handlers_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/proto"

"github.com/mindersec/minder/internal/auth"
"github.com/mindersec/minder/internal/auth/jwt"
Expand Down Expand Up @@ -78,7 +77,7 @@ func ProjectAuthorizationInterceptor(ctx context.Context, req interface{}, info

relation := opts.GetRelation()

relationName := relationAsName(relation)
relationName := minder.RelationAsName(relation)
if relationName == "" {
return nil, status.Errorf(codes.Internal, "error getting name for requested relation %v", relation)
}
Expand All @@ -105,21 +104,6 @@ func ProjectAuthorizationInterceptor(ctx context.Context, req interface{}, info
return handler(ctx, req)
}

// relationAsName returns the OpenFGA relation name for the given relation enum.
// It returns an empty string in the case of error.
func relationAsName(relation minder.Relation) string {
relationValue := relation.Descriptor().Values().ByNumber(relation.Number())
if relationValue == nil {
return ""
}
extension := proto.GetExtension(relationValue.Options(), minder.E_Name)
relationName, ok := extension.(string)
if !ok {
return ""
}
return relationName
}

// populateEntityContext populates the project in the entity context, by looking at the proto context or
// fetching the default project
func populateEntityContext(
Expand Down
71 changes: 4 additions & 67 deletions internal/controlplane/handlers_projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,76 +170,13 @@ func (s *Server) CreateProject(
return nil, err
}

var project *db.Project
if parentProjectID != uuid.Nil {
// Verify permissions if we have a parent
relationName := relationAsName(minderv1.Relation_RELATION_CREATE)
if err := s.authzClient.Check(ctx, relationName, parentProjectID); err != nil {
return nil, util.UserVisibleError(
codes.PermissionDenied, "user %q is not authorized to perform this operation on project %q",
auth.IdentityFromContext(ctx).Human(), parentProjectID)
}

if !features.ProjectAllowsProjectHierarchyOperations(ctx, s.store, parentProjectID) {
return nil, util.UserVisibleError(codes.PermissionDenied,
"project does not allow project hierarchy operations")
}
tx, err := s.store.BeginTransaction()
if err != nil {
return nil, status.Errorf(codes.Internal, "error starting transaction: %v", err)
}
defer s.store.Rollback(tx)
qtx := s.store.GetQuerierWithTransaction(tx)

project, err = s.projectCreator.ProvisionChildProject(ctx, qtx, parentProjectID, req.Name)
if err != nil {
return nil, err
}

if err := s.store.Commit(tx); err != nil {
return nil, status.Errorf(codes.Internal, "error committing transaction: %v", err)
}

} else {
// This is a top-level project creation request.
// We need to check if the user has the right to create projects in the system.
if !flags.Bool(ctx, s.featureFlags, flags.ProjectCreateDelete) {
return nil, util.UserVisibleError(codes.Unimplemented, "cannot create a new top-level project")
}

id := auth.IdentityFromContext(ctx)
if id.String() == "" {
return nil, util.UserVisibleError(codes.Unauthenticated, "cannot determine user ID")
}

tx, err := s.store.BeginTransaction()
if err != nil {
return nil, status.Errorf(codes.Internal, "error starting transaction: %v", err)
}
defer s.store.Rollback(tx)
qtx := s.store.GetQuerierWithTransaction(tx)
project, err = s.projectCreator.ProvisionSelfEnrolledProject(ctx, qtx, req.Name, id.String())
if err != nil {
return nil, err
}

if err := s.store.Commit(tx); err != nil {
return nil, status.Errorf(codes.Internal, "error committing transaction: %v", err)
}
}

if project == nil {
return nil, status.Errorf(codes.Internal, "project is nil after creation")
project, err := s.projectCreator.ProvisionProject(ctx, req.Name, parentProjectID)
if err != nil {
return nil, err
}

return &minderv1.CreateProjectResponse{
Project: &minderv1.Project{
ProjectId: project.ID.String(),
Name: project.Name,
Description: "",
CreatedAt: timestamppb.New(project.CreatedAt),
UpdatedAt: timestamppb.New(project.UpdatedAt),
},
Project: project,
}, nil
}

Expand Down
3 changes: 3 additions & 0 deletions internal/controlplane/handlers_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import (
pb "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
serverconfig "github.com/mindersec/minder/pkg/config/server"
"github.com/mindersec/minder/pkg/eventer"
"github.com/mindersec/minder/pkg/flags"
)

const (
Expand Down Expand Up @@ -271,6 +272,8 @@ func TestCreateUser_gRPC(t *testing.T) {
marketplaces.NewNoopMarketplace(),
&serverconfig.DefaultProfilesConfig{},
&serverconfig.FeaturesConfig{},
mockStore,
&flags.FakeClient{},
),
}

Expand Down
123 changes: 104 additions & 19 deletions internal/projects/creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,17 @@ import (
"github.com/rs/zerolog/log"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"google.golang.org/protobuf/types/known/timestamppb"

"github.com/mindersec/minder/internal/auth"
"github.com/mindersec/minder/internal/authz"
"github.com/mindersec/minder/internal/db"
"github.com/mindersec/minder/internal/marketplaces"
"github.com/mindersec/minder/internal/projects/features"
"github.com/mindersec/minder/internal/util"
minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
"github.com/mindersec/minder/pkg/config/server"
"github.com/mindersec/minder/pkg/flags"
"github.com/mindersec/minder/pkg/mindpak"
)

Expand All @@ -28,6 +33,13 @@ import (
// 1. Move the delete operations into this interface
// 2. The interface is very GitHub-specific. It needs to be made more generic.
type ProjectCreator interface {
// ProvisionProject orchestrates the creation of a project, checking permissions,
// managing the database transaction, and calling the appropriate provisioning logic.
ProvisionProject(
ctx context.Context,
projectName string,
parentProjectID uuid.UUID,
) (*minderv1.Project, error)

// ProvisionSelfEnrolledProject creates the core default components of the project
// (project, marketplace subscriptions, etc.).
Expand All @@ -37,36 +49,32 @@ type ProjectCreator interface {
projectName string,
userSub string,
) (outproj *db.Project, projerr error)

// ProvisionChildProject creates the components of a child project which is nested
// under a parent in the project hierarchy. The parent project is checked for
// existence before creating the child project.
ProvisionChildProject(
ctx context.Context,
qtx db.ExtendQuerier,
parentProjectID uuid.UUID,
projectName string,
) (outproj *db.Project, projerr error)
}

type projectCreator struct {
authzClient authz.Client
marketplace marketplaces.Marketplace
profilesCfg *server.DefaultProfilesConfig
featuresCfg *server.FeaturesConfig
authzClient authz.Client
marketplace marketplaces.Marketplace
profilesCfg *server.DefaultProfilesConfig
featuresCfg *server.FeaturesConfig
store db.Store
featureFlags flags.Interface
}

// NewProjectCreator creates a new instance of the project creator
func NewProjectCreator(authzClient authz.Client,
marketplace marketplaces.Marketplace,
profilesCfg *server.DefaultProfilesConfig,
featuresCfg *server.FeaturesConfig,
store db.Store,
featureFlags flags.Interface,
) ProjectCreator {
return &projectCreator{
authzClient: authzClient,
marketplace: marketplace,
profilesCfg: profilesCfg,
featuresCfg: featuresCfg,
authzClient: authzClient,
marketplace: marketplace,
profilesCfg: profilesCfg,
featuresCfg: featuresCfg,
store: store,
featureFlags: featureFlags,
}
}

Expand Down Expand Up @@ -147,7 +155,7 @@ func (p *projectCreator) ProvisionSelfEnrolledProject(
return &project, nil
}

func (p *projectCreator) ProvisionChildProject(
func (p *projectCreator) provisionChildProject(
ctx context.Context,
qtx db.ExtendQuerier,
parentProjectID uuid.UUID,
Expand Down Expand Up @@ -204,3 +212,80 @@ func (p *projectCreator) ProvisionChildProject(

return &subProject, nil
}

func (p *projectCreator) ProvisionProject(
ctx context.Context,
projectName string,
parentProjectID uuid.UUID,
) (*minderv1.Project, error) {
var project *db.Project

if parentProjectID != uuid.Nil {
// Verify permissions if we have a parent
if err := p.authzClient.Check(ctx, "create", parentProjectID); err != nil {
Comment thread
ShantKhatri marked this conversation as resolved.
Outdated
Comment thread
ShantKhatri marked this conversation as resolved.
Outdated
return nil, util.UserVisibleError(
codes.PermissionDenied, "user %q is not authorized to perform this operation on project %q",
auth.IdentityFromContext(ctx).Human(), parentProjectID)
}

if !features.ProjectAllowsProjectHierarchyOperations(ctx, p.store, parentProjectID) {
return nil, util.UserVisibleError(codes.PermissionDenied,
"project does not allow project hierarchy operations")
}

tx, err := p.store.BeginTransaction()
if err != nil {
return nil, status.Errorf(codes.Internal, "error starting transaction: %v", err)
}
defer p.store.Rollback(tx)
qtx := p.store.GetQuerierWithTransaction(tx)

project, err = p.provisionChildProject(ctx, qtx, parentProjectID, projectName)
if err != nil {
return nil, err
}

if err := p.store.Commit(tx); err != nil {
return nil, status.Errorf(codes.Internal, "error committing transaction: %v", err)
}

} else {
// This is a top-level project creation request, so we need to check
// if the user has the right to create projects in the system.
if !flags.Bool(ctx, p.featureFlags, flags.ProjectCreateDelete) {
return nil, util.UserVisibleError(codes.Unimplemented, "cannot create a new top-level project")
}

id := auth.IdentityFromContext(ctx)
if id.String() == "" {
return nil, util.UserVisibleError(codes.Unauthenticated, "cannot determine user ID")
}

tx, err := p.store.BeginTransaction()
if err != nil {
return nil, status.Errorf(codes.Internal, "error starting transaction: %v", err)
}
defer p.store.Rollback(tx)
qtx := p.store.GetQuerierWithTransaction(tx)

project, err = p.ProvisionSelfEnrolledProject(ctx, qtx, projectName, id.String())
if err != nil {
return nil, err
}

if err := p.store.Commit(tx); err != nil {
return nil, status.Errorf(codes.Internal, "error committing transaction: %v", err)
}
}

if project == nil {
return nil, status.Errorf(codes.Internal, "project is nil after creation")
}

return &minderv1.Project{
ProjectId: project.ID.String(),
Name: project.Name,
CreatedAt: timestamppb.New(project.CreatedAt),
UpdatedAt: timestamppb.New(project.UpdatedAt),
}, nil
}
Loading
Loading