Skip to content
Open
7 changes: 7 additions & 0 deletions cmd/server/app/migrate_up.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (

"github.com/mindersec/minder/database"
"github.com/mindersec/minder/internal/authz"
"github.com/mindersec/minder/internal/db"
"github.com/mindersec/minder/internal/providers/github/service"
"github.com/mindersec/minder/pkg/config"
serverconfig "github.com/mindersec/minder/pkg/config/server"
)
Expand Down Expand Up @@ -98,6 +100,11 @@ var upCmd = &cobra.Command{
return fmt.Errorf("error preparing authz client: %w", err)
}

cmd.Println("Backfilling organizations...")
if err := service.BackfillOrganizations(ctx, db.NewStore(dbConn)); err != nil {
return fmt.Errorf("error while backfilling organizations: %w", err)
}

return nil
},
}
Expand Down
4 changes: 4 additions & 0 deletions database/migrations/000118_organization_entity.down.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- SPDX-FileCopyrightText: Copyright 2026 The Minder Authors
-- SPDX-License-Identifier: Apache-2.0

-- Postgres doesn't easily drop enum values, down migration is a no-op
4 changes: 4 additions & 0 deletions database/migrations/000118_organization_entity.up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
-- SPDX-FileCopyrightText: Copyright 2026 The Minder Authors
-- SPDX-License-Identifier: Apache-2.0

ALTER TYPE entities ADD VALUE 'organization';
1 change: 1 addition & 0 deletions docs/docs/ref/proto.mdx

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

31 changes: 31 additions & 0 deletions internal/controlplane/handlers_entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,34 @@ func createEntityMessage(

return msg, nil
}

