Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Spike] Changes needed to support GitHub Actions direct authentication to Minder #4317

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

evankanderson
Copy link
Member

Summary

This is a SPIKE. It is not ready to merge, is more for discussion about how to approach managing Minder rules using IaC.

This change enables Minder to leverage GitHub Actions OIDC identities directly as authorizable identities without creating Keycloak entities for them. I suspect this may be desirable given the differences between machine identities (ephemeral, can be created & destroyed in seconds via automation) and human identities (generally long-lived, can accept contracts, etc).

A short version of the setup:

  • make run-docker and set up KeyCloak with GitHub authentication.

  • minder auth login as a human, which calls CreateUser and creates a project.

  • (set up ngrok or some other internet -> docker connectivity)

  • Run minder project role grant -s githubactions/repo:evankanderson/actions-id-token-testing:ref:refs/heads/main -r admin or the equivalent with your own action name.

    Note that this currently only supports exact-match action names (of the form repo:$SLUG:ref:$REFNAME)

  • Set up a workflow like the following: https://github.com/evankanderson/actions-id-token-testing/blob/d4cc940fd69e3de78d09af7d95a936c14b69eb3c/.github/workflows/minder-auth-token-test.yaml

    • Also set up something to apply; I simply copied in a single rule type for testing.
  • Run the workflow (the example uses a workflow_dispatch, but you could also use a push to main to trigger this automatically.

Your empty project will now have a codeql_enabled rule:

minder ruletype list                                                                                               
WARNING: Running against a test environment (localhost) and may not be stable
+--------------------------------------+--------------------------------------+----------------+--------------------------------+
|               PROJECT                |                  ID                  |      NAME      |          DESCRIPTION           |
+--------------------------------------+--------------------------------------+----------------+--------------------------------+
| 14ac1534-e81a-4060-b4b8-bff2f1ee076a | 5fc82ecc-db9b-4665-8a8e-ecb71eb751d7 | codeql_enabled | Verifies that CodeQL is        |
|                                      |                                      |                | enabled for the repository     |
+--------------------------------------+--------------------------------------+----------------+--------------------------------+

(Workflow run that created the rule -- see the "Apply Minder ruletypes" step)

Note that at no point was there a need to touch GitHub Secrets or otherwise expose credentials off the local machine -- only minder project role grant.

Fixes https://github.com/stacklok/minder-stories/issues/10

Change Type

Mark the type of change your PR introduces:

  • Bug fix (resolves an issue without affecting existing features)
  • Feature (adds new functionality without breaking changes)
  • Breaking change (may impact existing functionalities or require documentation updates)
  • Documentation (updates or additions to documentation)
  • Refactoring or test improvements (no bug fixes or new functionality)

Testing

This was tested manually.

Review Checklist:

  • :lolsob: Reviewed my own code for quality and clarity.
  • Added comments to complex or tricky code sections.
  • Updated any affected documentation.
  • Included tests that validate the fix or feature.
  • Checked that related changes are merged.

return nil, fmt.Errorf("failed to decode JWT payload: %w", err)
}

parsed, err := jwt.Parse(jwtPayload, jwt.WithVerify(false), jwt.WithToken(openid.New()))
Copy link
Member Author

Choose a reason for hiding this comment

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

Oop, this doesn't check audience, and should.

}

// Now that we've got the issuer, we can validate the token
keySet, err := m.getKeySet(parsed.Issuer())
Copy link
Member Author

Choose a reason for hiding this comment

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

There's a possible resource exhaustion attack here if you got a lot of OAuth issuers. We can probably narrow this down with an allow-list, e.g. from IdentityProvider API (get all the allowed issuers, drop the others early).

return m.jwks.Get(context.Background(), jwksUrl)
}

func getJWKSUrlForOpenId(issuer string) (string, error) {
Copy link
Member Author

Choose a reason for hiding this comment

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

You would think this function existed in github.com/lestrrat-go/jwx, but you'd be mistaken, except for in one test.

Comment on lines +128 to +133
// TODO: wire this in to IdentityProvider interface. Alternatively, have a different version
// for authzClient.Check that is IdentityProvider aware

if token.Issuer() == "https://token.actions.githubusercontent.com" {
return fmt.Sprintf("githubactions/%s", strings.ReplaceAll(token.Subject(), ":", "+"))
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Hahaha, this is gross. We should probably put all of this mangling / un-mangling in authzClient, rather than putting some inside and some outside (which is the current state of play).

Comment on lines 348 to +350
// Enable one or the other.
// This is temporary until we deprecate it completely in favor of email-based role assignments
if !flags.Bool(ctx, s.featureFlags, flags.UserManagement) {
if flags.Bool(ctx, s.featureFlags, flags.MachineAccounts) || !flags.Bool(ctx, s.featureFlags, flags.UserManagement) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Since machine accounts can't accept ToS or invitations, I'm extending the lifetime of this branch. Sorry-not-sorry!

Comment on lines 71 to 81
// TODO: this assumes that we store all users in the database, and that we don't
// need to namespace identify providers. We should revisit these assumptions.
//
if _, err := qtx.GetUserBySubject(ctx, identity.String()); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, util.UserVisibleError(codes.NotFound, "User not found")
if identity.Provider.String() == "" {
if _, err := qtx.GetUserBySubject(ctx, identity.String()); err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, util.UserVisibleError(codes.NotFound, "User not found")
}
return nil, status.Errorf(codes.Internal, "error getting user: %v", err)
}
return nil, status.Errorf(codes.Internal, "error getting user: %v", err)
}
Copy link
Member Author

Choose a reason for hiding this comment

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

Oh look:

TODO: ... We should revisit these assumptions.

Visited!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant