• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mindersec / minder / 12613632569

04 Jan 2025 08:51PM UTC coverage: 55.318% (+0.2%) from 55.145%
12613632569

Pull #5100

github

web-flow
Merge 690366ff2 into a6d9b2295
Pull Request #5100: Add support for Get Profile Status By ID in Cli and Api

117 of 188 new or added lines in 1 file covered. (62.23%)

3 existing lines in 2 files now uncovered.

17097 of 30907 relevant lines covered (55.32%)

37.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

27.32
/internal/controlplane/handlers_profile.go
1
// SPDX-FileCopyrightText: Copyright 2023 The Minder Authors
2
// SPDX-License-Identifier: Apache-2.0
3

4
package controlplane
5

6
import (
7
        "context"
8
        "database/sql"
9
        "errors"
10
        "fmt"
11
        "sort"
12
        "strings"
13

14
        "github.com/google/uuid"
15
        "github.com/rs/zerolog"
16
        "google.golang.org/grpc/codes"
17
        "google.golang.org/grpc/status"
18
        "google.golang.org/protobuf/types/known/timestamppb"
19

20
        "github.com/mindersec/minder/internal/db"
21
        "github.com/mindersec/minder/internal/engine/engcontext"
22
        "github.com/mindersec/minder/internal/engine/entities"
23
        entmodels "github.com/mindersec/minder/internal/entities/models"
24
        "github.com/mindersec/minder/internal/entities/properties"
25
        propSvc "github.com/mindersec/minder/internal/entities/properties/service"
26
        "github.com/mindersec/minder/internal/logger"
27
        ghprop "github.com/mindersec/minder/internal/providers/github/properties"
28
        "github.com/mindersec/minder/internal/util"
29
        minderv1 "github.com/mindersec/minder/pkg/api/protobuf/go/minder/v1"
30
        prof "github.com/mindersec/minder/pkg/profiles"
31
        "github.com/mindersec/minder/pkg/ruletypes"
32
)
33

34
type contextKey string
35

36
const requestKey contextKey = "request"
37

38
// CreateProfile creates a profile for a project
39
func (s *Server) CreateProfile(ctx context.Context,
40
        cpr *minderv1.CreateProfileRequest) (*minderv1.CreateProfileResponse, error) {
24✔
41
        in := cpr.GetProfile()
24✔
42
        if err := in.Validate(); err != nil {
26✔
43
                if errors.Is(err, minderv1.ErrValidationFailed) {
4✔
44
                        return nil, util.UserVisibleError(codes.InvalidArgument, "Couldn't create profile: %s", err)
2✔
45
                }
2✔
46
                return nil, status.Errorf(codes.InvalidArgument, "invalid profile: %v", err)
×
47
        }
48

49
        entityCtx := engcontext.EntityFromContext(ctx)
22✔
50

22✔
51
        // validate that project is valid and exist in the db
22✔
52
        err := entityCtx.ValidateProject(ctx, s.store)
22✔
53
        if err != nil {
22✔
54
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
55
        }
×
56

57
        newProfile, err := db.WithTransaction(s.store, func(qtx db.ExtendQuerier) (*minderv1.Profile, error) {
44✔
58
                return s.profiles.CreateProfile(ctx, entityCtx.Project.ID, uuid.Nil, in, qtx)
22✔
59
        })
22✔
60
        if err != nil {
26✔
61
                // assumption: service layer is setting meaningful errors
4✔
62
                return nil, err
4✔
63
        }
4✔
64

65
        resp := &minderv1.CreateProfileResponse{
18✔
66
                Profile: newProfile,
18✔
67
        }
18✔
68

18✔
69
        return resp, nil
18✔
70
}
71

72
// DeleteProfile is a method to delete a profile
73
func (s *Server) DeleteProfile(ctx context.Context,
74
        in *minderv1.DeleteProfileRequest) (*minderv1.DeleteProfileResponse, error) {
×
75
        entityCtx := engcontext.EntityFromContext(ctx)
×
76

×
77
        err := entityCtx.ValidateProject(ctx, s.store)
×
78
        if err != nil {
×
79
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
80
        }
×
81

82
        parsedProfileID, err := uuid.Parse(in.Id)
×
83
        if err != nil {
×
84
                return nil, util.UserVisibleError(codes.InvalidArgument, "invalid profile ID")
×
85
        }
×
86

87
        profile, err := s.store.GetProfileByID(ctx, db.GetProfileByIDParams{
×
88
                ProjectID: entityCtx.Project.ID,
×
89
                ID:        parsedProfileID,
×
90
        })
×
91
        if err != nil {
×
92
                if errors.Is(err, sql.ErrNoRows) {
×
93
                        return nil, status.Error(codes.NotFound, "profile not found")
×
94
                }
×
95
                return nil, status.Errorf(codes.Internal, "failed to get profile: %s", err)
×
96
        }
97

98
        // TEMPORARY HACK: Since we do not need to support the deletion of bundle
99
        // profile yet, reject deletion requests in the API
100
        // TODO: Move this deletion logic to ProfileService
101
        if profile.SubscriptionID.Valid {
×
102
                return nil, status.Errorf(codes.InvalidArgument, "cannot delete profile from bundle")
×
103
        }
×
104

105
        err = s.store.DeleteProfile(ctx, db.DeleteProfileParams{
×
106
                ID:        profile.ID,
×
107
                ProjectID: entityCtx.Project.ID,
×
108
        })
×
109
        if err != nil {
×
110
                return nil, status.Errorf(codes.Internal, "failed to delete profile: %s", err)
×
111
        }
×
112

113
        // Telemetry logging
114
        logger.BusinessRecord(ctx).Project = profile.ProjectID
×
115
        logger.BusinessRecord(ctx).Profile = logger.Profile{Name: profile.Name, ID: profile.ID}
×
116

×
117
        return &minderv1.DeleteProfileResponse{}, nil
×
118
}
119

120
// ListProfiles is a method to get all profiles for a project
121
func (s *Server) ListProfiles(ctx context.Context,
122
        req *minderv1.ListProfilesRequest) (*minderv1.ListProfilesResponse, error) {
×
123
        entityCtx := engcontext.EntityFromContext(ctx)
×
124

×
125
        err := entityCtx.ValidateProject(ctx, s.store)
×
126
        if err != nil {
×
127
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
128
        }
×
129

130
        listParams := db.ListProfilesByProjectIDAndLabelParams{
×
131
                ProjectID: entityCtx.Project.ID,
×
132
        }
×
133
        listParams.LabelsFromFilter(req.GetLabelFilter())
×
134

×
135
        zerolog.Ctx(ctx).Debug().Interface("listParams", listParams).Msg("profile list parameters")
×
136

×
137
        profiles, err := s.store.ListProfilesByProjectIDAndLabel(ctx, listParams)
×
138
        if err != nil {
×
139
                return nil, status.Errorf(codes.Unknown, "failed to get profiles: %s", err)
×
140
        }
×
141

142
        var resp minderv1.ListProfilesResponse
×
143
        resp.Profiles = make([]*minderv1.Profile, 0, len(profiles))
×
144
        profileMap := prof.MergeDatabaseListIntoProfiles(profiles)
×
145

×
146
        // Sort the profiles by name to get a consistent order. This is important for UI.
×
147
        profileNames := make([]string, 0, len(profileMap))
×
148
        for prfName := range profileMap {
×
149
                profileNames = append(profileNames, prfName)
×
150
        }
×
151
        sort.Strings(profileNames)
×
152

×
153
        for _, prfName := range profileNames {
×
154
                profile := profileMap[prfName]
×
155
                resp.Profiles = append(resp.Profiles, profile)
×
156
        }
×
157

158
        // Telemetry logging
159
        logger.BusinessRecord(ctx).Project = entityCtx.Project.ID
×
160

×
161
        return &resp, nil
×
162
}
163

164
// GetProfileById is a method to get a profile by id
165
func (s *Server) GetProfileById(ctx context.Context,
166
        in *minderv1.GetProfileByIdRequest) (*minderv1.GetProfileByIdResponse, error) {
×
167

×
168
        entityCtx := engcontext.EntityFromContext(ctx)
×
169

×
170
        err := entityCtx.ValidateProject(ctx, s.store)
×
171
        if err != nil {
×
172
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
173
        }
×
174

175
        parsedProfileID, err := uuid.Parse(in.Id)
×
176
        if err != nil {
×
177
                return nil, util.UserVisibleError(codes.InvalidArgument, "invalid profile ID")
×
178
        }
×
179

180
        profile, err := getProfilePBFromDB(ctx, parsedProfileID, entityCtx, s.store)
×
181
        if err != nil {
×
182
                if errors.Is(err, sql.ErrNoRows) || strings.Contains(err.Error(), "not found") {
×
183
                        return nil, util.UserVisibleError(codes.NotFound, "profile not found")
×
184
                }
×
185

186
                return nil, status.Errorf(codes.Internal, "failed to get profile: %s", err)
×
187
        }
188

189
        // Telemetry logging
190
        logger.BusinessRecord(ctx).Project = entityCtx.Project.ID
×
191
        logger.BusinessRecord(ctx).Profile = logger.Profile{Name: profile.Name, ID: parsedProfileID}
×
192

×
193
        return &minderv1.GetProfileByIdResponse{
×
194
                Profile: profile,
×
195
        }, nil
×
196
}
197

