Skip to content

Commit 20b5b70

Browse files
JAORMXclaude
andcommitted
Implement RegisterEntity generic API endpoint
Implements the RegisterEntity gRPC endpoint to provide a unified, synchronous API for registering any entity type (repositories, releases, artifacts, pull requests) in Minder. This change extracts common entity creation logic into a reusable EntityCreator service that is used by both synchronous (RegisterEntity) and asynchronous (webhook-based) entity registration flows. Key changes: - Add RegisterEntity RPC handler with generic entity creation - Create EntityCreator service to unify entity creation logic - Implement pluggable validator framework (RepositoryValidator) - Refactor RepositoryService to use EntityCreator (reduced from ~90 to ~30 lines) - Refactor async entity handler to use EntityCreator - Update proto to use google.protobuf.Struct for type-safe properties - Add comprehensive test coverage (27 new tests) Security improvements: - Input validation for property count (max 100) and key length (max 200) - Context cancellation protection in cleanup operations - Improved error wrapping for better debugging The implementation maintains backward compatibility with existing RegisterRepository RPC while providing a foundation for registering other entity types through a single unified API. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent e4f6f47 commit 20b5b70

File tree

20 files changed

+2233
-385
lines changed

20 files changed

+2233
-385
lines changed

docs/docs/ref/proto.mdx

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

internal/controlplane/handlers_entity_instances.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ import (
1313
"google.golang.org/grpc/codes"
1414

1515
"github.com/mindersec/minder/internal/engine/engcontext"
16+
"github.com/mindersec/minder/internal/entities/models"
17+
"github.com/mindersec/minder/internal/entities/service/validators"
1618
"github.com/mindersec/minder/internal/logger"
1719
"github.com/mindersec/minder/internal/util"
1820
pb "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
21+
"github.com/mindersec/minder/pkg/entities/properties"
1922
)
2023

2124
// ListEntities returns a list of entity instances for a given project and provider
@@ -181,3 +184,99 @@ func (s *Server) DeleteEntityById(
181184
Id: in.GetId(),
182185
}, nil
183186
}
187+
188+
// RegisterEntity creates a new entity instance
189+
func (s *Server) RegisterEntity(
190+
ctx context.Context,
191+
in *pb.RegisterEntityRequest,
192+
) (*pb.RegisterEntityResponse, error) {
193+
// 1. Extract context information
194+
entityCtx := engcontext.EntityFromContext(ctx)
195+
projectID := entityCtx.Project.ID
196+
providerName := entityCtx.Provider.Name
197+
198+
logger.BusinessRecord(ctx).Provider = providerName
199+
logger.BusinessRecord(ctx).Project = projectID
200+
201+
// 2. Validate entity type
202+
if in.GetEntityType() == pb.Entity_ENTITY_UNSPECIFIED {
203+
return nil, util.UserVisibleError(codes.InvalidArgument,
204+
"entity_type must be specified")
205+
}
206+
207+
// 3. Parse identifying properties
208+
identifyingProps, err := parseIdentifyingProperties(in)
209+
if err != nil {
210+
return nil, util.UserVisibleError(codes.InvalidArgument,
211+
"invalid identifying_properties: %v", err)
212+
}
213+
214+
// 4. Get provider from database
215+
provider, err := s.providerStore.GetByName(ctx, projectID, providerName)
216+
if err != nil {
217+
if errors.Is(err, sql.ErrNoRows) {
218+
return nil, util.UserVisibleError(codes.NotFound, "provider not found")
219+
}
220+
return nil, util.UserVisibleError(codes.Internal, "cannot get provider: %v", err)
221+
}
222+
223+
// 5. Create entity using EntityCreator service
224+
ewp, err := s.entityCreator.CreateEntity(ctx, provider, projectID,
225+
in.GetEntityType(), identifyingProps, nil) // Use default options
226+
if err != nil {
227+
if errors.Is(err, validators.ErrPrivateRepoForbidden) ||
228+
errors.Is(err, validators.ErrArchivedRepoForbidden) {
229+
return nil, util.UserVisibleError(codes.InvalidArgument, "%s", err.Error())
230+
}
231+
return nil, util.UserVisibleError(codes.Internal,
232+
"unable to register entity: %v", err)
233+
}
234+
235+
// 6. Convert to EntityInstance protobuf
236+
entityInstance := entityInstanceToProto(ewp, providerName)
237+
238+
// 7. Return response
239+
return &pb.RegisterEntityResponse{
240+
Entity: entityInstance,
241+
}, nil
242+
}
243+
244+
// parseIdentifyingProperties converts proto properties to Properties object
245+
func parseIdentifyingProperties(req *pb.RegisterEntityRequest) (*properties.Properties, error) {
246+
if req.GetIdentifyingProperties() == nil {
247+
return nil, errors.New("identifying_properties is required")
248+
}
249+
250+
propsMap := req.GetIdentifyingProperties().AsMap()
251+
252+
// Validate reasonable property count to prevent resource exhaustion
253+
const maxPropertyCount = 100
254+
if len(propsMap) > maxPropertyCount {
255+
return nil, fmt.Errorf("too many identifying properties: got %d, max %d",
256+
len(propsMap), maxPropertyCount)
257+
}
258+
259+
// Validate property keys are reasonable (alphanumeric, slash, underscore, hyphen)
260+
for key := range propsMap {
261+
if len(key) > 200 {
262+
return nil, fmt.Errorf("property key too long: %d characters", len(key))
263+
}
264+
// Note: Additional key sanitization could be added here if needed
265+
}
266+
267+
return properties.NewProperties(propsMap), nil
268+
}
269+
270+
// entityInstanceToProto converts EntityWithProperties to EntityInstance protobuf
271+
func entityInstanceToProto(ewp *models.EntityWithProperties, providerName string) *pb.EntityInstance {
272+
return &pb.EntityInstance{
273+
Id: ewp.Entity.ID.String(),
274+
Context: &pb.ContextV2{
275+
ProjectId: ewp.Entity.ProjectID.String(),
276+
Provider: providerName,
277+
},
278+
Type: ewp.Entity.Type,
279+
Name: ewp.Entity.Name,
280+
// Properties are intentionally omitted - use GetEntityById to fetch them
281+
}
282+
}

0 commit comments

Comments
 (0)