func (s *Server) publishOrganizationEntityEvent(
ctx context.Context,
providerID, projectID uuid.UUID,
login string,
) {
l := zerolog.Ctx(ctx)
msg := message.NewMessage(uuid.New().String(), nil)
msg.SetContext(ctx)

orgProps := properties.NewProperties(map[string]any{
properties.PropertyName: login,
})

event := messages.NewMinderEvent().
WithProjectID(projectID).
WithProviderID(providerID).
WithEntityType(pb.Entity_ENTITY_ORGANIZATION).
WithProperties(orgProps)

if err := event.ToMessage(msg); err != nil {
l.Error().Err(err).Msg("error marshalling organization entity event")
return
}

if err := s.evt.Publish(constants.TopicQueueReconcileEntityAdd, msg); err != nil {
l.Error().Err(err).Msg("error publishing organization entity event")
} else {
l.Info().Str("messageID", msg.UUID).Msg("published organization entity event for execution")
}
}
2 changes: 2 additions & 0 deletions internal/controlplane/handlers_evalstatus.go
Original file line number Diff line number Diff line change
Expand Up @@ -768,6 +768,8 @@ func dbEntityToEntity(dbEnt db.Entities) minderv1.Entity {
switch dbEnt {
case db.EntitiesPullRequest:
return minderv1.Entity_ENTITY_PULL_REQUESTS
case db.EntitiesOrganization:
return minderv1.Entity_ENTITY_ORGANIZATION
case db.EntitiesArtifact:
return minderv1.Entity_ENTITY_ARTIFACTS
case db.EntitiesRepository:
Expand Down
41 changes: 29 additions & 12 deletions internal/controlplane/handlers_oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -436,18 +436,13 @@ func (s *Server) processAppCallback(ctx context.Context, w http.ResponseWriter,

logger.BusinessRecord(ctx).Project = stateData.ProjectID

var confErr providers.ErrProviderInvalidConfig
_, err = s.ghProviders.CreateGitHubAppProvider(ctx, *token, stateData, installationID, state)
dbProv, err := s.ghProviders.CreateGitHubAppProvider(ctx, *token, stateData, installationID, state)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the only time the return value from CreateGitHubAppProvider is used is in a unit test, it should be easy to change that to return a provider facet or some other struct which can be used to resolve any provider properties we need. (Currently, it seems like mostly the org name, but possibly other data in the future.)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rather than returning a bunch of state which needs to be passed along to publishOrganizationEntityEvent, let's move that call into the ghProviders service, rather than having it here and in handlers_user.go.

if err != nil {
if errors.As(err, &confErr) {
return newHttpError(http.StatusBadRequest, "Invalid provider config").SetContents(
"The provider configuration is invalid: %s", confErr.Details)
}
if errors.Is(err, service.ErrInvalidTokenIdentity) {
return newHttpError(http.StatusForbidden, "User token mismatch").SetContents(
"The provided login token was associated with a different GitHub user.")
}
return fmt.Errorf("error creating GitHub App provider: %w", err)
return handleProviderCreationError(err)
}

if dbProv != nil {
s.publishOrganizationEntityEvent(ctx, dbProv.Provider.ID, dbProv.Provider.ProjectID, dbProv.InstallationOwner)
}

if stateData.RedirectUrl.Valid || stateData.EncryptedRedirect.Valid {
Expand Down Expand Up @@ -541,7 +536,16 @@ func (s *Server) handleAppInstallWithoutInvite(ctx context.Context, token *oauth
}

_, err = db.WithTransaction(s.store, func(qtx db.ExtendQuerier) (*db.Project, error) {
return s.ghProviders.CreateGitHubAppWithoutInvitation(ctx, qtx, *userID, installationID)
proj, dbProv, err := s.ghProviders.CreateGitHubAppWithoutInvitation(ctx, qtx, *userID, installationID)
if err != nil {
return nil, err
}
if dbProv != nil && proj != nil {
// It is generally safe to publish an event from within a transaction, as long
// as the event handler evaluates the state matching later.
s.publishOrganizationEntityEvent(ctx, dbProv.Provider.ID, proj.ID, dbProv.InstallationOwner)
}
return proj, nil
})
return err
}
Expand Down Expand Up @@ -787,3 +791,16 @@ func (s *Server) decryptRedirect(stateData *db.GetProjectIDBySessionStateRow) (*
}
return parsedURL, nil
}

func handleProviderCreationError(err error) error {
var confErr providers.ErrProviderInvalidConfig
if errors.As(err, &confErr) {
return newHttpError(http.StatusBadRequest, "Invalid provider config").SetContents(
"The provider configuration is invalid: %s", confErr.Details)
}
if errors.Is(err, service.ErrInvalidTokenIdentity) {
return newHttpError(http.StatusForbidden, "User token mismatch").SetContents(
"The provided login token was associated with a different GitHub user.")
}
return fmt.Errorf("error creating GitHub App provider: %w", err)
}
4 changes: 2 additions & 2 deletions internal/controlplane/handlers_oauth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -829,7 +829,7 @@ func TestHandleGitHubAppCallback(t *testing.T) {
}, nil)
service.EXPECT().
CreateGitHubAppProvider(gomock.Any(), gomock.Any(), gomock.Any(), installationID, gomock.Any()).
Return(&db.Provider{}, nil)
Return(&ghService.GitHubProviderFacet{Provider: &db.Provider{}}, nil)
},
checkResponse: func(t *testing.T, resp httptest.ResponseRecorder) {
t.Helper()
Expand All @@ -851,7 +851,7 @@ func TestHandleGitHubAppCallback(t *testing.T) {
db.EXPECT().Rollback(gomock.Any()).Return(nil)
service.EXPECT().
CreateGitHubAppWithoutInvitation(gomock.Any(), gomock.Any(), userId, installationID).
Return(nil, nil)
Return(nil, nil, nil)
},
checkResponse: func(t *testing.T, resp httptest.ResponseRecorder) {
t.Helper()
Expand Down
3 changes: 2 additions & 1 deletion internal/controlplane/handlers_profile.go
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,8 @@ func (s *Server) getRuleEvalStatus(
repoPath = fmt.Sprintf("%s/%s", prRepoOwner, prRepoName)
}
case db.EntitiesBuildEnvironment, db.EntitiesRelease, db.EntitiesPipelineRun,
db.EntitiesTaskRun, db.EntitiesBuild:
db.EntitiesTaskRun, db.EntitiesBuild, db.EntitiesOrganization:
Comment on lines 429 to +430

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably okay for now, but it seems like alerts will have incorrect URLs (and log errors) until we fix it. It can be a good idea to file an issue for this if we merge this PR, to track the need to update it later.

// TODO: Alert URLs for organizations are incorrect
zerolog.Ctx(ctx).Warn().Msgf("attempting to set alerts for unsupported entity type: %v", dbRuleEvalStat.EntityType)
default:
zerolog.Ctx(ctx).Error().Msgf("unknown entity type: %v", dbRuleEvalStat.EntityType)
Expand Down
5 changes: 4 additions & 1 deletion internal/controlplane/handlers_user.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,14 @@ func (s *Server) claimGitHubInstalls(ctx context.Context, qtx db.ExtendQuerier)

for _, i := range installs {
// TODO: if we can get an GitHub auth token for the user, we can do the rest with CreateGitHubAppWithoutInvitation
proj, err := s.ghProviders.CreateGitHubAppWithoutInvitation(ctx, qtx, userID, i.AppInstallationID)
proj, dbProv, err := s.ghProviders.CreateGitHubAppWithoutInvitation(ctx, qtx, userID, i.AppInstallationID)
if err != nil {
zerolog.Ctx(ctx).Error().Err(err).Int64("org_id", i.OrganizationID).Msg("failed to create GitHub app at first login")
continue
}
if dbProv != nil {
s.publishOrganizationEntityEvent(ctx, dbProv.Provider.ID, proj.ID, dbProv.InstallationOwner)
}
Comment on lines +142 to +144

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, let's try to centralize this pattern in the GitHub provider.

if proj != nil {
userProjects = append(userProjects, proj)
}
Expand Down
6 changes: 3 additions & 3 deletions internal/controlplane/handlers_user_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ func TestCreateUser_gRPC(t *testing.T) {
Return(&db.Project{
ID: projectID,
Name: "github-org1",
}, nil)
}, nil, nil)

store.EXPECT().Commit(gomock.Any())
store.EXPECT().Rollback(gomock.Any())
Expand Down Expand Up @@ -206,14 +206,14 @@ func TestCreateUser_gRPC(t *testing.T) {
Return(&db.Project{
ID: projectID,
Name: "github-org1",
}, nil)
}, nil, nil)

prov.EXPECT().
CreateGitHubAppWithoutInvitation(gomock.Any(), gomock.Any(), int64(31337), int64(11)).
Return(&db.Project{
ID: uuid.New(),
Name: "github-org2",
}, nil)
}, nil, nil)