198
// GetProfileByName implements the RPC method for getting a profile by name
199
func (s *Server) GetProfileByName(ctx context.Context,
200
        in *minderv1.GetProfileByNameRequest) (*minderv1.GetProfileByNameResponse, error) {
×
201
        entityCtx := engcontext.EntityFromContext(ctx)
×
202

×
203
        err := entityCtx.ValidateProject(ctx, s.store)
×
204
        if err != nil {
×
205
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
206
        }
×
207

208
        if in.Name == "" {
×
209
                return nil, util.UserVisibleError(codes.InvalidArgument, "profile name must be specified")
×
210
        }
×
211

212
        profiles, err := s.store.GetProfileByProjectAndName(ctx, db.GetProfileByProjectAndNameParams{
×
213
                ProjectID: entityCtx.Project.ID,
×
214
                Name:      in.Name,
×
215
        })
×
216
        if err != nil {
×
217
                if errors.Is(err, sql.ErrNoRows) {
×
218
                        return nil, util.UserVisibleError(codes.NotFound, "profile %q not found", in.Name)
×
219
                }
×
220
                return nil, err
×
221
        }
222

223
        pols := prof.MergeDatabaseGetByNameIntoProfiles(profiles)
×
224

×
225
        // Telemetry logging
×
226
        logger.BusinessRecord(ctx).Project = entityCtx.Project.ID
×
227

×
228
        if len(pols) == 0 {
×
229
                return nil, util.UserVisibleError(codes.NotFound, "profile %q not found", in.Name)
×
230
        } else if len(pols) > 1 {
×
231
                return nil, fmt.Errorf("expected only one profile, got %d", len(pols))
×
232
        }
×
233

234
        // This should be only one profile
235
        for _, profile := range pols {
×
236
                return &minderv1.GetProfileByNameResponse{
×
237
                        Profile: profile,
×
238
                }, nil
×
239
        }
×
240

241
        return nil, util.UserVisibleError(codes.NotFound, "profile %q not found", in.Name)
×
242
}
243

244
func getProfilePBFromDB(
245
        ctx context.Context,
246
        id uuid.UUID,
247
        entityCtx engcontext.EntityContext,
248
        querier db.ExtendQuerier,
249
) (*minderv1.Profile, error) {
×
250
        profiles, err := querier.GetProfileByProjectAndID(ctx, db.GetProfileByProjectAndIDParams{
×
251
                ProjectID: entityCtx.Project.ID,
×
252
                ID:        id,
×
253
        })
×
254
        if err != nil {
×
255
                return nil, err
×
256
        }
×
257

258
        pols := prof.MergeDatabaseGetIntoProfiles(profiles)
×
259
        if len(pols) == 0 {
×
260
                return nil, fmt.Errorf("profile not found")
×
261
        } else if len(pols) > 1 {
×
262
                return nil, fmt.Errorf("expected only one profile, got %d", len(pols))
×
263
        }
×
264

265
        // This should be only one profile
266
        for _, profile := range pols {
×
267
                return profile, nil
×
268
        }
×
269

270
        return nil, fmt.Errorf("profile not found")
×
271
}
272

273
// TODO: We need to replace this with a more generic method that can be used for all entities
274
// probably coming from the properties.
275
func getRuleEvalEntityInfo(
276
        rs db.ListRuleEvaluationsByProfileIdRow,
277
        efp *entmodels.EntityWithProperties,
278
) map[string]string {
×
279
        entityInfo := map[string]string{}
×
280

×
281
        if name := efp.Properties.GetProperty(properties.PropertyName); name != nil {
×
282
                entityInfo["name"] = name.GetString()
×
283
        }
×
284

285
        if owner := efp.Properties.GetProperty(ghprop.RepoPropertyOwner); owner != nil {
×
286
                entityInfo["repo_owner"] = owner.GetString()
×
287
        }
×
288
        if name := efp.Properties.GetProperty(ghprop.RepoPropertyName); name != nil {
×
289
                entityInfo["repo_name"] = name.GetString()
×
290
        }
×
291

292
        if artName := efp.Properties.GetProperty(ghprop.ArtifactPropertyName); artName != nil {
×
293
                entityInfo["artifact_name"] = artName.GetString()
×
294
        }
×
295

296
        if artType := efp.Properties.GetProperty(ghprop.ArtifactPropertyType); artType != nil {
×
297
                entityInfo["artifact_type"] = artType.GetString()
×
298
        }
×
299

300
        entityInfo["provider"] = rs.Provider
×
301
        entityInfo["entity_type"] = efp.Entity.Type.ToString()
×
302
        entityInfo["entity_id"] = rs.EntityID.String()
×
303

×
304
        // temporary: These will be replaced by entity_id
×
305
        if rs.EntityType == db.EntitiesRepository {
×
306
                entityInfo["repository_id"] = efp.Entity.ID.String()
×
307
        } else if rs.EntityType == db.EntitiesArtifact {
×
308
                entityInfo["artifact_id"] = efp.Entity.ID.String()
×
309
        }
×
310

311
        return entityInfo
×
312
}
313

