Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/go_modules/github.com/aws/aws-sdk…
Browse files Browse the repository at this point in the history
…-go-v2/config-1.28.5
  • Loading branch information
rdimitrov authored Nov 19, 2024
2 parents 83373d1 + 8590a30 commit 61d9d63
Show file tree
Hide file tree
Showing 12 changed files with 230 additions and 14 deletions.
14 changes: 14 additions & 0 deletions database/mock/store.go

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

7 changes: 6 additions & 1 deletion database/query/entitlements.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ WHERE e.project_id = sqlc.arg(project_id)::UUID AND e.feature = sqlc.arg(feature
-- name: GetEntitlementFeaturesByProjectID :many
SELECT feature
FROM entitlements
WHERE project_id = sqlc.arg(project_id)::UUID;
WHERE project_id = sqlc.arg(project_id)::UUID;

-- name: CreateEntitlements :exec
INSERT INTO entitlements (feature, project_id)
SELECT unnest(sqlc.arg(features)::text[]), sqlc.arg(project_id)::UUID
ON CONFLICT DO NOTHING;
9 changes: 9 additions & 0 deletions internal/controlplane/handlers_projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,15 @@ func (s *Server) CreateProject(
return nil, status.Errorf(codes.Internal, "error creating subproject: %v", err)
}

// Retrieve the membership-to-feature mapping from the configuration
projectFeatures := s.cfg.Features.GetFeaturesForMemberships(ctx)
if err := qtx.CreateEntitlements(ctx, db.CreateEntitlementsParams{
Features: projectFeatures,
ProjectID: subProject.ID,
}); err != nil {
return nil, status.Errorf(codes.Internal, "error creating entitlements: %v", err)
}

if err := s.authzClient.Adopt(ctx, parent.ID, subProject.ID); err != nil {
return nil, status.Errorf(codes.Internal, "error creating subproject: %v", err)
}
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 @@ -83,6 +83,8 @@ func TestCreateUser_gRPC(t *testing.T) {
store.EXPECT().
CreateUser(gomock.Any(), gomock.Any()).
Return(returnedUser, nil)
store.EXPECT().CreateEntitlements(gomock.Any(), gomock.Any()).
Return(nil)
store.EXPECT().Commit(gomock.Any())
store.EXPECT().Rollback(gomock.Any())
tokenResult, _ := openid.NewBuilder().GivenName("Foo").FamilyName("Bar").Email("[email protected]").Subject("subject1").Build()
Expand Down Expand Up @@ -262,6 +264,7 @@ func TestCreateUser_gRPC(t *testing.T) {
authz,
marketplaces.NewNoopMarketplace(),
&serverconfig.DefaultProfilesConfig{},
&serverconfig.FeaturesConfig{},
),
}

Expand Down
17 changes: 17 additions & 0 deletions internal/db/entitlements.sql.go

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

1 change: 1 addition & 0 deletions internal/db/querier.go

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

12 changes: 12 additions & 0 deletions internal/projects/creator.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,17 +39,20 @@ type projectCreator struct {
authzClient authz.Client
marketplace marketplaces.Marketplace
profilesCfg *server.DefaultProfilesConfig
featuresCfg *server.FeaturesConfig
}

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

Expand Down Expand Up @@ -105,6 +108,15 @@ func (p *projectCreator) ProvisionSelfEnrolledProject(
return nil, fmt.Errorf("failed to create default project: %v", err)
}

// Retrieve the membership-to-feature mapping from the configuration
projectFeatures := p.featuresCfg.GetFeaturesForMemberships(ctx)
if err := qtx.CreateEntitlements(ctx, db.CreateEntitlementsParams{
Features: projectFeatures,
ProjectID: project.ID,
}); err != nil {
return nil, fmt.Errorf("error creating entitlements: %w", err)
}

// Enable any default profiles and rule types in the project.
// For now, we subscribe to a single bundle and a single profile.
// Both are specified in the service config.
Expand Down
45 changes: 39 additions & 6 deletions internal/projects/creator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,17 @@ package projects_test
import (
"context"
"fmt"
"reflect"
"testing"

"github.com/google/uuid"
"github.com/lestrrat-go/jwx/v2/jwt/openid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/mock/gomock"

mockdb "github.com/mindersec/minder/database/mock"
"github.com/mindersec/minder/internal/auth/jwt"
"github.com/mindersec/minder/internal/authz/mock"
"github.com/mindersec/minder/internal/db"
"github.com/mindersec/minder/internal/marketplaces"
Expand All @@ -33,10 +37,28 @@ func TestProvisionSelfEnrolledProject(t *testing.T) {
Return(db.Project{
ID: uuid.New(),
}, nil)
mockStore.EXPECT().CreateEntitlements(gomock.Any(), gomock.Any()).
DoAndReturn(func(_ context.Context, params db.CreateEntitlementsParams) error {
expectedFeatures := []string{"featureA", "featureB"}
if !reflect.DeepEqual(params.Features, expectedFeatures) {
t.Errorf("expected features %v, got %v", expectedFeatures, params.Features)
}
return nil
})

ctx := prepareTestToken(context.Background(), t, []any{
"teamA",
"teamB",
"teamC",
})

creator := projects.NewProjectCreator(authzClient, marketplaces.NewNoopMarketplace(), &server.DefaultProfilesConfig{}, &server.FeaturesConfig{
MembershipFeatureMapping: map[string]string{
"teamA": "featureA",
"teamB": "featureB",
},
})

ctx := context.Background()

creator := projects.NewProjectCreator(authzClient, marketplaces.NewNoopMarketplace(), &server.DefaultProfilesConfig{})
_, err := creator.ProvisionSelfEnrolledProject(
ctx,
mockStore,
Expand All @@ -62,8 +84,7 @@ func TestProvisionSelfEnrolledProjectFailsWritingProjectToDB(t *testing.T) {
Return(db.Project{}, fmt.Errorf("failed to create project"))

ctx := context.Background()

creator := projects.NewProjectCreator(authzClient, marketplaces.NewNoopMarketplace(), &server.DefaultProfilesConfig{})
creator := projects.NewProjectCreator(authzClient, marketplaces.NewNoopMarketplace(), &server.DefaultProfilesConfig{}, &server.FeaturesConfig{})
_, err := creator.ProvisionSelfEnrolledProject(
ctx,
mockStore,
Expand Down Expand Up @@ -94,7 +115,7 @@ func TestProvisionSelfEnrolledProjectInvalidName(t *testing.T) {

mockStore := mockdb.NewMockStore(ctrl)
ctx := context.Background()
creator := projects.NewProjectCreator(authzClient, marketplaces.NewNoopMarketplace(), &server.DefaultProfilesConfig{})
creator := projects.NewProjectCreator(authzClient, marketplaces.NewNoopMarketplace(), &server.DefaultProfilesConfig{}, &server.FeaturesConfig{})

for _, tc := range testCases {
_, err := creator.ProvisionSelfEnrolledProject(
Expand All @@ -107,3 +128,15 @@ func TestProvisionSelfEnrolledProjectInvalidName(t *testing.T) {
}

}

// prepareTestToken creates a JWT token with the specified roles and returns the context with the token.
func prepareTestToken(ctx context.Context, t *testing.T, roles []any) context.Context {
t.Helper()

token := openid.New()
require.NoError(t, token.Set("realm_access", map[string]any{
"roles": roles,
}))

return jwt.WithAuthTokenContext(ctx, token)
}
2 changes: 1 addition & 1 deletion internal/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ func AllInOneServerService(
fallbackTokenClient := ghprov.NewFallbackTokenClient(cfg.Provider)
ghClientFactory := clients.NewGitHubClientFactory(providerMetrics)
providerStore := providers.NewProviderStore(store)
projectCreator := projects.NewProjectCreator(authzClient, marketplace, &cfg.DefaultProfiles)
projectCreator := projects.NewProjectCreator(authzClient, marketplace, &cfg.DefaultProfiles, &cfg.Features)
propSvc := propService.NewPropertiesService(store)

// TODO: isolate GitHub-specific wiring. We'll need to isolate GitHub
Expand Down
1 change: 1 addition & 0 deletions pkg/config/server/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Config struct {
Auth AuthConfig `mapstructure:"auth"`
WebhookConfig WebhookConfig `mapstructure:"webhook-config"`
Events EventConfig `mapstructure:"events"`
Features FeaturesConfig `mapstructure:"features"`
Authz AuthzConfig `mapstructure:"authz"`
Provider ProviderConfig `mapstructure:"provider"`
Marketplace MarketplaceConfig `mapstructure:"marketplace"`
Expand Down
53 changes: 53 additions & 0 deletions pkg/config/server/features.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: Copyright 2024 The Minder Authors
// SPDX-License-Identifier: Apache-2.0

package server

import (
"context"

"github.com/mindersec/minder/internal/auth/jwt"
)

// FeaturesConfig is the configuration for the features
type FeaturesConfig struct {
// MembershipFeatureMapping maps a membership to a feature
MembershipFeatureMapping map[string]string `mapstructure:"membership_feature_mapping"`
}

// GetFeaturesForMemberships returns the features associated with the memberships in the context
func (fc *FeaturesConfig) GetFeaturesForMemberships(ctx context.Context) []string {
memberships := extractMembershipsFromContext(ctx)

features := make([]string, 0, len(memberships))
for _, m := range memberships {
if feature := fc.MembershipFeatureMapping[m]; feature != "" {
features = append(features, feature)
}
}

return features
}

// extractMembershipsFromContext extracts memberships from the JWT in the context.
// Returns empty slice if no memberships are found.
func extractMembershipsFromContext(ctx context.Context) []string {
realmAccess, ok := jwt.GetUserClaimFromContext[map[string]any](ctx, "realm_access")
if !ok {
return nil
}

rawMemberships, ok := realmAccess["roles"].([]any)
if !ok {
return nil
}

memberships := make([]string, 0, len(rawMemberships))
for _, membership := range rawMemberships {
if membershipStr, ok := membership.(string); ok {
memberships = append(memberships, membershipStr)
}
}

return memberships
}
80 changes: 74 additions & 6 deletions pkg/profiles/validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"context"
"database/sql"
"encoding/json"
"os"
"strings"
"testing"

"github.com/google/uuid"
Expand Down Expand Up @@ -151,6 +151,78 @@ func TestValidatorScenarios(t *testing.T) {

var ruleTypeName = "branch_protection_allow_force_pushes"
var ruleTypeDisplayName = "Allow force pushes to the branch"
var ruleTypeContent = `
---
version: v1
release_phase: beta
type: rule-type
name: branch_protection_allow_force_pushes
display_name: Prevent overwriting git history
short_failure_message: Force pushes are allowed
severity:
value: medium
context:
provider: github
description: Disallow force pushes to the branch
guidance: |
Ensure that the appropriate setting is disabled for the branch
protection rule.
This setting prevents users with push access to force push to the
branch.
For more information, see [GitHub's
documentation](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule).
def:
# Defines the section of the pipeline the rule will appear in.
# This will affect the template used to render multiple parts
# of the rule.
in_entity: repository
# Defines the schema for parameters that will be passed to the rule
param_schema:
properties:
branch:
type: string
description: "The name of the branch to check. If left empty, the default branch will be used."
required:
- branch
# Defines the schema for writing a rule with this rule being checked
rule_schema:
type: object
properties: {}
# Defines the configuration for ingesting data relevant for the rule
ingest:
type: rest
rest:
# This is the path to the data source. Given that this will evaluate
# for each repository in the organization, we use a template that
# will be evaluated for each repository. The structure to use is the
# protobuf structure for the entity that is being evaluated.
endpoint: '{{ $branch_param := index .Params "branch" }}/repos/{{.Entity.Owner}}/{{.Entity.Name}}/branches/{{if ne $branch_param "" }}{{ $branch_param }}{{ else }}{{ .Entity.DefaultBranch }}{{ end }}/protection'
# This is the method to use to retrieve the data. It should already default to JSON
parse: json
fallback:
- http_code: 404
body: |
{"http_status": 404, "message": "Not Protected"}
# Defines the configuration for evaluating data ingested against the given policy
eval:
type: jq
jq:
- ingested:
def: ".allow_force_pushes.enabled"
constant: false
# Defines the configuration for remediating the rule
remediate:
type: gh_branch_protection
gh_branch_protection:
patch: |
{"allow_force_pushes": false }
# Defines the configuration for alerting on the rule
alert:
type: security_advisory
security_advisory: {}
`
var ruleName = "MyRule"
var ruleUUID = uuid.New()
var projectID = uuid.New()
Expand Down Expand Up @@ -277,11 +349,7 @@ func makeProfile(opts ...func(*minderv1.Profile)) *minderv1.Profile {

func loadRawRuleTypeDef() (json.RawMessage, error) {
// read rule type from disk and set it up as a fixture
f, err := os.Open("../../examples/rules-and-profiles/rule-types/github/branch_protection_allow_force_pushes.yaml")
if err != nil {
return nil, err
}
defer f.Close()
f := strings.NewReader(ruleTypeContent)

ruleType := &minderv1.RuleType{}
if err := minderv1.ParseResource(f, ruleType); err != nil {
Expand Down

0 comments on commit 61d9d63

Please sign in to comment.