From ccc05b5d7fc3768dda614d1e91da442ff4d32869 Mon Sep 17 00:00:00 2001 From: Diego Caspi Date: Thu, 12 Dec 2024 16:57:17 +0100 Subject: [PATCH 01/23] feat(gitprovider): add Azure DevOps support and update provider configurations - Introduced Azure DevOps as a supported Git provider in the application. - Updated configuration schemas to include 'azure' in the list of supported providers. - Added Azure-specific provider implementation with functionality for creating and managing pull requests. - Included tests for Azure repository URL parsing. This enhancement allows users to interact with Azure DevOps repositories alongside existing GitHub and GitLab support. Signed-off-by: Diego Caspi Signed-off-by: Diego Caspi --- go.mod | 1 + go.sum | 3 + internal/directives/git_pr_opener.go | 1 + .../schemas/git-open-pr-config.json | 4 +- .../schemas/git-wait-for-pr-config.json | 4 +- internal/gitprovider/azure/azure.go | 234 ++++++++++++++++++ internal/gitprovider/azure/azure_test.go | 93 +++++++ ui/src/gen/directives/git-open-pr-config.json | 5 +- .../directives/git-wait-for-pr-config.json | 5 +- 9 files changed, 342 insertions(+), 8 deletions(-) create mode 100644 internal/gitprovider/azure/azure.go create mode 100644 internal/gitprovider/azure/azure_test.go diff --git a/go.mod b/go.mod index 777a3edb1..8850e41b1 100644 --- a/go.mod +++ b/go.mod @@ -111,6 +111,7 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.9 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect diff --git a/go.sum b/go.sum index fb09c5bf1..3992aa170 100644 --- a/go.sum +++ b/go.sum @@ -271,6 +271,7 @@ github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -373,6 +374,8 @@ github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxU github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0 h1:mmJCWLe63QvybxhW1iBmQWEaCKdc4SKgALfTNZ+OphU= +github.com/microsoft/azure-devops-go-api/azuredevops/v7 v7.1.0/go.mod h1:mDunUZ1IUJdJIRHvFb+LPBUtxe3AYB5MI6BMXNg8194= github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/migueleliasweb/go-github-mock v1.0.1 h1:amLEECVny28RCD1ElALUpQxrAimamznkg9rN2O7t934= diff --git a/internal/directives/git_pr_opener.go b/internal/directives/git_pr_opener.go index 24259edfc..bcb080dd1 100644 --- a/internal/directives/git_pr_opener.go +++ b/internal/directives/git_pr_opener.go @@ -14,6 +14,7 @@ import ( "github.com/akuity/kargo/internal/credentials" "github.com/akuity/kargo/internal/gitprovider" + _ "github.com/akuity/kargo/internal/gitprovider/azure" // Azure provider registration _ "github.com/akuity/kargo/internal/gitprovider/github" // GitHub provider registration _ "github.com/akuity/kargo/internal/gitprovider/gitlab" // GitLab provider registration ) diff --git a/internal/directives/schemas/git-open-pr-config.json b/internal/directives/schemas/git-open-pr-config.json index 6f0a97d07..6438c32fe 100644 --- a/internal/directives/schemas/git-open-pr-config.json +++ b/internal/directives/schemas/git-open-pr-config.json @@ -15,8 +15,8 @@ }, "provider": { "type": "string", - "description": "The name of the Git provider to use. Currently only 'github' and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly specified.", - "enum": ["github", "gitlab"] + "description": "The name of the Git provider to use. Currently only 'github', 'gitlab' and 'azure' are supported. Kargo will try to infer the provider if it is not explicitly specified.", + "enum": ["github", "gitlab", "azure"] }, "repoURL": { "type": "string", diff --git a/internal/directives/schemas/git-wait-for-pr-config.json b/internal/directives/schemas/git-wait-for-pr-config.json index dd7a711fa..59f2c0899 100644 --- a/internal/directives/schemas/git-wait-for-pr-config.json +++ b/internal/directives/schemas/git-wait-for-pr-config.json @@ -11,8 +11,8 @@ }, "provider": { "type": "string", - "description": "The name of the Git provider to use. Currently only 'github' and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly specified.", - "enum": ["github", "gitlab"] + "description": "The name of the Git provider to use. Currently only 'github', 'gitlab' and 'azure' are supported. Kargo will try to infer the provider if it is not explicitly specified.", + "enum": ["github", "gitlab", "azure"] }, "prNumber": { "type": "number", diff --git a/internal/gitprovider/azure/azure.go b/internal/gitprovider/azure/azure.go new file mode 100644 index 000000000..d7e3748be --- /dev/null +++ b/internal/gitprovider/azure/azure.go @@ -0,0 +1,234 @@ +package azure + +import ( + "context" + "fmt" + "net/url" + "slices" + "strings" + + "github.com/akuity/kargo/internal/git" + "github.com/akuity/kargo/internal/gitprovider" + "k8s.io/utils/ptr" + + "github.com/microsoft/azure-devops-go-api/azuredevops/v7" + adogit "github.com/microsoft/azure-devops-go-api/azuredevops/v7/git" +) + +const ProviderName = "azure" + +// Azure DevOps URLs can be of two different forms: +// +// - https://dev.azure.com/org/project/_git/repo +// - https://org.visualstudio.com/project/_git/repo +// +// We support both forms. +var providerSuffixes = []string{"dev.azure.com", "visualstudio.com"} + +var registration = gitprovider.Registration{ + Predicate: func(repoURL string) bool { + u, err := url.Parse(repoURL) + if err != nil { + return false + } + return slices.ContainsFunc(providerSuffixes, func(suffix string) bool { + return strings.HasSuffix(u.Host, suffix) + }) + }, + NewProvider: func( + repoURL string, + opts *gitprovider.Options, + ) (gitprovider.Interface, error) { + return NewProvider(repoURL, opts) + }, +} + +func init() { + gitprovider.Register(ProviderName, registration) +} + +type provider struct { + org string + project string + repo string + connection *azuredevops.Connection +} + +// NewProvider returns an Azure DevOps-based implementation of gitprovider.Interface. +func NewProvider( + repoURL string, + opts *gitprovider.Options, +) (gitprovider.Interface, error) { + if opts == nil || opts.Token == "" { + return nil, fmt.Errorf("options are required for Azure DevOps provider") + } + org, project, repo, err := parseRepoURL(repoURL) + if err != nil { + return nil, fmt.Errorf("error creating Azure DevOps provider: %w", err) + } + organizationUrl := fmt.Sprintf("https://dev.azure.com/%s", org) + connection := azuredevops.NewPatConnection(organizationUrl, opts.Token) + + return &provider{ + org: org, + project: project, + repo: repo, + connection: connection, + }, nil +} + +// CreatePullRequest implements gitprovider.Interface. +func (p *provider) CreatePullRequest( + ctx context.Context, + opts *gitprovider.CreatePullRequestOpts, +) (*gitprovider.PullRequest, error) { + gitClient, err := adogit.NewClient(ctx, p.connection) + if err != nil { + return nil, err + } + repository, err := gitClient.GetRepository(ctx, adogit.GetRepositoryArgs{ + Project: &p.project, + RepositoryId: &p.repo, + }) + if err != nil { + return nil, err + } + repoID := ptr.To(repository.Id.String()) + sourceRefName := ptr.To(fmt.Sprintf("refs/heads/%s", opts.Head)) + targetRefName := ptr.To(fmt.Sprintf("refs/heads/%s", opts.Base)) + adoPR, err := gitClient.CreatePullRequest(ctx, adogit.CreatePullRequestArgs{ + Project: &p.project, + RepositoryId: repoID, + GitPullRequestToCreate: &adogit.GitPullRequest{ + Title: &opts.Title, + Description: &opts.Description, + SourceRefName: sourceRefName, + TargetRefName: targetRefName, + }, + }) + if err != nil { + return nil, fmt.Errorf("error creating pull request from %q to %q: %w", opts.Head, opts.Base, err) + } + pr, err := convertADOPullRequest(adoPR) + if err != nil { + return nil, fmt.Errorf("error converting pull request %d: %w", adoPR.PullRequestId, err) + } + return pr, nil +} + +// GetPullRequest implements gitprovider.Interface. +func (p *provider) GetPullRequest( + ctx context.Context, + id int64, +) (*gitprovider.PullRequest, error) { + gitClient, err := adogit.NewClient(ctx, p.connection) + if err != nil { + return nil, err + } + adoPR, err := gitClient.GetPullRequest(ctx, adogit.GetPullRequestArgs{ + Project: &p.project, + RepositoryId: &p.repo, + PullRequestId: ptr.To(int(id)), + }) + if err != nil { + return nil, err + } + pr, err := convertADOPullRequest(adoPR) + if err != nil { + return nil, fmt.Errorf("error converting pull request %d: %w", id, err) + } + return pr, nil +} + +// ListPullRequests implements gitprovider.Interface. +func (p *provider) ListPullRequests( + ctx context.Context, + opts *gitprovider.ListPullRequestOptions, +) ([]gitprovider.PullRequest, error) { + gitClient, err := adogit.NewClient(ctx, p.connection) + if err != nil { + return nil, err + } + adoPRs, err := gitClient.GetPullRequests(ctx, adogit.GetPullRequestsArgs{ + Project: &p.project, + RepositoryId: &p.repo, + SearchCriteria: &adogit.GitPullRequestSearchCriteria{ + Status: ptr.To(mapADOPrState(opts.State)), + SourceRefName: ptr.To(opts.HeadBranch), + TargetRefName: ptr.To(opts.BaseBranch), + }, + }) + if err != nil { + return nil, err + } + + pts := []gitprovider.PullRequest{} + for _, adoPR := range *adoPRs { + pr, err := convertADOPullRequest(&adoPR) + if err != nil { + return nil, fmt.Errorf("error converting pull request %d: %w", adoPR.PullRequestId, err) + } + pts = append(pts, *pr) + } + return pts, nil +} + +// mapADOPrState maps a gitprovider.PullRequestState to an adogit.PullRequestStatus. +func mapADOPrState(state gitprovider.PullRequestState) adogit.PullRequestStatus { + switch state { + case gitprovider.PullRequestStateOpen: + return adogit.PullRequestStatusValues.Active + case gitprovider.PullRequestStateClosed: + return adogit.PullRequestStatusValues.Completed + } + return adogit.PullRequestStatusValues.All +} + +// convertADOPullRequest converts an adogit.GitPullRequest to a gitprovider.PullRequest. +func convertADOPullRequest(pr *adogit.GitPullRequest) (*gitprovider.PullRequest, error) { + if pr.LastMergeSourceCommit == nil { + return nil, fmt.Errorf("no last merge source commit found for pull request %d", ptr.Deref(pr.PullRequestId, 0)) + } + mergeCommit := ptr.Deref(pr.LastMergeCommit, adogit.GitCommitRef{}) + return &gitprovider.PullRequest{ + Number: int64(ptr.Deref(pr.PullRequestId, 0)), + URL: ptr.Deref(pr.Url, ""), + Open: ptr.Deref(pr.Status, "notSet") == "active", + Merged: ptr.Deref(pr.Status, "notSet") == "completed", + MergeCommitSHA: ptr.Deref(mergeCommit.CommitId, ""), + Object: pr, + HeadSHA: ptr.Deref(pr.LastMergeSourceCommit.CommitId, ""), + }, nil +} + +func parseRepoURL(repoURL string) (string, string, string, error) { + u, err := url.Parse(git.NormalizeURL(repoURL)) + if err != nil { + return "", "", "", fmt.Errorf("error parsing Azure DevOps repository URL %q: %w", repoURL, err) + } + if u.Host == "dev.azure.com" { + return parseModernRepoURL(u) + } else if strings.HasSuffix(u.Host, ".visualstudio.com") { + return parseLegacyRepoURL(u) + } + return "", "", "", fmt.Errorf("unsupported host %q", u.Host) +} + +// parseModernRepoURL parses a modern Azure DevOps repository URL. example: https://dev.azure.com/org/project/_git/repo +func parseModernRepoURL(u *url.URL) (string, string, string, error) { + parts := strings.Split(u.Path, "/") + if len(parts) != 5 { + return "", "", "", fmt.Errorf("could not extract repository organization, project, and name from URL %q", u) + } + return parts[1], parts[2], parts[4], nil +} + +// parseLegacyRepoURL parses a legacy Azure DevOps repository URL. example: https://org.visualstudio.com/project/_git/repo +func parseLegacyRepoURL(u *url.URL) (string, string, string, error) { + organization := strings.TrimSuffix(u.Host, ".visualstudio.com") + parts := strings.Split(u.Path, "/") + if len(parts) != 4 { + return "", "", "", fmt.Errorf("could not extract repository organization, project, and name from URL %q", u) + } + return organization, parts[1], parts[3], nil +} diff --git a/internal/gitprovider/azure/azure_test.go b/internal/gitprovider/azure/azure_test.go new file mode 100644 index 000000000..b081592dd --- /dev/null +++ b/internal/gitprovider/azure/azure_test.go @@ -0,0 +1,93 @@ +package azure + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseRepoURL(t *testing.T) { + testCases := []struct { + name string + url string + expectedOrg string + expectedProj string + expectedRepo string + errExpected bool + }{ + { + name: "invalid URL", + url: "not-a-url", + errExpected: true, + }, + { + name: "unsupported host", + url: "https://github.com/org/repo", + errExpected: true, + }, + { + name: "modern URL with missing parts", + url: "https://dev.azure.com/org", + errExpected: true, + }, + { + name: "legacy URL with missing parts", + url: "https://org.visualstudio.com", + errExpected: true, + }, + { + name: "modern URL format", + url: "https://dev.azure.com/myorg/myproject/_git/myrepo", + expectedOrg: "myorg", + expectedProj: "myproject", + expectedRepo: "myrepo", + errExpected: false, + }, + { + name: "modern URL format with .git suffix", + url: "https://dev.azure.com/myorg/myproject/_git/myrepo.git", + expectedOrg: "myorg", + expectedProj: "myproject", + expectedRepo: "myrepo", + errExpected: false, + }, + { + name: "legacy URL format", + url: "https://myorg.visualstudio.com/myproject/_git/myrepo", + expectedOrg: "myorg", + expectedProj: "myproject", + expectedRepo: "myrepo", + errExpected: false, + }, + { + name: "legacy URL format with .git suffix", + url: "https://myorg.visualstudio.com/myproject/_git/myrepo.git", + expectedOrg: "myorg", + expectedProj: "myproject", + expectedRepo: "myrepo", + errExpected: false, + }, + { + name: "modern URL format with dot in repo name", + url: "https://dev.azure.com/myorg/myproject/_git/my.repo", + expectedOrg: "myorg", + expectedProj: "myproject", + expectedRepo: "my.repo", + errExpected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + org, proj, repo, err := parseRepoURL(tc.url) + if tc.errExpected { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tc.expectedOrg, org) + require.Equal(t, tc.expectedProj, proj) + require.Equal(t, tc.expectedRepo, repo) + } + }) + } +} diff --git a/ui/src/gen/directives/git-open-pr-config.json b/ui/src/gen/directives/git-open-pr-config.json index 8f554bf2d..8a29fd2b7 100644 --- a/ui/src/gen/directives/git-open-pr-config.json +++ b/ui/src/gen/directives/git-open-pr-config.json @@ -14,10 +14,11 @@ }, "provider": { "type": "string", - "description": "The name of the Git provider to use. Currently only 'github' and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly specified.", + "description": "The name of the Git provider to use. Currently only 'github', 'gitlab' and 'azure' are supported. Kargo will try to infer the provider if it is not explicitly specified.", "enum": [ "github", - "gitlab" + "gitlab", + "azure" ] }, "repoURL": { diff --git a/ui/src/gen/directives/git-wait-for-pr-config.json b/ui/src/gen/directives/git-wait-for-pr-config.json index 5d4296707..0ff58f6d6 100644 --- a/ui/src/gen/directives/git-wait-for-pr-config.json +++ b/ui/src/gen/directives/git-wait-for-pr-config.json @@ -10,10 +10,11 @@ }, "provider": { "type": "string", - "description": "The name of the Git provider to use. Currently only 'github' and 'gitlab' are supported. Kargo will try to infer the provider if it is not explicitly specified.", + "description": "The name of the Git provider to use. Currently only 'github', 'gitlab' and 'azure' are supported. Kargo will try to infer the provider if it is not explicitly specified.", "enum": [ "github", - "gitlab" + "gitlab", + "azure" ] }, "prNumber": { From d5f698e677e70cced4fe4577bd59991f1abc11bc Mon Sep 17 00:00:00 2001 From: Mayursinh Sarvaiya Date: Wed, 11 Dec 2024 10:25:29 -0400 Subject: [PATCH 02/23] fix(auth): auto attach `offline_access` only if idp supports (#3117) Signed-off-by: Mayursinh Sarvaiya Signed-off-by: Diego Caspi --- .../auth/context/auth-context-provider.tsx | 2 +- ui/src/features/auth/context/auth-context.tsx | 2 +- .../features/auth/{utils.ts => jwt-utils.ts} | 8 -------- ui/src/features/auth/oidc-login.tsx | 15 ++++++-------- ui/src/features/auth/oidc-utils.ts | 20 +++++++++++++++++++ ui/src/features/auth/token-renew.tsx | 2 +- ui/src/features/common/layout/main-layout.tsx | 2 +- ui/src/pages/user.tsx | 2 +- 8 files changed, 31 insertions(+), 22 deletions(-) rename ui/src/features/auth/{utils.ts => jwt-utils.ts} (82%) create mode 100644 ui/src/features/auth/oidc-utils.ts diff --git a/ui/src/features/auth/context/auth-context-provider.tsx b/ui/src/features/auth/context/auth-context-provider.tsx index 643a1d27b..72c30048c 100644 --- a/ui/src/features/auth/context/auth-context-provider.tsx +++ b/ui/src/features/auth/context/auth-context-provider.tsx @@ -2,7 +2,7 @@ import React, { PropsWithChildren, useMemo } from 'react'; import { authTokenKey, refreshTokenKey } from '@ui/config/auth'; -import { extractInfoFromJWT, JWTInfo } from '../utils'; +import { extractInfoFromJWT, JWTInfo } from '../jwt-utils'; import { AuthContext, AuthContextType } from './auth-context'; diff --git a/ui/src/features/auth/context/auth-context.tsx b/ui/src/features/auth/context/auth-context.tsx index d08787c68..8b02d8171 100644 --- a/ui/src/features/auth/context/auth-context.tsx +++ b/ui/src/features/auth/context/auth-context.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { JWTInfo } from '../utils'; +import { JWTInfo } from '../jwt-utils'; export interface AuthContextType { isLoggedIn: boolean; diff --git a/ui/src/features/auth/utils.ts b/ui/src/features/auth/jwt-utils.ts similarity index 82% rename from ui/src/features/auth/utils.ts rename to ui/src/features/auth/jwt-utils.ts index f22d74416..f04093923 100644 --- a/ui/src/features/auth/utils.ts +++ b/ui/src/features/auth/jwt-utils.ts @@ -1,5 +1,3 @@ -import { ClientAuth } from 'oauth4webapi'; - // https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims export type JWTInfo = { sub: string; @@ -40,9 +38,3 @@ export const getUserEmail = (user?: JWTInfo | null) => { return meta; }; - -export const oidcClientAuth: ClientAuth = () => { - // equivalent function for token_endpoint_auth_method: 'none' -}; - -export const shouldAllowIdpHttpRequest = () => __UI_VERSION__ === 'development'; diff --git a/ui/src/features/auth/oidc-login.tsx b/ui/src/features/auth/oidc-login.tsx index e8815cd67..f924e52bf 100644 --- a/ui/src/features/auth/oidc-login.tsx +++ b/ui/src/features/auth/oidc-login.tsx @@ -18,7 +18,11 @@ import { useLocation } from 'react-router-dom'; import { OIDCConfig } from '@ui/gen/service/v1alpha1/service_pb'; import { useAuthContext } from './context/use-auth-context'; -import { oidcClientAuth, shouldAllowIdpHttpRequest as shouldAllowHttpRequest } from './utils'; +import { + getOIDCScopes, + oidcClientAuth, + shouldAllowIdpHttpRequest as shouldAllowHttpRequest +} from './oidc-utils'; const codeVerifierKey = 'PKCE_code_verifier'; @@ -92,14 +96,7 @@ export const OIDCLogin = ({ oidcConfig }: Props) => { url.searchParams.set('code_challenge_method', 'S256'); url.searchParams.set('redirect_uri', redirectURI); url.searchParams.set('response_type', 'code'); - url.searchParams.set( - 'scope', - [ - ...oidcConfig.scopes, - // Add offline_access scope if it does not exist - ...(oidcConfig.scopes.includes('offline_access') ? [] : ['offline_access']) - ].join(' ') - ); + url.searchParams.set('scope', getOIDCScopes(oidcConfig, as).join(' ')); window.location.replace(url.toString()); }; diff --git a/ui/src/features/auth/oidc-utils.ts b/ui/src/features/auth/oidc-utils.ts new file mode 100644 index 000000000..c1ea81c32 --- /dev/null +++ b/ui/src/features/auth/oidc-utils.ts @@ -0,0 +1,20 @@ +import { AuthorizationServer, ClientAuth } from 'oauth4webapi'; + +import { OIDCConfig } from '@ui/gen/service/v1alpha1/service_pb'; + +export const oidcClientAuth: ClientAuth = () => { + // equivalent function for token_endpoint_auth_method: 'none' +}; + +export const shouldAllowIdpHttpRequest = () => __UI_VERSION__ === 'development'; + +export const getOIDCScopes = (userOIDCConfig: OIDCConfig, idp: AuthorizationServer) => { + const scopes = [...userOIDCConfig.scopes]; + + // add offline_access scope automatically only if it is supported by IDP + if (!scopes.includes('offline_access') && idp.scopes_supported?.includes('offline_access')) { + scopes.push('offline_access'); + } + + return scopes; +}; diff --git a/ui/src/features/auth/token-renew.tsx b/ui/src/features/auth/token-renew.tsx index 96ac2ae5e..31fe7ae8f 100644 --- a/ui/src/features/auth/token-renew.tsx +++ b/ui/src/features/auth/token-renew.tsx @@ -18,7 +18,7 @@ import { getPublicConfig } from '@ui/gen/service/v1alpha1/service-KargoService_c import { LoadingState } from '../common'; import { useAuthContext } from './context/use-auth-context'; -import { oidcClientAuth, shouldAllowIdpHttpRequest as shouldAllowHttpRequest } from './utils'; +import { oidcClientAuth, shouldAllowIdpHttpRequest as shouldAllowHttpRequest } from './oidc-utils'; export const TokenRenew = () => { const navigate = useNavigate(); diff --git a/ui/src/features/common/layout/main-layout.tsx b/ui/src/features/common/layout/main-layout.tsx index 3f201f7bb..e307673c3 100644 --- a/ui/src/features/common/layout/main-layout.tsx +++ b/ui/src/features/common/layout/main-layout.tsx @@ -13,7 +13,7 @@ import { Outlet } from 'react-router-dom'; import { paths } from '@ui/config/paths'; import { useAuthContext } from '@ui/features/auth/context/use-auth-context'; -import { isJWTDirty } from '@ui/features/auth/utils'; +import { isJWTDirty } from '@ui/features/auth/jwt-utils'; import { KargoLogo } from '@ui/features/common/logo/logo'; import * as styles from './main-layout.module.less'; diff --git a/ui/src/pages/user.tsx b/ui/src/pages/user.tsx index 6201f874a..378dbcccc 100644 --- a/ui/src/pages/user.tsx +++ b/ui/src/pages/user.tsx @@ -7,7 +7,7 @@ import { Navigate } from 'react-router-dom'; import { redirectToQueryParam } from '@ui/config/auth'; import { paths } from '@ui/config/paths'; import { useAuthContext } from '@ui/features/auth/context/use-auth-context'; -import { isAdmin, isJWTDirty } from '@ui/features/auth/utils'; +import { isAdmin, isJWTDirty } from '@ui/features/auth/jwt-utils'; import { PageTitle } from '@ui/features/common'; export const User = () => { From cfcf3cf7594d8df48e024895619c05f4e3704d3d Mon Sep 17 00:00:00 2001 From: Mayursinh Sarvaiya Date: Wed, 11 Dec 2024 13:52:42 -0400 Subject: [PATCH 03/23] fix(auth): token renew failure (#3122) Signed-off-by: Mayursinh Sarvaiya Signed-off-by: Diego Caspi --- ui/src/features/auth/token-renew.tsx | 34 ++++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/ui/src/features/auth/token-renew.tsx b/ui/src/features/auth/token-renew.tsx index 31fe7ae8f..571d069ca 100644 --- a/ui/src/features/auth/token-renew.tsx +++ b/ui/src/features/auth/token-renew.tsx @@ -79,25 +79,35 @@ export const TokenRenew = () => { } (async () => { - const response = await refreshTokenGrantRequest(as, client, oidcClientAuth, refreshToken, { - [allowInsecureRequests]: shouldAllowHttpRequest(), - additionalParameters: [['client_id', client.client_id]] - }); - - const result = await processRefreshTokenResponse(as, client, response); + try { + const response = await refreshTokenGrantRequest(as, client, oidcClientAuth, refreshToken, { + [allowInsecureRequests]: shouldAllowHttpRequest(), + additionalParameters: [['client_id', client.client_id]] + }); - if (!result.id_token) { + const result = await processRefreshTokenResponse(as, client, response); + + if (!result.id_token) { + notification.error({ + message: 'OIDC: Proccess Authorization Code Grant Response error', + placement: 'bottomRight' + }); + logout(); + navigate(paths.login); + return; + } + + onLogin(result.id_token, result.refresh_token); + navigate(searchParams.get(redirectToQueryParam) || paths.home); + } catch (err) { notification.error({ - message: 'OIDC: Proccess Authorization Code Grant Response error', + message: `OIDC: ${JSON.stringify(err)}`, placement: 'bottomRight' }); + logout(); navigate(paths.login); - return; } - - onLogin(result.id_token, result.refresh_token); - navigate(searchParams.get(redirectToQueryParam) || paths.home); })(); }, [as, client]); From 4561dcf9c0cc23bf4547178d91a77d6424548fc8 Mon Sep 17 00:00:00 2001 From: Hidde Beydals Date: Thu, 12 Dec 2024 00:00:06 +0100 Subject: [PATCH 04/23] chore(chart): start using Kubernetes' builtin gRPC probe (#3041) Signed-off-by: Hidde Beydals Signed-off-by: Diego Caspi --- charts/kargo/templates/api/deployment.yaml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/charts/kargo/templates/api/deployment.yaml b/charts/kargo/templates/api/deployment.yaml index 60735249e..b0c744d65 100644 --- a/charts/kargo/templates/api/deployment.yaml +++ b/charts/kargo/templates/api/deployment.yaml @@ -87,24 +87,30 @@ spec: protocol: TCP {{- if .Values.api.probes.enabled }} livenessProbe: + {{- if .Values.api.tls.enabled }} exec: command: - /usr/local/bin/grpc_health_probe - -addr=:8080 -{{- if .Values.api.tls.enabled }} - -tls - -tls-no-verify -{{- end }} + {{- else }} + grpc: + port: 8080 + {{- end }} initialDelaySeconds: 10 readinessProbe: + {{- if .Values.api.tls.enabled }} exec: command: - /usr/local/bin/grpc_health_probe - -addr=:8080 -{{- if .Values.api.tls.enabled }} - -tls - -tls-no-verify -{{- end }} + {{- else }} + grpc: + port: 8080 + {{- end }} initialDelaySeconds: 5 {{- end }} {{- if or .Values.kubeconfigSecrets.kargo (and .Values.api.oidc.enabled .Values.api.oidc.dex.enabled) .Values.api.tls.enabled .Values.api.cabundle.configMapName .Values.api.cabundle.secretName }} From 6b5091c71161d61edd1ffa96c254f4a1abea3cff Mon Sep 17 00:00:00 2001 From: Faeka Ansari Date: Thu, 12 Dec 2024 19:53:41 +0530 Subject: [PATCH 05/23] docs: manual rolebindings for global namespaces (#2934) Signed-off-by: Faeka Ansari Signed-off-by: Diego Caspi --- .../20-managing-credentials.md | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/docs/docs/30-how-to-guides/20-managing-credentials.md b/docs/docs/30-how-to-guides/20-managing-credentials.md index 3cfb33836..65601e9de 100644 --- a/docs/docs/30-how-to-guides/20-managing-credentials.md +++ b/docs/docs/30-how-to-guides/20-managing-credentials.md @@ -5,7 +5,7 @@ sidebar_label: Managing Credentials # Managing Credentials -To manage the progression of freight from stage to stage, Kargo will often +To manage the progression of Freight from Stage to Stage, Kargo will often require read/write permissions on private GitOps repositories and read-only permissions on private container image and/or Helm chart repositories. @@ -106,6 +106,50 @@ Refer to [the advanced section of the installation guide](./10-installing-kargo.md#advanced-installation) for more details. +:::note +Operators must manually ensure Kargo controllers receive read-only access +to `Secret`s in the designated namespaces. For example, if `kargo-global-creds` +is designated as a global credentials namespace, the following `RoleBinding` +should be created within that `Namespace`: + +```yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kargo-controller-read-secrets + namespace: kargo-global-creds +subjects: + - kind: ServiceAccount + name: kargo-controller + namespace: kargo +roleRef: + kind: ClusterRole + name: kargo-controller-read-secrets + apiGroup: rbac.authorization.k8s.io +``` +::: + +:::info +By default, Kargo controllers lack cluster-wide permissions on `Secret` +resources. Instead, the Kargo _management controller_ dynamically expands +controller access to `Secret`s on a namespace-by-namespace basis as new +`Project`s are created. + +_It is because this process does not account for "global" credential namespaces +that these bindings must be created manually by an operator._ +::: + +:::warning +Setting `controller.serviceAccount.clusterWideSecretReadingEnabled` setting to +`true` during Kargo installation will grant Kargo controllers cluster-wide read +permission on `Secret` resources. + +__This is highly discouraged, especially in sharded environments where this +permission would have the undesirable effect of granting remote Kargo +controllers read permissions on all `Secret`s throughout the Kargo control +plane's cluster -- including `Secret`s having nothing to do with Kargo.__ +::: + :::note Any matching credentials (exact match _or_ pattern match) found in a project's own `Namespace` take precedence over those found in any global credentials @@ -125,12 +169,6 @@ searched in lexical order by name. Only after no exact match _and_ no pattern match is found in one global credentials `Namespace` does Kargo search the next. ::: -:::caution -It is important to understand the security implications of this feature. Any -credentials stored in a global credentials `Namespace` will be available to -_all_ Kargo projects. -::: - ## Managing Credentials with the CLI The Kargo CLI can be used to manage credentials in a project's `Namespace.` From 34cf92af0ab1df602729844357fb065844dcb7b7 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:51:25 -0500 Subject: [PATCH 06/23] chore(deps): bump nanoid from 3.3.6 to 3.3.8 in /docs/plugins/gtag (#3130) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Diego Caspi --- docs/plugins/gtag/pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/plugins/gtag/pnpm-lock.yaml b/docs/plugins/gtag/pnpm-lock.yaml index b2c87350c..2bd88100b 100644 --- a/docs/plugins/gtag/pnpm-lock.yaml +++ b/docs/plugins/gtag/pnpm-lock.yaml @@ -2912,8 +2912,8 @@ packages: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true - nanoid@3.3.6: - resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -7858,7 +7858,7 @@ snapshots: dns-packet: 5.4.0 thunky: 1.1.0 - nanoid@3.3.6: {} + nanoid@3.3.8: {} negotiator@0.6.3: {} @@ -8270,7 +8270,7 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.6 + nanoid: 3.3.8 picocolors: 1.0.0 source-map-js: 1.0.2 From 7ab935e3a8c7d69e481dd1eda764c290dd9c6d43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:10:23 -0500 Subject: [PATCH 07/23] chore(deps): bump nanoid from 3.3.7 to 3.3.8 in /docs (#3132) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Diego Caspi --- docs/pnpm-lock.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml index 84983258b..bacf30505 100644 --- a/docs/pnpm-lock.yaml +++ b/docs/pnpm-lock.yaml @@ -3234,8 +3234,8 @@ packages: resolution: {integrity: sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==} hasBin: true - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + nanoid@3.3.8: + resolution: {integrity: sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -9154,7 +9154,7 @@ snapshots: dns-packet: 5.6.1 thunky: 1.1.0 - nanoid@3.3.7: {} + nanoid@3.3.8: {} negotiator@0.6.3: {} @@ -9575,7 +9575,7 @@ snapshots: postcss@8.4.31: dependencies: - nanoid: 3.3.7 + nanoid: 3.3.8 picocolors: 1.0.0 source-map-js: 1.0.2 From da2c1370617e834e1014f1fd4ac2db2326779c06 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 22:10:59 +0000 Subject: [PATCH 08/23] chore(deps/tools): bump golang.org/x/crypto from 0.30.0 to 0.31.0 in /hack/tools (#3134) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Signed-off-by: Diego Caspi --- hack/tools/go.mod | 2 +- hack/tools/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hack/tools/go.mod b/hack/tools/go.mod index 79baa840d..0d5c10719 100644 --- a/hack/tools/go.mod +++ b/hack/tools/go.mod @@ -327,7 +327,7 @@ require ( go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect go.uber.org/zap/exp v0.3.0 // indirect - golang.org/x/crypto v0.30.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/exp/typeparams v0.0.0-20241108190413-2d47ceb2692f // indirect golang.org/x/mod v0.22.0 // indirect diff --git a/hack/tools/go.sum b/hack/tools/go.sum index d455558f4..8c66385dd 100644 --- a/hack/tools/go.sum +++ b/hack/tools/go.sum @@ -884,8 +884,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= -golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= -golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f h1:XdNn9LlyWAhLVp6P/i8QYBW+hlyhrhei9uErw2B5GJo= golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f/go.mod h1:D5SMRVC3C2/4+F/DB1wZsLRnSNimn2Sp/NPsCrsv8ak= From 3a3b1dbd93bc9789464d0d3e1518131359e7b251 Mon Sep 17 00:00:00 2001 From: Faeka Ansari Date: Sat, 14 Dec 2024 01:44:32 +0530 Subject: [PATCH 09/23] docs: add a resources page for Kargo related talks (#2935) Signed-off-by: Faeka Ansari Signed-off-by: Diego Caspi --- docs/docs/45-resources.md | 50 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/docs/45-resources.md diff --git a/docs/docs/45-resources.md b/docs/docs/45-resources.md new file mode 100644 index 000000000..6da6a651a --- /dev/null +++ b/docs/docs/45-resources.md @@ -0,0 +1,50 @@ +--- +description: Learn about Kargo through our talks from conferences +sidebar_label: Resources +--- + +# Resources + +Learn more about Kargo through talks and webinars presented at conferences like KubeCon, ArgoCon, GitOpsCon, etc! + +## Kargo GA Webinar 🎉 +By Kelsey Hightower & Jesse Suen + +
+
+