314
func (s *Server) getRuleEvaluationStatuses(
315
        ctx context.Context,
316
        dbRuleEvaluationStatuses []db.ListRuleEvaluationsByProfileIdRow,
317
        profileId string,
318
) []*minderv1.RuleEvaluationStatus {
×
319
        ruleEvaluationStatuses := make(
×
320
                []*minderv1.RuleEvaluationStatus, 0, len(dbRuleEvaluationStatuses),
×
321
        )
×
322
        // Loop through the rule evaluation statuses and convert them to protobuf
×
323
        for _, dbRuleEvalStat := range dbRuleEvaluationStatuses {
×
324
                // Get the rule evaluation status
×
325
                st, err := s.getRuleEvalStatus(ctx, profileId, dbRuleEvalStat)
×
326
                if err != nil {
×
327
                        if errors.Is(err, sql.ErrNoRows) || errors.Is(err, propSvc.ErrEntityNotFound) {
×
328
                                zerolog.Ctx(ctx).Error().
×
329
                                        Str("profile_id", profileId).
×
330
                                        Str("rule_id", dbRuleEvalStat.RuleTypeID.String()).
×
331
                                        Str("entity_id", dbRuleEvalStat.EntityID.String()).
×
332
                                        Err(err).Msg("entity not found. error getting rule evaluation status")
×
333
                                continue
×
334
                        }
335

336
                        zerolog.Ctx(ctx).Error().
×
337
                                Str("profile_id", profileId).
×
338
                                Str("rule_id", dbRuleEvalStat.RuleTypeID.String()).
×
339
                                Str("entity_id", dbRuleEvalStat.EntityID.String()).
×
340
                                Err(err).Msg("error getting rule evaluation status")
×
341
                        continue
×
342
                }
343
                // Append the rule evaluation status to the list
344
                ruleEvaluationStatuses = append(ruleEvaluationStatuses, st)
×
345
        }
346
        return ruleEvaluationStatuses
×
347
}
348

349
// getRuleEvalStatus is a helper function to get rule evaluation status from a db row
350
//
351
//nolint:gocyclo
352
func (s *Server) getRuleEvalStatus(
353
        ctx context.Context,
354
        profileID string,
355
        dbRuleEvalStat db.ListRuleEvaluationsByProfileIdRow,
356
) (*minderv1.RuleEvaluationStatus, error) {
×
357
        l := zerolog.Ctx(ctx)
×
358
        var guidance string
×
359
        var err error
×
360

×
361
        // the caller just ignores allt the errors anyway, so we don't start a transaction as the integrity issues
×
362
        // would not be discovered anyway
×
363
        efp, err := s.props.EntityWithPropertiesByID(ctx, dbRuleEvalStat.EntityID, nil)
×
364
        if err != nil {
×
365
                return nil, fmt.Errorf("error fetching entity for properties: %w", err)
×
366
        }
×
367

368
        err = s.props.RetrieveAllPropertiesForEntity(ctx, efp, s.providerManager,
×
369
                propSvc.ReadBuilder().WithStoreOrTransaction(s.store).TolerateStaleData())
×
370
        if err != nil {
×
371
                return nil, fmt.Errorf("error fetching properties for entity: %w", err)
×
372
        }
×
373

374
        if dbRuleEvalStat.EvalStatus == db.EvalStatusTypesFailure ||
×
375
                dbRuleEvalStat.EvalStatus == db.EvalStatusTypesError {
×
376
                ruleTypeInfo, err := s.store.GetRuleTypeByID(ctx, dbRuleEvalStat.RuleTypeID)
×
377
                if err != nil {
×
378
                        l.Err(err).Msg("error getting rule type info from db")
×
379
                } else {
×
380
                        guidance = ruleTypeInfo.Guidance
×
381
                }
×
382
        }
383
        remediationURL := ""
×
384
        if dbRuleEvalStat.EntityType == db.EntitiesRepository {
×
385
                remediationURL, err = getRemediationURLFromMetadata(
×
386
                        dbRuleEvalStat.RemMetadata,
×
387
                        efp.Entity.Name,
×
388
                )
×
389
                if err != nil {
×
390
                        // A failure parsing the alert metadata points to a corrupt record. Log but don't err.
×
391
                        zerolog.Ctx(ctx).Error().Err(err).Msg("error parsing remediation pull request data")
×
392
                }
×
393
        }
394

395
        releasePhase, err := ruletypes.GetPBReleasePhaseFromDBReleaseStatus(&dbRuleEvalStat.RuleTypeReleasePhase)
×
396
        if err != nil {
×
397
                l.Err(err).Msg("error getting release phase")
×
398
        }
×
399

400
        st := &minderv1.RuleEvaluationStatus{
×
401
                ProfileId:           profileID,
×
402
                RuleId:              dbRuleEvalStat.RuleTypeID.String(),
×
403
                RuleName:            dbRuleEvalStat.RuleTypeName,
×
404
                RuleTypeName:        dbRuleEvalStat.RuleTypeName,
×
405
                RuleDescriptionName: dbRuleEvalStat.RuleName,
×
406
                RuleDisplayName:     dbRuleEvalStat.RuleTypeDisplayName,
×
407
                Entity:              string(dbRuleEvalStat.EntityType),
×
408
                Status:              string(dbRuleEvalStat.EvalStatus),
×
409
                Details:             dbRuleEvalStat.EvalDetails,
×
410
                EntityInfo:          getRuleEvalEntityInfo(dbRuleEvalStat, efp),
×
411
                Guidance:            guidance,
×
412
                LastUpdated:         timestamppb.New(dbRuleEvalStat.EvalLastUpdated),
×
413
                RemediationStatus:   string(dbRuleEvalStat.RemStatus),
×
414
                RemediationDetails:  dbRuleEvalStat.RemDetails,
×
415
                RemediationUrl:      remediationURL,
×
416
                Alert: &minderv1.EvalResultAlert{
×
417
                        Status:      string(dbRuleEvalStat.AlertStatus),
×
418
                        Details:     dbRuleEvalStat.AlertDetails,
×
419
                        LastUpdated: timestamppb.New(dbRuleEvalStat.AlertLastUpdated),
×
420
                },
×
421
                RemediationLastUpdated: timestamppb.New(dbRuleEvalStat.RemLastUpdated),
×
422
                ReleasePhase:           releasePhase,
×
423
        }
×
424

×
425
        // If the alert is on and its metadata is valid, parse it and set the URL
×
426
        if st.Alert.Status == string(db.AlertStatusTypesOn) {
×
427
                // Due to the fact that this code was written around the old history tables
×
428
                // There was an assumption that repository information was always included
×
429
                // details about the repository. For other types of entity, we now need to
×
430
                // explicitly pull information about the repository.
×
431
                // TODO: Change all this logic to store the alert URL in the alert metadata
×
432
                // This logic should not be in the presentation layer of Minder.
×
433
                var repoPath string
×
434
                if dbRuleEvalStat.EntityType == db.EntitiesRepository {
×
435
                        repoPath = fmt.Sprintf("%s/%s", st.EntityInfo["repo_owner"], st.EntityInfo["repo_name"])
×
436
                } else if dbRuleEvalStat.EntityType == db.EntitiesArtifact {
×
437
                        artRepoOwner := efp.Properties.GetProperty(ghprop.ArtifactPropertyRepoOwner).GetString()
×
438
                        artRepoName := efp.Properties.GetProperty(ghprop.ArtifactPropertyRepoName).GetString()
×
439
                        if artRepoOwner != "" && artRepoName != "" {
×
440
                                repoPath = fmt.Sprintf("%s/%s", artRepoOwner, artRepoName)
×
441
                        }
×
442
                } else if dbRuleEvalStat.EntityType == db.EntitiesPullRequest {
×
443
                        prRepoOwner := efp.Properties.GetProperty(ghprop.PullPropertyRepoOwner).GetString()
×
444
                        prRepoName := efp.Properties.GetProperty(ghprop.PullPropertyRepoName).GetString()
×
445
                        if prRepoOwner != "" && prRepoName != "" {
×
446
                                repoPath = fmt.Sprintf("%s/%s", prRepoOwner, prRepoName)
×
447
                        }
×
448
                }
449
                alertURL, err := getAlertURLFromMetadata(
×
450
                        dbRuleEvalStat.AlertMetadata,
×
451
                        repoPath,
×
452
                )
×
453
                if err != nil {
×
454
                        l.Err(err).Msg("error getting alert URL from metadata")
×
455
                } else {
×
456
                        st.Alert.Url = alertURL
×
457
                }
×
458
        }
459
        return st, nil
×
460
}
461

462
// GetProfileStatusByProject is a method to get profile status for a project
463
func (s *Server) GetProfileStatusByProject(ctx context.Context,
464
        _ *minderv1.GetProfileStatusByProjectRequest) (*minderv1.GetProfileStatusByProjectResponse, error) {
×
465

×
466
        entityCtx := engcontext.EntityFromContext(ctx)
×
467

×
468
        err := entityCtx.ValidateProject(ctx, s.store)
×
469
        if err != nil {
×
470
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
471
        }
×
472

473
        // read profile status
474
        dbstats, err := s.store.GetProfileStatusByProject(ctx, entityCtx.Project.ID)
×
475
        if err != nil {
×
476
                if errors.Is(err, sql.ErrNoRows) {
×
477
                        return nil, status.Errorf(codes.NotFound, "profile statuses not found for project")
×
478
                }
×
479
                return nil, status.Errorf(codes.Unknown, "failed to get profile status: %s", err)
×
480
        }
481

482
        res := &minderv1.GetProfileStatusByProjectResponse{
×
483
                ProfileStatus: make([]*minderv1.ProfileStatus, 0, len(dbstats)),
×
484
        }
×
485

×
486
        for _, dbstat := range dbstats {
×
487
                res.ProfileStatus = append(res.ProfileStatus, &minderv1.ProfileStatus{
×
488
                        ProfileId:     dbstat.ID.String(),
×
489
                        ProfileName:   dbstat.Name,
×
490
                        ProfileStatus: string(dbstat.ProfileStatus),
×
491
                })
×
492
        }
×
493

494
        // Telemetry logging
495
        logger.BusinessRecord(ctx).Project = entityCtx.Project.ID
×
496

×
497
        return res, nil
×
498
}
499

