From 279a0e39fddb10daf342332560a189f8ad5d3fc8 Mon Sep 17 00:00:00 2001 From: dekiel Date: Fri, 10 Jan 2025 11:49:54 +0100 Subject: [PATCH 1/2] Added output structure to hold oidc token verifier output data. Added methods to set the values in output struct and method to write the value as json file. Added supporting cli flag, interface, mocks and tests. --- .mockery.yaml | 8 +- cmd/oidc-token-verifier/main.go | 86 +++++++++++- cmd/oidc-token-verifier/main_test.go | 129 ++++++++++++++++++ .../mocks/mock_TrustedIssuerProvider.go | 80 +++++++++++ .../oidc_token_verifier_suite_test.go | 13 ++ pkg/oidc/oidc.go | 3 +- 6 files changed, 315 insertions(+), 4 deletions(-) create mode 100644 cmd/oidc-token-verifier/main_test.go create mode 100644 cmd/oidc-token-verifier/mocks/mock_TrustedIssuerProvider.go create mode 100644 cmd/oidc-token-verifier/oidc_token_verifier_suite_test.go diff --git a/.mockery.yaml b/.mockery.yaml index c1bfcb203735..2432da096c70 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -9,4 +9,10 @@ packages: config: all: True dir: "{{.InterfaceDir}}/mocks" - outpkg: "{{.PackageName}}mocks" \ No newline at end of file + outpkg: "{{.PackageName}}mocks" + github.com/kyma-project/test-infra/cmd/oidc-token-verifier: + config: + dir: "{{.InterfaceDir}}/mocks" + outpkg: "{{.PackageName}}mocks" + interfaces: + TrustedIssuerProvider: \ No newline at end of file diff --git a/cmd/oidc-token-verifier/main.go b/cmd/oidc-token-verifier/main.go index 9f8c9a80917b..55649a862476 100644 --- a/cmd/oidc-token-verifier/main.go +++ b/cmd/oidc-token-verifier/main.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "fmt" "os" @@ -25,6 +26,7 @@ type options struct { trustedWorkflows []string debug bool oidcTokenExpirationTime int // OIDC token expiration time in minutes + outputPath string } var ( @@ -50,6 +52,7 @@ func NewRootCmd() *cobra.Command { rootCmd.PersistentFlags().StringVarP(&opts.clientID, "client-id", "c", "image-builder", "OIDC token client ID, this is used to verify the audience claim in the token. The value should be the same as the audience claim value in the token.") rootCmd.PersistentFlags().BoolVarP(&opts.debug, "debug", "d", false, "Enable debug mode") rootCmd.PersistentFlags().IntVarP(&opts.oidcTokenExpirationTime, "oidc-token-expiration-time", "e", 10, "OIDC token expiration time in minutes") + rootCmd.PersistentFlags().StringVarP(&opts.outputPath, "output-path", "o", "/oidc-verifier-output.json", "Path to the file where the output data will be saved") return rootCmd } @@ -74,6 +77,67 @@ func init() { rootCmd.AddCommand(verifyCmd) } +type TrustedIssuerProvider interface { + GetIssuer() tioidc.Issuer +} + +// output is a struct that holds the output values that are printed to the file. +// The data provided in this struct is relevant for the component that uses the OIDC token verifier. +// The output values are printed to the file in the json format. +type output struct { + GithubURL string + ClientID string +} + +// setGithubURLOutput sets the Github URL value to the output struct. +// The Github URL value is read from the TokenProcessor trusted issuer. +func (output *output) setGithubURLOutput(logger Logger, issuerProvider TrustedIssuerProvider) error { + var githubURL string + + if githubURL = issuerProvider.GetIssuer().GetGithubURL(); githubURL == "" { + return fmt.Errorf("github URL not found in the tokenProcessor trusted issuer: %s", issuerProvider.GetIssuer()) + } + + output.GithubURL = githubURL + + logger.Debugw("Set output Github URL value", "githubURL", output.GithubURL) + + return nil +} + +// setClientIDOutput sets the client ID value to the output struct. +// The client ID value is read from the TokenProcessor trusted issuer. +func (output *output) setClientIDOutput(logger Logger, issuerProvider TrustedIssuerProvider) error { + var clientID string + if clientID = issuerProvider.GetIssuer().ClientID; clientID == "" { + return fmt.Errorf("client ID not found in the tokenProcessor trusted issuer: %s", issuerProvider.GetIssuer()) + } + + output.ClientID = clientID + + logger.Debugw("Set output client ID value", "clientID", output.ClientID) + + return nil +} + +// writeOutputFile writes the output values to the json file. +// The file path is specified by the --output-path flag. +func (output *output) writeOutputFile(logger Logger, path string) error { + outputFile, err := os.Create(path) + if err != nil { + return err + } + + err = json.NewEncoder(outputFile).Encode(output) + if err != nil { + return err + } + + logger.Debugw("Output values written to the file", "path", path, "output", output) + + return nil +} + // isTokenProvided checks if the token flag is set. // If not, check if AUTHORIZATION environment variable is set. // If neither is set, return an error. @@ -140,7 +204,6 @@ func (opts *options) verifyToken() error { return err } logger.Infow("Token processor created for trusted issuer", "issuer", tokenProcessor.Issuer()) - fmt.Printf("GITHUB_URL=%s\n", tokenProcessor.GetIssuer().GetGithubURL()) ctx := context.Background() // Create a new provider using OIDC discovery to get the public keys. @@ -176,9 +239,28 @@ func (opts *options) verifyToken() error { if err != nil { return err } + logger.Infow("Token claims expectations verified successfully") logger.Infow("All token checks passed successfully") + outputData := output{} + err = outputData.setGithubURLOutput(logger, &tokenProcessor) + if err != nil { + return err + } + + err = outputData.setClientIDOutput(logger, &tokenProcessor) + if err != nil { + return err + } + + err = outputData.writeOutputFile(logger, opts.outputPath) + if err != nil { + return err + } + + logger.Infow("Output data written to the file", "path", opts.outputPath) + return nil } @@ -186,4 +268,4 @@ func main() { if err := rootCmd.Execute(); err != nil { panic(err) } -} +} \ No newline at end of file diff --git a/cmd/oidc-token-verifier/main_test.go b/cmd/oidc-token-verifier/main_test.go new file mode 100644 index 000000000000..29891775ddbc --- /dev/null +++ b/cmd/oidc-token-verifier/main_test.go @@ -0,0 +1,129 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + + "github.com/kyma-project/test-infra/cmd/oidc-token-verifier/mocks" + tioidc "github.com/kyma-project/test-infra/pkg/oidc" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.uber.org/zap" +) + +var _ = Describe("Output", func() { + var ( + logger Logger + issuerProvider *mainmocks.MockTrustedIssuerProvider + issuer tioidc.Issuer + out output + ) + + BeforeEach(func() { + logger = zap.NewNop().Sugar() + issuerProvider = &mainmocks.MockTrustedIssuerProvider{} + issuer = tioidc.Issuer{ + Name: "test-issuer", + IssuerURL: "https://test-issuer.com", + JWKSURL: "https://test-issuer.com/jwks", + ExpectedJobWorkflowRef: "test-workflow", + GithubURL: "https://github-test.com", + ClientID: "test-client-id", + } + out = output{} + }) + + Describe("setGithubURLOutput", func() { + Context("when the Github URL is found in the tokenProcessor trusted issuer", func() { + It("should set the Github URL in the output struct", func() { + issuerProvider.On("GetIssuer").Return(issuer) + + err := out.setGithubURLOutput(logger, issuerProvider) + Expect(err).NotTo(HaveOccurred(), "Expected no error, but got: %v", err) + Expect(out.GithubURL).To(Equal(issuer.GithubURL), "Expected Github URL to be %s, but got %s", issuer.GithubURL, out.GithubURL) + }) + }) + + Context("when the Github URL is not found in the tokenProcessor trusted issuer", func() { + It("should return an error", func() { + issuer.GithubURL = "" + issuerProvider.On("GetIssuer").Return(issuer) + + err := out.setGithubURLOutput(logger, issuerProvider) + Expect(err).To(HaveOccurred(), "Expected an error, but got none") + Expect(err).To(MatchError(fmt.Errorf("github URL not found in the tokenProcessor trusted issuer: %s", issuerProvider.GetIssuer())), "Expected error message to be 'github URL not found in the tokenProcessor trusted issuer', but got %s", err) + Expect(out.GithubURL).To(BeEmpty(), "Expected Github URL to be empty, but got %v", out.GithubURL) + }) + }) + }) + + Describe("setClientIDOutput", func() { + Context("when the Client ID is found in the tokenProcessor trusted issuer", func() { + It("should set the Client ID in the output struct", func() { + issuerProvider.On("GetIssuer").Return(issuer) + + err := out.setClientIDOutput(logger, issuerProvider) + Expect(err).NotTo(HaveOccurred(), "Expected no error, but got: %v", err) + Expect(out.ClientID).To(Equal(issuer.ClientID), "Expected Client ID to be %s, but got %s", issuer.ClientID, out.ClientID) + }) + }) + + Context("when the Client ID is not found in the tokenProcessor trusted issuer", func() { + It("should return an error", func() { + issuer.ClientID = "" + issuerProvider.On("GetIssuer").Return(issuer) + + err := out.setClientIDOutput(logger, issuerProvider) + Expect(err).To(HaveOccurred(), "Expected an error, but got none") + Expect(err).To(MatchError(fmt.Errorf("client ID not found in the tokenProcessor trusted issuer: %s", issuerProvider.GetIssuer())), "Expected error message to be 'client ID not found in the tokenProcessor trusted issuer', but got %s", err) + Expect(out.ClientID).To(BeEmpty(), "Expected Client ID to be empty, but got %v", out.ClientID) + }) + }) + }) + + Describe("writeOutputFile", func() { + var filePath = "./output.json" + + BeforeEach(func() { + // Verify if the path exists and is writable + file, err := os.Create(filePath) + Expect(err).NotTo(HaveOccurred(), "Expected no error creating the file, but got: %v", err) + file.Close() + }) + + AfterEach(func() { + // Remove created artifacts + err := os.Remove(filePath) + Expect(err).NotTo(HaveOccurred(), "Expected no error removing the file, but got: %v", err) + }) + + Context("when the output file is successfully written", func() { + It("should write the output values to the json file", func() { + out.GithubURL = issuer.GithubURL + out.ClientID = issuer.ClientID + + err := out.writeOutputFile(logger, filePath) + Expect(err).NotTo(HaveOccurred(), "Expected no error, but got: %v", err) + + file, err := os.Open(filePath) + Expect(err).NotTo(HaveOccurred(), "Expected no error opening the file, but got: %v", err) + defer file.Close() + + var writtenOutput output + err = json.NewDecoder(file).Decode(&writtenOutput) + Expect(err).NotTo(HaveOccurred(), "Expected no error decoding the file, but got: %v", err) + Expect(writtenOutput).To(Equal(out), "Expected written output to be %v, but got %v", out, writtenOutput) + }) + }) + + Context("when there is an error creating the output file", func() { + It("should return an error", func() { + filePath := "/invalid-path/output.json" + + err := out.writeOutputFile(logger, filePath) + Expect(err).To(HaveOccurred(), "Expected an error, but got none") + }) + }) + }) +}) diff --git a/cmd/oidc-token-verifier/mocks/mock_TrustedIssuerProvider.go b/cmd/oidc-token-verifier/mocks/mock_TrustedIssuerProvider.go new file mode 100644 index 000000000000..eaabea9193f3 --- /dev/null +++ b/cmd/oidc-token-verifier/mocks/mock_TrustedIssuerProvider.go @@ -0,0 +1,80 @@ +// Code generated by mockery v2.46.3. DO NOT EDIT. + +package mainmocks + +import ( + oidc "github.com/kyma-project/test-infra/pkg/oidc" + mock "github.com/stretchr/testify/mock" +) + +// MockTrustedIssuerProvider is an autogenerated mock type for the TrustedIssuerProvider type +type MockTrustedIssuerProvider struct { + mock.Mock +} + +type MockTrustedIssuerProvider_Expecter struct { + mock *mock.Mock +} + +func (_m *MockTrustedIssuerProvider) EXPECT() *MockTrustedIssuerProvider_Expecter { + return &MockTrustedIssuerProvider_Expecter{mock: &_m.Mock} +} + +// GetIssuer provides a mock function with given fields: +func (_m *MockTrustedIssuerProvider) GetIssuer() oidc.Issuer { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetIssuer") + } + + var r0 oidc.Issuer + if rf, ok := ret.Get(0).(func() oidc.Issuer); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(oidc.Issuer) + } + + return r0 +} + +// MockTrustedIssuerProvider_GetIssuer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetIssuer' +type MockTrustedIssuerProvider_GetIssuer_Call struct { + *mock.Call +} + +// GetIssuer is a helper method to define mock.On call +func (_e *MockTrustedIssuerProvider_Expecter) GetIssuer() *MockTrustedIssuerProvider_GetIssuer_Call { + return &MockTrustedIssuerProvider_GetIssuer_Call{Call: _e.mock.On("GetIssuer")} +} + +func (_c *MockTrustedIssuerProvider_GetIssuer_Call) Run(run func()) *MockTrustedIssuerProvider_GetIssuer_Call { + _c.Call.Run(func(args mock.Arguments) { + run() + }) + return _c +} + +func (_c *MockTrustedIssuerProvider_GetIssuer_Call) Return(_a0 oidc.Issuer) *MockTrustedIssuerProvider_GetIssuer_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *MockTrustedIssuerProvider_GetIssuer_Call) RunAndReturn(run func() oidc.Issuer) *MockTrustedIssuerProvider_GetIssuer_Call { + _c.Call.Return(run) + return _c +} + +// NewMockTrustedIssuerProvider creates a new instance of MockTrustedIssuerProvider. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMockTrustedIssuerProvider(t interface { + mock.TestingT + Cleanup(func()) +}) *MockTrustedIssuerProvider { + mock := &MockTrustedIssuerProvider{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/cmd/oidc-token-verifier/oidc_token_verifier_suite_test.go b/cmd/oidc-token-verifier/oidc_token_verifier_suite_test.go new file mode 100644 index 000000000000..92c7ee7d8a5b --- /dev/null +++ b/cmd/oidc-token-verifier/oidc_token_verifier_suite_test.go @@ -0,0 +1,13 @@ +package main_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestOidcTokenVerifier(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "OidcTokenVerifier Suite") +} diff --git a/pkg/oidc/oidc.go b/pkg/oidc/oidc.go index 409045e1e230..de67b8dd4858 100644 --- a/pkg/oidc/oidc.go +++ b/pkg/oidc/oidc.go @@ -83,6 +83,7 @@ type Issuer struct { JWKSURL string `json:"jwks_url" yaml:"jwks_url"` ExpectedJobWorkflowRef string `json:"expected_job_workflow_ref" yaml:"expected_job_workflow_ref"` GithubURL string `json:"github_url" yaml:"github_url"` + ClientID string `json:"client_id" yaml:"client_id"` } func (i Issuer) GetGithubURL() string { @@ -486,4 +487,4 @@ func (tokenProcessor *TokenProcessor) ValidateClaims(claims ClaimsInterface, tok return fmt.Errorf("expecations validation failed: %w", err) } return nil -} +} \ No newline at end of file From 3b490c54488bc3f774dba90967e3e60059b570ca Mon Sep 17 00:00:00 2001 From: dekiel Date: Fri, 10 Jan 2025 15:00:51 +0100 Subject: [PATCH 2/2] Added json and yaml struct tags. --- cmd/oidc-token-verifier/main.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cmd/oidc-token-verifier/main.go b/cmd/oidc-token-verifier/main.go index 55649a862476..1fb034c6c28b 100644 --- a/cmd/oidc-token-verifier/main.go +++ b/cmd/oidc-token-verifier/main.go @@ -85,8 +85,8 @@ type TrustedIssuerProvider interface { // The data provided in this struct is relevant for the component that uses the OIDC token verifier. // The output values are printed to the file in the json format. type output struct { - GithubURL string - ClientID string + GithubURL string `json:"github_url" yaml:"github_url"` + ClientID string `json:"client_id" yaml:"client_id"` } // setGithubURLOutput sets the Github URL value to the output struct. @@ -268,4 +268,4 @@ func main() { if err := rootCmd.Execute(); err != nil { panic(err) } -} \ No newline at end of file +}