store.EXPECT().Commit(gomock.Any())
store.EXPECT().Rollback(gomock.Any())
Expand Down
1 change: 1 addition & 0 deletions internal/db/models.go

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

3 changes: 2 additions & 1 deletion internal/eea/eea.go
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,8 @@ func (e *EEA) buildEntityWrapper(
case db.EntitiesPullRequest:
return e.buildPullRequestInfoWrapper(ctx, entityID, projID)
case db.EntitiesBuildEnvironment, db.EntitiesRelease,
db.EntitiesPipelineRun, db.EntitiesTaskRun, db.EntitiesBuild:
db.EntitiesPipelineRun, db.EntitiesTaskRun, db.EntitiesBuild, db.EntitiesOrganization:
// TODO: Support evaluate policy on organizations
return nil, fmt.Errorf("entity type %q not yet supported", entity)
Comment on lines +258 to 260

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think leaving this this way will prevent us from evaluating policy on organizations (but I'd need to test to be sure). Definitely something to file a follow-up issue on if we leave like this.

default:
return nil, fmt.Errorf("unknown entity type: %q", entity)
Expand Down
4 changes: 4 additions & 0 deletions internal/engine/entities/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ func EntityTypeFromDB(entity db.Entities) minderv1.Entity {
return minderv1.Entity_ENTITY_PIPELINE_RUN
case db.EntitiesTaskRun:
return minderv1.Entity_ENTITY_TASK_RUN
case db.EntitiesOrganization:
return minderv1.Entity_ENTITY_ORGANIZATION
case db.EntitiesBuild:
return minderv1.Entity_ENTITY_BUILD
default:
Expand All @@ -76,6 +78,8 @@ func EntityTypeToDB(entity minderv1.Entity) db.Entities {
dbEnt = db.EntitiesPipelineRun
case minderv1.Entity_ENTITY_TASK_RUN:
dbEnt = db.EntitiesTaskRun
case minderv1.Entity_ENTITY_ORGANIZATION:
dbEnt = db.EntitiesOrganization
case minderv1.Entity_ENTITY_BUILD:
dbEnt = db.EntitiesBuild
case minderv1.Entity_ENTITY_UNSPECIFIED:
Expand Down
2 changes: 2 additions & 0 deletions internal/engine/entities/entity_type_conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ func EntityTypeToDBType(entityType pb.Entity) (db.Entities, error) {
return db.EntitiesPipelineRun, nil
case pb.Entity_ENTITY_TASK_RUN:
return db.EntitiesTaskRun, nil
case pb.Entity_ENTITY_ORGANIZATION:
return db.EntitiesOrganization, nil
case pb.Entity_ENTITY_BUILD:
return db.EntitiesBuild, nil
case pb.Entity_ENTITY_UNSPECIFIED:
Expand Down
2 changes: 1 addition & 1 deletion internal/logger/telemetry_store_watermill.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func newTelemetryStoreFromEntity(inf *entities.EntityInfoWrapper) (*TelemetrySto
ts.PullRequest = ent
case minderv1.Entity_ENTITY_BUILD_ENVIRONMENTS,
minderv1.Entity_ENTITY_RELEASE, minderv1.Entity_ENTITY_PIPELINE_RUN,
minderv1.Entity_ENTITY_TASK_RUN, minderv1.Entity_ENTITY_BUILD:
minderv1.Entity_ENTITY_TASK_RUN, minderv1.Entity_ENTITY_BUILD, minderv1.Entity_ENTITY_ORGANIZATION:
// Noop, see https://github.com/mindersec/minder/issues/3838
case minderv1.Entity_ENTITY_UNSPECIFIED:
// Do nothing
Expand Down
7 changes: 6 additions & 1 deletion internal/providers/github/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -1006,11 +1006,16 @@ func IsMinderHook(hook *github.Hook, hostURL string) (bool, error) {
return false, nil
}

// GetGithubAppOwner returns the owner of the GitHub App given a provider name.
func GetGithubAppOwner(provName string) string {
return strings.TrimPrefix(provName, string(db.ProviderClassGithubApp)+"-")
}

// CanHandleOwner checks if the GitHub provider has the right credentials to handle the owner
func CanHandleOwner(_ context.Context, prov db.Provider, owner string) bool {
// TODO: this is fragile and does not handle organization renames, in the future we can make sure the credential
// has admin permissions on the owner
if prov.Name == fmt.Sprintf("%s-%s", db.ProviderClassGithubApp, owner) {
if prov.Class == db.ProviderClassGithubApp && GetGithubAppOwner(prov.Name) == owner {
return true
}
if prov.Class == db.ProviderClassGithub {
Expand Down
2 changes: 2 additions & 0 deletions internal/providers/github/entities.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ func (c *GitHub) RegisterEntity(
case minderv1.Entity_ENTITY_ARTIFACTS:
fallthrough
case minderv1.Entity_ENTITY_RELEASE:
fallthrough
case minderv1.Entity_ENTITY_ORGANIZATION:
// Nothing to do, accept:
return props, nil
case minderv1.Entity_ENTITY_REPOSITORIES:
Expand Down
2 changes: 2 additions & 0 deletions internal/providers/github/properties/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ func (ghEntityFetcher) EntityPropertyFetcher(entType minderv1.Entity) GhProperty
return NewArtifactFetcher()
case minderv1.Entity_ENTITY_RELEASE:
return NewReleaseFetcher()
case minderv1.Entity_ENTITY_ORGANIZATION:
return NewOrganizationFetcher()
}

return nil
Expand Down
Loading
Loading