500
// PatchProfile updates a profile for a project with a partial request
501
func (s *Server) PatchProfile(ctx context.Context, ppr *minderv1.PatchProfileRequest) (*minderv1.PatchProfileResponse, error) {
14✔
502
        patch := ppr.GetPatch()
14✔
503
        entityCtx := engcontext.EntityFromContext(ctx)
14✔
504

14✔
505
        err := entityCtx.ValidateProject(ctx, s.store)
14✔
506
        if err != nil {
14✔
507
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
508
        }
×
509

510
        if ppr.GetId() == "" {
14✔
511
                return nil, util.UserVisibleError(codes.InvalidArgument, "profile ID must be specified")
×
512
        }
×
513

514
        profileID, err := uuid.Parse(ppr.GetId())
14✔
515
        if err != nil {
14✔
516
                return nil, util.UserVisibleError(codes.InvalidArgument, "Malformed UUID")
×
517
        }
×
518

519
        patchedProfile, err := db.WithTransaction(s.store, func(qtx db.ExtendQuerier) (*minderv1.Profile, error) {
28✔
520
                return s.profiles.PatchProfile(ctx, entityCtx.Project.ID, profileID, patch, ppr.GetUpdateMask(), qtx)
14✔
521
        })
14✔
522
        if err != nil {
19✔
523
                // assumption: service layer sets sensible errors
5✔
524
                return nil, err
5✔
525
        }
5✔
526

527
        return &minderv1.PatchProfileResponse{
9✔
528
                Profile: patchedProfile,
9✔
529
        }, nil
9✔
530
}
531

532
// UpdateProfile updates a profile for a project
533
func (s *Server) UpdateProfile(ctx context.Context,
534
        cpr *minderv1.UpdateProfileRequest) (*minderv1.UpdateProfileResponse, error) {
×
535
        in := cpr.GetProfile()
×
536

×
537
        entityCtx := engcontext.EntityFromContext(ctx)
×
538

×
539
        err := entityCtx.ValidateProject(ctx, s.store)
×
540
        if err != nil {
×
541
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
542
        }
×
543

544
        updatedProfile, err := db.WithTransaction(s.store, func(qtx db.ExtendQuerier) (*minderv1.Profile, error) {
×
545
                return s.profiles.UpdateProfile(ctx, entityCtx.Project.ID, uuid.Nil, in, qtx)
×
546
        })
×
547

548
        if err != nil {
×
549
                // assumption: service layer sets sensible errors
×
550
                return nil, err
×
551
        }
×
552

553
        return &minderv1.UpdateProfileResponse{
×
554
                Profile: updatedProfile,
×
555
        }, nil
×
556
}
557

558
// GetProfileStatusByName retrieves profile status by name
559
func (s *Server) GetProfileStatusByName(
560
        ctx context.Context,
561
        in *minderv1.GetProfileStatusByNameRequest,
562
) (*minderv1.GetProfileStatusByNameResponse, error) {
1✔
563
        ctx = context.WithValue(ctx, requestKey, in)
1✔
564

1✔
565
        // Validate name is not empty
1✔
566
        if in.Name == "" {
1✔
NEW
567
                return nil, util.UserVisibleError(codes.InvalidArgument, "profile name cannot be empty")
×
NEW
568
        }
×
569

570
        entityCtx := engcontext.EntityFromContext(ctx)
1✔
571

1✔
572
        if err := entityCtx.ValidateProject(ctx, s.store); err != nil {
1✔
NEW
573
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
NEW
574
        }
×
575

576
        dbProfileStatus, err := s.store.GetProfileStatusByNameAndProject(ctx, db.GetProfileStatusByNameAndProjectParams{
1✔
577
                ProjectID: engcontext.EntityFromContext(ctx).Project.ID,
1✔
578
                Name:      in.Name,
1✔
579
        })
1✔
580
        if err != nil {
1✔
NEW
581
                if errors.Is(err, sql.ErrNoRows) {
×
NEW
582
                        return nil, util.UserVisibleError(codes.NotFound, "profile %q status not found", in.Name)
×
NEW
583
                }
×
NEW
584
                return nil, status.Errorf(codes.Unknown, "failed to get profile: %s", err)
×
585
        }
586

587
        resp, err := s.processProfileStatusByName(ctx, dbProfileStatus.Name, dbProfileStatus.ID,
1✔
588
                timestamppb.New(dbProfileStatus.LastUpdated), string(dbProfileStatus.ProfileStatus), in)
1✔
589
        if err != nil {
1✔
NEW
590
                return nil, err
×
NEW
591
        }
×
592

593
        return &minderv1.GetProfileStatusByNameResponse{
1✔
594
                ProfileStatus:        resp.ProfileStatus,
1✔
595
                RuleEvaluationStatus: resp.RuleEvaluationStatus,
1✔
596
        }, nil
1✔
597
}
598

