Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions internal/auth/authz/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/nais/api/internal/graph/ident"
"github.com/nais/api/internal/graph/pagination"
"github.com/nais/api/internal/slug"
"k8s.io/utils/ptr"
)

func ListRoles(ctx context.Context, page *pagination.Pagination) (*RoleConnection, error) {
Expand Down Expand Up @@ -73,6 +74,16 @@ func ForUser(ctx context.Context, userID uuid.UUID) ([]*Role, error) {
return ur.Roles, nil
}

func ForGitHubRepo(ctx context.Context, teamSlug slug.Slug) ([]*Role, error) {
role, err := GetRole(ctx, "GitHub repository")
if err != nil {
return nil, err
}

role.TargetTeamSlug = ptr.To(teamSlug)
return []*Role{role}, nil
}

func ForServiceAccount(ctx context.Context, serviceAccountID uuid.UUID) ([]*Role, error) {
sar, err := fromContext(ctx).serviceAccountRoles.Load(ctx, serviceAccountID)
if err != nil && errors.Is(err, pgx.ErrNoRows) {
Expand Down
119 changes: 119 additions & 0 deletions internal/auth/middleware/github_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package middleware

import (
"context"
"fmt"
"net/http"

"github.com/coreos/go-oidc/v3/oidc"
"github.com/google/uuid"
"github.com/nais/api/internal/auth/authz"
"github.com/nais/api/internal/github/repository"
"github.com/sirupsen/logrus"
)

type ghClaims struct {
// Ref string `json:"ref"`
Repository string `json:"repository"`
// RepositoryID string `json:"repository_id"`
// RepositoryOwner string `json:"repository_owner"`
// RepositoryOwnerID string `json:"repository_owner_id"`
// RunID string `json:"run_id"`
// RunNumber string `json:"run_number"`
// RunAttempt string `json:"run_attempt"`
// Actor string `json:"actor"`
// ActorID string `json:"actor_id"`
// Workflow string `json:"workflow"`
// HeadRef string `json:"head_ref"`
// BaseRef string `json:"base_ref"`
// EventName string `json:"event_name"`
// RefType string `json:"ref_type"`
// Environment string `json:"environment"`
// JobWorkflowRef string `json:"job_workflow_ref"`
}

func GitHubOIDC(ctx context.Context, log logrus.FieldLogger) func(next http.Handler) http.Handler {
provider, err := oidc.NewProvider(ctx, "https://token.actions.githubusercontent.com")
if err != nil {
log.WithError(err).Error("failed to initialize GitHub OIDC provider. Will not support GitHub OIDC authentication")
return func(sub http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
sub.ServeHTTP(w, r)
})
}
}

verifier := provider.Verifier(&oidc.Config{
ClientID: "api.nais.io",
})

return func(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()

token := r.Header.Get("Authorization")
if token == "" {
next.ServeHTTP(w, r)
return
}

idToken, err := verifier.Verify(r.Context(), token)
if err != nil {
next.ServeHTTP(w, r)
return
}

claims := &ghClaims{}
if err := idToken.Claims(claims); err != nil {
log.WithError(err).Debug("failed to parse claims from token")
next.ServeHTTP(w, r)
return
}

repos, err := repository.GetByName(ctx, claims.Repository)
if err != nil {
log.WithError(err).Debug("failed to get repository from token claims")
next.ServeHTTP(w, r)
return
}

if len(repos) == 0 {
log.WithField("repository", claims.Repository).Debug("no repository found matching token claims")
next.ServeHTTP(w, r)
return
}

roles := []*authz.Role{}
for _, repo := range repos {
repoRoles, err := authz.ForGitHubRepo(ctx, repo.TeamSlug)
if err != nil {
log.WithError(err).Debug("failed to get roles for github repo")
next.ServeHTTP(w, r)
return
}
roles = append(roles, repoRoles...)
}

usr := &GitHubRepoActor{
RepositoryName: claims.Repository,
}

next.ServeHTTP(w, r.WithContext(authz.ContextWithActor(ctx, usr, roles)))
}
return http.HandlerFunc(fn)
}
}

type GitHubRepoActor struct {
RepositoryName string
}

func (g *GitHubRepoActor) GetID() uuid.UUID { return uuid.Nil }

func (g *GitHubRepoActor) Identity() string {
return fmt.Sprintf("github-repo:%s", g.RepositoryName)
}

func (g *GitHubRepoActor) IsServiceAccount() bool { return true }

func (g *GitHubRepoActor) IsAdmin() bool { return false }
1 change: 1 addition & 0 deletions internal/cmd/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ type Fakes struct {
WithFakePrometheus bool `env:"WITH_FAKE_PROMETHEUS"`
WithFakeCostClient bool `env:"WITH_FAKE_COST_CLIENT"`
WithFakePriceClient bool `env:"WITH_FAKE_PRICE_CLIENT"`
WithSkipGHOIDC bool `env:"WITH_SKIP_GH_OIDC"`
}

func (f Fakes) Inform(log logrus.FieldLogger) {
Expand Down
5 changes: 5 additions & 0 deletions internal/cmd/api/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,11 @@ func runHttpServer(
middlewares = append(middlewares, jwtMiddleware)
}

if !fakes.WithSkipGHOIDC {
log.Warn("Skipping GitHub OIDC authentication")
middlewares = append(middlewares, middleware.GitHubOIDC(ctx, log))
}

middlewares = append(
middlewares,
middleware.ApiKeyAuthentication(),
Expand Down
20 changes: 20 additions & 0 deletions internal/database/migrations/0051_github_repo_role.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
-- +goose Up
INSERT INTO
roles (name, description)
VALUES
(
'GitHub repository',
'Granted to repositories linked to a team.'
)
;

INSERT INTO
role_authorizations (role_name, authorization_name)
VALUES
('GitHub repository', 'valkeys:create'),
('GitHub repository', 'valkeys:delete'),
('GitHub repository', 'valkeys:update'),
('GitHub repository', 'opensearches:create'),
('GitHub repository', 'opensearches:delete'),
('GitHub repository', 'opensearches:update')
;
12 changes: 12 additions & 0 deletions internal/github/repository/queries.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,15 @@ func RemoveFromTeam(ctx context.Context, input RemoveRepositoryFromTeamInput) er
})
})
}

func GetByName(ctx context.Context, name string) ([]*Repository, error) {
repos, err := db(ctx).GetByName(ctx, name)
if err != nil {
return nil, err
}
ret := make([]*Repository, 0, len(repos))
for _, r := range repos {
ret = append(ret, toGraphRepository(r))
}
return ret, nil
}
11 changes: 11 additions & 0 deletions internal/github/repository/queries/repository.sql
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,14 @@ WHERE
team_slug = @team_slug
AND github_repository = @github_repository
;

-- name: GetByName :many
SELECT
*
FROM
team_repositories
WHERE
github_repository = @github_repository
ORDER BY
team_slug ASC
;
1 change: 1 addition & 0 deletions internal/github/repository/repositorysql/querier.go

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

31 changes: 31 additions & 0 deletions internal/github/repository/repositorysql/repository.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/integration/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ func newGQLRunner(
WithFakePrometheus: true,
WithFakeCostClient: true,
WithFakePriceClient: true,
WithSkipGHOIDC: true,
},
watchers,
watcherMgr,
Expand Down