599
// GetProfileStatusById retrieves profile status by ID
600
func (s *Server) GetProfileStatusById(
601
        ctx context.Context,
602
        in *minderv1.GetProfileStatusByIdRequest,
603
) (*minderv1.GetProfileStatusByIdResponse, error) {
1✔
604
        ctx = context.WithValue(ctx, requestKey, in)
1✔
605

1✔
606
        if in.Id == "" {
1✔
NEW
607
                return nil, util.UserVisibleError(codes.InvalidArgument, "profile id cannot be empty")
×
NEW
608
        }
×
609

610
        entityCtx := engcontext.EntityFromContext(ctx)
1✔
611

1✔
612
        if err := entityCtx.ValidateProject(ctx, s.store); err != nil {
1✔
NEW
613
                return nil, status.Errorf(codes.InvalidArgument, "error in entity context: %v", err)
×
NEW
614
        }
×
615

616
        dbProfileStatus, err := s.store.GetProfileStatusByIdAndProject(ctx, db.GetProfileStatusByIdAndProjectParams{
1✔
617
                ProjectID: engcontext.EntityFromContext(ctx).Project.ID,
1✔
618
                ID:        uuid.MustParse(in.Id),
1✔
619
        })
1✔
620
        if err != nil {
1✔
NEW
621
                if errors.Is(err, sql.ErrNoRows) {
×
NEW
622
                        return nil, util.UserVisibleError(codes.NotFound, "profile %q status not found", in.Id)
×
NEW
623
                }
×
NEW
624
                return nil, status.Errorf(codes.Unknown, "failed to get profile: %s", err)
×
625
        }
626

627
        resp, err := s.processProfileStatusById(ctx, dbProfileStatus.Name, dbProfileStatus.ID,
1✔
628
                timestamppb.New(dbProfileStatus.LastUpdated), string(dbProfileStatus.ProfileStatus), in)
1✔
629
        if err != nil {
1✔
NEW
630
                return nil, err
×
NEW
631
        }
×
632

633
        return &minderv1.GetProfileStatusByIdResponse{
1✔
634
                ProfileStatus:        resp.ProfileStatus,
1✔
635
                RuleEvaluationStatus: resp.RuleEvaluationStatus,
1✔
636
        }, nil
1✔
637
}
638

639
func extractEntitySelector(entity *minderv1.EntityTypedId) *uuid.NullUUID {
2✔
640
        if entity == nil {
4✔
641
                return nil
2✔
642
        }
2✔
NEW
643
        var selector uuid.NullUUID
×
NEW
644
        if err := selector.Scan(entity.GetId()); err != nil {
×
NEW
645
                return nil
×
NEW
646
        }
×
NEW
647
        return &selector
×
648
}
649

650
func (s *Server) processProfileStatusByName(
651
        ctx context.Context,
652
        profileName string,
653
        profileID uuid.UUID,
654
        lastUpdated *timestamppb.Timestamp,
655
        profileStatus string,
656
        req *minderv1.GetProfileStatusByNameRequest,
657
) (*minderv1.GetProfileStatusResponse, error) {
1✔
658
        var ruleEvaluationStatuses []*minderv1.RuleEvaluationStatus
1✔
659

1✔
660
        selector, ruleType, ruleName, err := extractFiltersFromNameRequest(req)
1✔
661
        if err != nil {
1✔
NEW
662
                return nil, err
×
NEW
663
        }
×
664

665
        if selector != nil || req.GetAll() {
1✔
NEW
666
                var entityID uuid.NullUUID
×
NEW
667
                if selector != nil {
×
NEW
668
                        entityID = *selector
×
NEW
669
                }
×
NEW
670
                dbRuleEvaluationStatuses, err := s.store.ListRuleEvaluationsByProfileId(ctx, db.ListRuleEvaluationsByProfileIdParams{
×
NEW
671
                        ProfileID:    profileID,
×
NEW
672
                        EntityID:     entityID,
×
NEW
673
                        RuleTypeName: *ruleType,
×
NEW
674
                        RuleName:     *ruleName,
×
NEW
675
                })
×
NEW
676
                if err != nil && !errors.Is(err, sql.ErrNoRows) {
×
NEW
677
                        return nil, status.Errorf(codes.Unknown, "failed to list rule evaluation status: %s", err)
×
NEW
678
                }
×
679

NEW
680
                ruleEvaluationStatuses = s.getRuleEvaluationStatuses(
×
NEW
681
                        ctx, dbRuleEvaluationStatuses, profileID.String(),
×
NEW
682
                )
×
683
        }
684

685
        // Telemetry logging
686
        entityCtx := engcontext.EntityFromContext(ctx)
1✔
687
        logger.BusinessRecord(ctx).Project = entityCtx.Project.ID
1✔
688
        logger.BusinessRecord(ctx).Profile = logger.Profile{Name: profileName, ID: profileID}
1✔
689

1✔
690
        return &minderv1.GetProfileStatusResponse{
1✔
691
                ProfileStatus: &minderv1.ProfileStatus{
1✔
692
                        ProfileId:     profileID.String(),
1✔
693
                        ProfileName:   profileName,
1✔
694
                        ProfileStatus: profileStatus,
1✔
695
                        LastUpdated:   lastUpdated,
1✔
696
                },
1✔
697
                RuleEvaluationStatus: ruleEvaluationStatuses,
1✔
698
        }, nil
1✔
699
}
700

701
func (s *Server) processProfileStatusById(
702
        ctx context.Context,
703
        profileName string,
704
        profileID uuid.UUID,
705
        lastUpdated *timestamppb.Timestamp,
706
        profileStatus string,
707
        req *minderv1.GetProfileStatusByIdRequest,
708
) (*minderv1.GetProfileStatusResponse, error) {
1✔
709
        var ruleEvaluationStatuses []*minderv1.RuleEvaluationStatus
1✔
710

1✔
711
        selector, ruleType, ruleName, err := extractFiltersFromIdRequest(req)
1✔
712
        if err != nil {
1✔
NEW
713
                return nil, err
×
NEW
714
        }
×
715

716
        // Only fetch rule evaluations if selector is present or all is requested
717
        if selector != nil || req.GetAll() {
1✔
NEW
718
                var entityID uuid.NullUUID
×
NEW
719
                if selector != nil {
×
NEW
720
                        entityID = *selector
×
NEW
721
                }
×
NEW
722
                dbRuleEvaluationStatuses, err := s.store.ListRuleEvaluationsByProfileId(ctx, db.ListRuleEvaluationsByProfileIdParams{
×
NEW
723
                        ProfileID:    profileID,
×
NEW
724
                        EntityID:     *&entityID,
×
NEW
725
                        RuleTypeName: *ruleType,
×
NEW
726
                        RuleName:     *ruleName,
×
NEW
727
                })
×
NEW
728
                if err != nil && !errors.Is(err, sql.ErrNoRows) {
×
NEW
729
                        return nil, status.Errorf(codes.Unknown, "failed to list rule evaluation status: %s", err)
×
NEW
730
                }
×
731

NEW
732
                ruleEvaluationStatuses = s.getRuleEvaluationStatuses(
×
NEW
733
                        ctx, dbRuleEvaluationStatuses, profileID.String(),
×
NEW
734
                )
×
735
        }
736

737
        // Telemetry logging
738
        entityCtx := engcontext.EntityFromContext(ctx)
1✔
739
        logger.BusinessRecord(ctx).Project = entityCtx.Project.ID
1✔
740
        logger.BusinessRecord(ctx).Profile = logger.Profile{Name: profileName, ID: profileID}
1✔
741

1✔
742
        return &minderv1.GetProfileStatusResponse{
1✔
743
                ProfileStatus: &minderv1.ProfileStatus{
1✔
744
                        ProfileId:     profileID.String(),
1✔
745
                        ProfileName:   profileName,
1✔
746
                        ProfileStatus: profileStatus,
1✔
747
                        LastUpdated:   lastUpdated,
1✔
748
                },
1✔
749
                RuleEvaluationStatus: ruleEvaluationStatuses,
1✔
750
        }, nil
1✔
751
}
752

753
func extractFiltersFromNameRequest(
754
        req *minderv1.GetProfileStatusByNameRequest) (
755
        *uuid.NullUUID, *sql.NullString, *sql.NullString, error) {
1✔
756
        if e := req.GetEntity(); e != nil {
1✔
NEW
757
                if !e.GetType().IsValid() {
×
NEW
758
                        return nil, nil, nil, util.UserVisibleError(codes.InvalidArgument,
×
NEW
759
                                "invalid entity type %s, please use one of %s",
×
NEW
760
                                e.GetType(), entities.KnownTypesCSV())
×
NEW
761
                }
×
762
        }
763

764
        selector := extractEntitySelector(req.GetEntity())
1✔
765

1✔
766
        ruleType := &sql.NullString{
1✔
767
                String: req.GetRuleType(),
1✔
768
                Valid:  req.GetRuleType() != "",
1✔
769
        }
1✔
770
        if !ruleType.Valid {
2✔
771
                //nolint:staticcheck // ignore SA1019: Deprecated field supported for backward compatibility
1✔
772
                ruleType = &sql.NullString{
1✔
773
                        String: req.GetRule(),
1✔
774
                        Valid:  req.GetRule() != "",
1✔
775
                }
1✔
776
        }
1✔
777

778
        ruleName := &sql.NullString{
1✔
779
                String: req.GetRuleName(),
1✔
780
                Valid:  req.GetRuleName() != "",
1✔
781
        }
1✔
782

1✔
783
        return selector, ruleType, ruleName, nil
1✔
784
}
785

786
func extractFiltersFromIdRequest(
787
        req *minderv1.GetProfileStatusByIdRequest) (
788
        *uuid.NullUUID, *sql.NullString, *sql.NullString, error) {
1✔
789
        if e := req.GetEntity(); e != nil {
1✔
NEW
790
                if !e.GetType().IsValid() {
×
NEW
791
                        return nil, nil, nil, util.UserVisibleError(codes.InvalidArgument,
×
NEW
792
                                "invalid entity type %s, please use one of %s",
×
NEW
793
                                e.GetType(), entities.KnownTypesCSV())
×
NEW
794
                }
×
795
        }
796

797
        selector := extractEntitySelector(req.GetEntity())
1✔
798

1✔
799
        ruleType := &sql.NullString{
1✔
800
                String: req.GetRuleType(),
1✔
801
                Valid:  req.GetRuleType() != "",
1✔
802
        }
1✔
803

1✔
804
        ruleName := &sql.NullString{
1✔
805
                String: req.GetRuleName(),
1✔
806
                Valid:  req.GetRuleName() != "",
1✔
807
        }
1✔
808

1✔
809
        return selector, ruleType, ruleName, nil
1✔
810
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc