// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: MPL-2.0 package structs import ( "errors" "fmt" "testing" "time" "github.com/hashicorp/go-multierror" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/helper/pointer" "github.com/hashicorp/nomad/helper/uuid" "github.com/shoenig/test/must" "github.com/stretchr/testify/require" ) func TestACLToken_Canonicalize(t *testing.T) { testCases := []struct { name string testFn func() }{ { name: "token with accessor", testFn: func() { mockToken := &ACLToken{ AccessorID: uuid.Generate(), SecretID: uuid.Generate(), Name: "my cool token " + uuid.Generate(), Type: "client", Policies: []string{"foo", "bar"}, Roles: []*ACLTokenRoleLink{}, Global: false, CreateTime: time.Now().UTC(), CreateIndex: 10, ModifyIndex: 20, } mockToken.SetHash() copiedMockToken := mockToken.Copy() mockToken.Canonicalize() require.Equal(t, copiedMockToken, mockToken) }, }, { name: "token without accessor", testFn: func() { mockToken := &ACLToken{ Name: "my cool token " + uuid.Generate(), Type: "client", Policies: []string{"foo", "bar"}, Global: false, } mockToken.Canonicalize() require.NotEmpty(t, mockToken.AccessorID) require.NotEmpty(t, mockToken.SecretID) require.NotEmpty(t, mockToken.CreateTime) }, }, { name: "token with ttl without accessor", testFn: func() { mockToken := &ACLToken{ Name: "my cool token " + uuid.Generate(), Type: "client", Policies: []string{"foo", "bar"}, Global: false, ExpirationTTL: 10 * time.Hour, } mockToken.Canonicalize() require.NotEmpty(t, mockToken.AccessorID) require.NotEmpty(t, mockToken.SecretID) require.NotEmpty(t, mockToken.CreateTime) require.NotEmpty(t, mockToken.ExpirationTime) }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { tc.testFn() }) } } func TestACLTokenValidate(t *testing.T) { ci.Parallel(t) testCases := []struct { name string inputACLToken *ACLToken inputExistingACLToken *ACLToken expectedErrorContains string }{ { name: "missing type", inputACLToken: &ACLToken{}, inputExistingACLToken: nil, expectedErrorContains: "client or management", }, { name: "missing policies or roles", inputACLToken: &ACLToken{ Type: ACLClientToken, }, inputExistingACLToken: nil, expectedErrorContains: "missing policies or roles", }, { name: "invalid policies", inputACLToken: &ACLToken{ Type: ACLManagementToken, Policies: []string{"foo"}, }, inputExistingACLToken: nil, expectedErrorContains: "associated with policies or roles", }, { name: "invalid roles", inputACLToken: &ACLToken{ Type: ACLManagementToken, Roles: []*ACLTokenRoleLink{{Name: "foo"}}, }, inputExistingACLToken: nil, expectedErrorContains: "associated with policies or roles", }, { name: "name too long", inputACLToken: &ACLToken{ Type: ACLManagementToken, Name: uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate(), }, inputExistingACLToken: nil, expectedErrorContains: "name too long", }, { name: "negative TTL", inputACLToken: &ACLToken{ Type: ACLManagementToken, Name: "foo", ExpirationTTL: -1 * time.Hour, }, inputExistingACLToken: nil, expectedErrorContains: "should not be negative", }, { name: "TTL too small", inputACLToken: &ACLToken{ Type: ACLManagementToken, Name: "foo", CreateTime: time.Date(2022, time.July, 11, 16, 23, 0, 0, time.UTC), ExpirationTime: pointer.Of(time.Date(2022, time.July, 11, 16, 23, 10, 0, time.UTC)), }, inputExistingACLToken: nil, expectedErrorContains: "expiration time cannot be less than", }, { name: "TTL too large", inputACLToken: &ACLToken{ Type: ACLManagementToken, Name: "foo", CreateTime: time.Date(2022, time.July, 11, 16, 23, 0, 0, time.UTC), ExpirationTime: pointer.Of(time.Date(2042, time.July, 11, 16, 23, 0, 0, time.UTC)), }, inputExistingACLToken: nil, expectedErrorContains: "expiration time cannot be more than", }, { name: "valid management", inputACLToken: &ACLToken{ Type: ACLManagementToken, Name: "foo", }, inputExistingACLToken: nil, expectedErrorContains: "", }, { name: "valid client", inputACLToken: &ACLToken{ Type: ACLClientToken, Name: "foo", Policies: []string{"foo"}, }, inputExistingACLToken: nil, expectedErrorContains: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualOutputError := tc.inputACLToken.Validate(1*time.Minute, 24*time.Hour, tc.inputExistingACLToken) if tc.expectedErrorContains != "" { require.ErrorContains(t, actualOutputError, tc.expectedErrorContains) } else { require.NoError(t, actualOutputError) } }) } } func TestACLToken_HasExpirationTime(t *testing.T) { testCases := []struct { name string inputACLToken *ACLToken expectedOutput bool `` }{ { name: "nil acl token", inputACLToken: nil, expectedOutput: false, }, { name: "default empty value", inputACLToken: &ACLToken{}, expectedOutput: false, }, { name: "expiration set to now", inputACLToken: &ACLToken{ ExpirationTime: pointer.Of(time.Now().UTC()), }, expectedOutput: true, }, { name: "expiration set to past", inputACLToken: &ACLToken{ ExpirationTime: pointer.Of(time.Date(2022, time.February, 21, 19, 35, 0, 0, time.UTC)), }, expectedOutput: true, }, { name: "expiration set to future", inputACLToken: &ACLToken{ ExpirationTime: pointer.Of(time.Date(2087, time.April, 25, 12, 0, 0, 0, time.UTC)), }, expectedOutput: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualOutput := tc.inputACLToken.HasExpirationTime() require.Equal(t, tc.expectedOutput, actualOutput) }) } } func TestACLToken_IsExpired(t *testing.T) { testCases := []struct { name string inputACLToken *ACLToken inputTime time.Time expectedOutput bool }{ { name: "token without expiry", inputACLToken: &ACLToken{}, inputTime: time.Now().UTC(), expectedOutput: false, }, { name: "empty input time", inputACLToken: &ACLToken{}, inputTime: time.Time{}, expectedOutput: false, }, { name: "token not expired", inputACLToken: &ACLToken{ ExpirationTime: pointer.Of(time.Date(2022, time.May, 9, 10, 27, 0, 0, time.UTC)), }, inputTime: time.Date(2022, time.May, 9, 10, 26, 0, 0, time.UTC), expectedOutput: false, }, { name: "token expired", inputACLToken: &ACLToken{ ExpirationTime: pointer.Of(time.Date(2022, time.May, 9, 10, 27, 0, 0, time.UTC)), }, inputTime: time.Date(2022, time.May, 9, 10, 28, 0, 0, time.UTC), expectedOutput: true, }, { name: "empty input time", inputACLToken: &ACLToken{ ExpirationTime: pointer.Of(time.Date(2022, time.May, 9, 10, 27, 0, 0, time.UTC)), }, inputTime: time.Time{}, expectedOutput: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualOutput := tc.inputACLToken.IsExpired(tc.inputTime) require.Equal(t, tc.expectedOutput, actualOutput) }) } } func TestACLToken_HasRoles(t *testing.T) { testCases := []struct { name string inputToken *ACLToken inputRoleIDs []string expectedOutput bool }{ { name: "client token request all subset", inputToken: &ACLToken{ Type: ACLClientToken, Roles: []*ACLTokenRoleLink{ {ID: "foo"}, {ID: "bar"}, {ID: "baz"}, }, }, inputRoleIDs: []string{"foo", "bar", "baz"}, expectedOutput: true, }, { name: "client token request partial subset", inputToken: &ACLToken{ Type: ACLClientToken, Roles: []*ACLTokenRoleLink{ {ID: "foo"}, {ID: "bar"}, {ID: "baz"}, }, }, inputRoleIDs: []string{"foo", "baz"}, expectedOutput: true, }, { name: "client token request one subset", inputToken: &ACLToken{ Type: ACLClientToken, Roles: []*ACLTokenRoleLink{ {ID: "foo"}, {ID: "bar"}, {ID: "baz"}, }, }, inputRoleIDs: []string{"baz"}, expectedOutput: true, }, { name: "client token request no subset", inputToken: &ACLToken{ Type: ACLClientToken, Roles: []*ACLTokenRoleLink{ {ID: "foo"}, {ID: "bar"}, {ID: "baz"}, }, }, inputRoleIDs: []string{"new"}, expectedOutput: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualOutput := tc.inputToken.HasRoles(tc.inputRoleIDs) require.Equal(t, tc.expectedOutput, actualOutput) }) } } func TestACLRole_SetHash(t *testing.T) { testCases := []struct { name string inputACLRole *ACLRole expectedOutput []byte }{ { name: "no hash set", inputACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, {Name: "mocked-test-policy-2"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{}, }, expectedOutput: []byte{ 122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160, 171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102, }, }, { name: "hash set with change", inputACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, {Name: "mocked-test-policy-2"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{ 137, 147, 2, 29, 53, 94, 78, 13, 45, 51, 127, 193, 21, 248, 230, 126, 34, 106, 216, 73, 248, 219, 209, 146, 204, 107, 185, 2, 89, 255, 198, 5, }, }, expectedOutput: []byte{ 122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160, 171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualOutput := tc.inputACLRole.SetHash() require.Equal(t, tc.expectedOutput, actualOutput) require.Equal(t, tc.inputACLRole.Hash, actualOutput) }) } } func TestACLRole_Validate(t *testing.T) { testCases := []struct { name string inputACLRole *ACLRole expectedError bool expectedErrorContains string }{ { name: "role name too long", inputACLRole: &ACLRole{ Name: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, expectedError: true, expectedErrorContains: "invalid name", }, { name: "role name too short", inputACLRole: &ACLRole{ Name: "", }, expectedError: true, expectedErrorContains: "invalid name", }, { name: "role name with invalid characters", inputACLRole: &ACLRole{ Name: "--#$%$^%_%%_?>", }, expectedError: true, expectedErrorContains: "invalid name", }, { name: "description too long", inputACLRole: &ACLRole{ Name: "acl-role", Description: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", }, expectedError: true, expectedErrorContains: "description longer than", }, { name: "no policies", inputACLRole: &ACLRole{ Name: "acl-role", Description: "", }, expectedError: true, expectedErrorContains: "at least one policy should be specified", }, { name: "valid", inputACLRole: &ACLRole{ Name: "acl-role", Description: "", Policies: []*ACLRolePolicyLink{ {Name: "policy-1"}, }, }, expectedError: false, expectedErrorContains: "", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualOutput := tc.inputACLRole.Validate() if tc.expectedError { require.ErrorContains(t, actualOutput, tc.expectedErrorContains) } else { require.NoError(t, actualOutput) } }) } } func TestACLRole_Canonicalize(t *testing.T) { testCases := []struct { name string inputACLRole *ACLRole }{ { name: "no ID set", inputACLRole: &ACLRole{}, }, { name: "id set", inputACLRole: &ACLRole{ID: "some-random-uuid"}, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { existing := tc.inputACLRole.Copy() tc.inputACLRole.Canonicalize() if existing.ID == "" { require.NotEmpty(t, tc.inputACLRole.ID) } else { require.Equal(t, existing.ID, tc.inputACLRole.ID) } }) } } func TestACLRole_Equals(t *testing.T) { testCases := []struct { name string composedACLRole *ACLRole inputACLRole *ACLRole expectedOutput bool }{ { name: "equal with hash set", composedACLRole: &ACLRole{ Name: "acl-role-", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, {Name: "mocked-test-policy-2"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{ 122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160, 171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102, }, }, inputACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, {Name: "mocked-test-policy-2"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{ 122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160, 171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102, }, }, expectedOutput: true, }, { name: "equal without hash set", composedACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, {Name: "mocked-test-policy-2"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{}, }, inputACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, {Name: "mocked-test-policy-2"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{}, }, expectedOutput: true, }, { name: "both nil", composedACLRole: nil, inputACLRole: nil, expectedOutput: true, }, { name: "not equal composed nil", composedACLRole: nil, inputACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, {Name: "mocked-test-policy-2"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{ 122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160, 171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102, }, }, expectedOutput: false, }, { name: "not equal input nil", composedACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, {Name: "mocked-test-policy-2"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{ 122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160, 171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102, }, }, inputACLRole: nil, expectedOutput: false, }, { name: "not equal with hash set", composedACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, {Name: "mocked-test-policy-2"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{ 122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160, 171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102, }, }, inputACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{ 137, 147, 2, 29, 53, 94, 78, 13, 45, 51, 127, 193, 21, 248, 230, 126, 34, 106, 216, 73, 248, 219, 209, 146, 204, 107, 185, 2, 89, 255, 198, 5, }, }, expectedOutput: false, }, { name: "not equal without hash set", composedACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, {Name: "mocked-test-policy-2"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{}, }, inputACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{}, }, expectedOutput: false, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualOutput := tc.composedACLRole.Equal(tc.inputACLRole) require.Equal(t, tc.expectedOutput, actualOutput) }) } } func TestACLRole_Copy(t *testing.T) { testCases := []struct { name string inputACLRole *ACLRole }{ { name: "nil input", inputACLRole: nil, }, { name: "general 1", inputACLRole: &ACLRole{ Name: "acl-role", Description: "mocked-test-acl-role", Policies: []*ACLRolePolicyLink{ {Name: "mocked-test-policy-1"}, {Name: "mocked-test-policy-2"}, }, CreateIndex: 10, ModifyIndex: 10, Hash: []byte{ 122, 193, 189, 171, 197, 13, 37, 81, 141, 213, 188, 212, 179, 223, 148, 160, 171, 141, 155, 136, 21, 128, 252, 100, 149, 195, 236, 148, 94, 70, 173, 102, }, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualOutput := tc.inputACLRole.Copy() require.Equal(t, tc.inputACLRole, actualOutput) }) } } func TestACLRole_Stub(t *testing.T) { testCases := []struct { name string inputACLRole *ACLRole expectedOutput *ACLRoleListStub }{ { name: "partially hydrated", inputACLRole: &ACLRole{ ID: "1d6332c8-02d7-325e-f675-a9bb4aff0c51", Name: "my-lovely-role", Description: "", Policies: []*ACLRolePolicyLink{ {Name: "my-lovely-policy"}, }, Hash: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, CreateIndex: 24, ModifyIndex: 24, }, expectedOutput: &ACLRoleListStub{ ID: "1d6332c8-02d7-325e-f675-a9bb4aff0c51", Name: "my-lovely-role", Description: "", Policies: []*ACLRolePolicyLink{ {Name: "my-lovely-policy"}, }, Hash: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, CreateIndex: 24, ModifyIndex: 24, }, }, { name: "hully hydrated", inputACLRole: &ACLRole{ ID: "1d6332c8-02d7-325e-f675-a9bb4aff0c51", Name: "my-lovely-role", Description: "this-is-my-lovely-role", Policies: []*ACLRolePolicyLink{ {Name: "my-lovely-policy"}, }, Hash: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, CreateIndex: 24, ModifyIndex: 24, }, expectedOutput: &ACLRoleListStub{ ID: "1d6332c8-02d7-325e-f675-a9bb4aff0c51", Name: "my-lovely-role", Description: "this-is-my-lovely-role", Policies: []*ACLRolePolicyLink{ {Name: "my-lovely-policy"}, }, Hash: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9}, CreateIndex: 24, ModifyIndex: 24, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { actualOutput := tc.inputACLRole.Stub() require.Equal(t, tc.expectedOutput, actualOutput) }) } } func Test_ACLRolesUpsertRequest(t *testing.T) { req := ACLRolesUpsertRequest{} require.False(t, req.IsRead()) } func Test_ACLRolesDeleteByIDRequest(t *testing.T) { req := ACLRolesDeleteByIDRequest{} require.False(t, req.IsRead()) } func Test_ACLRolesListRequest(t *testing.T) { req := ACLRolesListRequest{} require.True(t, req.IsRead()) } func Test_ACLRolesByIDRequest(t *testing.T) { req := ACLRolesByIDRequest{} require.True(t, req.IsRead()) } func Test_ACLRoleByIDRequest(t *testing.T) { req := ACLRoleByIDRequest{} require.True(t, req.IsRead()) } func Test_ACLRoleByNameRequest(t *testing.T) { req := ACLRoleByNameRequest{} require.True(t, req.IsRead()) } func Test_ACLAuthMethodListRequest(t *testing.T) { req := ACLAuthMethodListRequest{} must.True(t, req.IsRead()) } func Test_ACLAuthMethodGetRequest(t *testing.T) { req := ACLAuthMethodGetRequest{} must.True(t, req.IsRead()) } func TestACLAuthMethodSetHash(t *testing.T) { ci.Parallel(t) am := &ACLAuthMethod{ Name: "foo", Type: "bad type", } out1 := am.SetHash() must.NotNil(t, out1) must.NotNil(t, am.Hash) must.Eq(t, out1, am.Hash) am.Type = "good type" out2 := am.SetHash() must.NotNil(t, out2) must.NotNil(t, am.Hash) must.Eq(t, out2, am.Hash) must.NotEq(t, out1, out2) } func TestACLAuthMethod_Stub(t *testing.T) { ci.Parallel(t) maxTokenTTL, _ := time.ParseDuration("3600s") am := ACLAuthMethod{ Name: fmt.Sprintf("acl-auth-method-%s", uuid.Short()), Type: "acl-auth-mock-type", TokenLocality: "locality", MaxTokenTTL: maxTokenTTL, Default: true, Config: &ACLAuthMethodConfig{ OIDCDiscoveryURL: "http://example.com", OIDCClientID: "mock", OIDCClientSecret: "very secret secret", BoundAudiences: []string{"audience1", "audience2"}, AllowedRedirectURIs: []string{"foo", "bar"}, DiscoveryCaPem: []string{"foo"}, SigningAlgs: []string{"bar"}, ClaimMappings: map[string]string{"foo": "bar"}, ListClaimMappings: map[string]string{"foo": "bar"}, }, CreateTime: time.Now().UTC(), CreateIndex: 10, ModifyIndex: 10, } am.SetHash() must.Eq(t, am.Stub(), &ACLAuthMethodStub{ Name: am.Name, Type: am.Type, Default: am.Default, Hash: am.Hash, CreateIndex: am.CreateIndex, ModifyIndex: am.ModifyIndex, }) nilAuthMethod := &ACLAuthMethod{} must.Eq(t, nilAuthMethod.Stub(), &ACLAuthMethodStub{}) } func TestACLAuthMethod_Equal(t *testing.T) { ci.Parallel(t) maxTokenTTL, _ := time.ParseDuration("3600s") am1 := &ACLAuthMethod{ Name: fmt.Sprintf("acl-auth-method-%s", uuid.Short()), Type: "acl-auth-mock-type", TokenLocality: "locality", MaxTokenTTL: maxTokenTTL, Default: true, Config: &ACLAuthMethodConfig{ OIDCDiscoveryURL: "http://example.com", OIDCClientID: "mock", OIDCClientSecret: "very secret secret", BoundAudiences: []string{"audience1", "audience2"}, AllowedRedirectURIs: []string{"foo", "bar"}, DiscoveryCaPem: []string{"foo"}, SigningAlgs: []string{"bar"}, ClaimMappings: map[string]string{"foo": "bar"}, ListClaimMappings: map[string]string{"foo": "bar"}, }, CreateTime: time.Now().UTC(), CreateIndex: 10, ModifyIndex: 10, } am1.SetHash() // am2 differs from am1 by 1 nested conf field am2 := am1.Copy() am2.Config.OIDCClientID = "mock2" am2.SetHash() tests := []struct { name string method1 *ACLAuthMethod method2 *ACLAuthMethod want bool }{ {"one nil", am1, &ACLAuthMethod{}, false}, {"both nil", &ACLAuthMethod{}, &ACLAuthMethod{}, true}, {"one is different than the other", am1, am2, false}, {"equal", am1, am1.Copy(), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.method1.Equal(tt.method2) must.Eq(t, got, tt.want, must.Sprintf( "ACLAuthMethod.Equal() got %v, want %v, test case: %s", got, tt.want, tt.name)) }) } } func TestACLAuthMethod_Copy(t *testing.T) { ci.Parallel(t) maxTokenTTL, _ := time.ParseDuration("3600s") am1 := &ACLAuthMethod{ Name: fmt.Sprintf("acl-auth-method-%s", uuid.Short()), Type: "acl-auth-mock-type", TokenLocality: "locality", MaxTokenTTL: maxTokenTTL, Default: true, Config: &ACLAuthMethodConfig{ OIDCDiscoveryURL: "http://example.com", OIDCClientID: "mock", OIDCClientSecret: "very secret secret", BoundAudiences: []string{"audience1", "audience2"}, AllowedRedirectURIs: []string{"foo", "bar"}, DiscoveryCaPem: []string{"foo"}, SigningAlgs: []string{"bar"}, ClaimMappings: map[string]string{"foo": "bar"}, ListClaimMappings: map[string]string{"foo": "bar"}, }, CreateTime: time.Now().UTC(), CreateIndex: 10, ModifyIndex: 10, } am1.SetHash() am2 := am1.Copy() am2.SetHash() must.Eq(t, am1, am2) am3 := am1.Copy() am3.Config.AllowedRedirectURIs = []string{"new", "urls"} am3.SetHash() must.NotEq(t, am1, am3) } func TestACLAuthMethod_Validate(t *testing.T) { ci.Parallel(t) goodTTL, _ := time.ParseDuration("3600s") badTTL, _ := time.ParseDuration("3600h") tests := []struct { name string method *ACLAuthMethod wantErr bool errContains string }{ { "valid method", &ACLAuthMethod{ Name: "mock-auth-method", Type: "OIDC", TokenLocality: "local", MaxTokenTTL: goodTTL, }, false, "", }, {"invalid name", &ACLAuthMethod{Name: "is this name invalid?"}, true, "invalid name"}, {"invalid token locality", &ACLAuthMethod{TokenLocality: "regional"}, true, "invalid token locality"}, {"invalid type", &ACLAuthMethod{Type: "groovy"}, true, "invalid token type"}, {"invalid max ttl", &ACLAuthMethod{MaxTokenTTL: badTTL}, true, "invalid token type"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { minTTL, _ := time.ParseDuration("10s") maxTTL, _ := time.ParseDuration("10h") got := tt.method.Validate(minTTL, maxTTL) if tt.wantErr { must.Error(t, got, must.Sprintf( "ACLAuthMethod.Validate() got error, didn't expect it; test case: %s", tt.name)) must.StrContains(t, got.Error(), tt.errContains, must.Sprintf( "ACLAuthMethod.Validate() got %v error message, expected %v; test case: %s", got, tt.errContains, tt.name)) } else { must.NoError(t, got, must.Sprintf( "ACLAuthMethod.Validate() expected an error but didn't get one; test case: %s", tt.name)) } }) } } func TestACLAuthMethod_Merge(t *testing.T) { ci.Parallel(t) name := fmt.Sprintf("acl-auth-method-%s", uuid.Short()) maxTokenTTL, _ := time.ParseDuration("3600s") am1 := &ACLAuthMethod{ Name: name, TokenLocality: "global", } am2 := &ACLAuthMethod{ Name: name, Type: "OIDC", TokenLocality: "locality", MaxTokenTTL: maxTokenTTL, Default: true, Config: &ACLAuthMethodConfig{ OIDCDiscoveryURL: "http://example.com", OIDCClientID: "mock", OIDCClientSecret: "very secret secret", BoundAudiences: []string{"audience1", "audience2"}, AllowedRedirectURIs: []string{"foo", "bar"}, DiscoveryCaPem: []string{"foo"}, SigningAlgs: []string{"bar"}, ClaimMappings: map[string]string{"foo": "bar"}, ListClaimMappings: map[string]string{"foo": "bar"}, }, CreateTime: time.Now().UTC(), CreateIndex: 10, ModifyIndex: 10, } am1.Merge(am2) must.Eq(t, am1.TokenLocality, "global") minTTL, _ := time.ParseDuration("10s") maxTTL, _ := time.ParseDuration("10h") must.NoError(t, am1.Validate(minTTL, maxTTL)) } func TestACLAuthMethodConfig_Copy(t *testing.T) { ci.Parallel(t) amc1 := &ACLAuthMethodConfig{ OIDCDiscoveryURL: "http://example.com", OIDCClientID: "mock", OIDCClientSecret: "very secret secret", OIDCScopes: []string{"groups"}, BoundAudiences: []string{"audience1", "audience2"}, AllowedRedirectURIs: []string{"foo", "bar"}, DiscoveryCaPem: []string{"foo"}, SigningAlgs: []string{"bar"}, ClaimMappings: map[string]string{"foo": "bar"}, ListClaimMappings: map[string]string{"foo": "bar"}, } amc2 := amc1.Copy() must.Eq(t, amc1, amc2) amc3 := amc1.Copy() amc3.AllowedRedirectURIs = []string{"new", "urls"} must.NotEq(t, amc1, amc3) } func TestACLAuthMethod_Canonicalize(t *testing.T) { now := time.Now().UTC() tests := []struct { name string inputMethod *ACLAuthMethod }{ { "no create time or modify time set", &ACLAuthMethod{}, }, { "create time set to now, modify time not set", &ACLAuthMethod{CreateTime: now}, }, { "both create time and modify time set", &ACLAuthMethod{CreateTime: now, ModifyTime: now.Add(time.Hour)}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { existing := tt.inputMethod.Copy() tt.inputMethod.Canonicalize() if existing.CreateTime.IsZero() { must.NotEq(t, time.Time{}, tt.inputMethod.CreateTime) } else { must.Eq(t, existing.CreateTime, tt.inputMethod.CreateTime) } if existing.ModifyTime.IsZero() { must.NotEq(t, time.Time{}, tt.inputMethod.ModifyTime) } }) } } func TestACLAuthMethod_TokenLocalityIsGlobal(t *testing.T) { ci.Parallel(t) globalAuthMethod := &ACLAuthMethod{TokenLocality: "global"} must.True(t, globalAuthMethod.TokenLocalityIsGlobal()) localAuthMethod := &ACLAuthMethod{TokenLocality: "local"} must.False(t, localAuthMethod.TokenLocalityIsGlobal()) } func TestACLBindingRule_Canonicalize(t *testing.T) { ci.Parallel(t) testCases := []struct { name string inputACLBindingRule *ACLBindingRule }{ { name: "new binding rule", inputACLBindingRule: &ACLBindingRule{}, }, { name: "existing binding rule", inputACLBindingRule: &ACLBindingRule{ ID: "some-random-uuid", CreateTime: time.Now().UTC(), ModifyTime: time.Now().UTC(), }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { // Make a copy, so we can compare the modified object to it. copiedBindingRule := tc.inputACLBindingRule.Copy() tc.inputACLBindingRule.Canonicalize() if copiedBindingRule.ID == "" { must.NotEq(t, "", tc.inputACLBindingRule.ID) must.NotEq(t, copiedBindingRule.CreateTime, tc.inputACLBindingRule.CreateTime) must.NotEq(t, copiedBindingRule.ModifyTime, tc.inputACLBindingRule.ModifyTime) } else { must.Eq(t, copiedBindingRule.ID, tc.inputACLBindingRule.ID) must.Eq(t, copiedBindingRule.CreateTime, tc.inputACLBindingRule.CreateTime) must.NotEq(t, copiedBindingRule.ModifyTime, tc.inputACLBindingRule.ModifyTime) } }) } } func TestACLBindingRule_Validate(t *testing.T) { ci.Parallel(t) testCases := []struct { name string inputACLBindingRule *ACLBindingRule expectedError error }{ { name: "valid policy type rule", inputACLBindingRule: &ACLBindingRule{ Description: "some short description", AuthMethod: "auth0", Selector: "group_name in list.groups", BindType: ACLBindingRuleBindTypePolicy, BindName: "some-policy-name", }, expectedError: nil, }, { name: "invalid policy type rule", inputACLBindingRule: &ACLBindingRule{ Description: "some short description", AuthMethod: "auth0", Selector: "group_name in list.groups", BindType: ACLBindingRuleBindTypePolicy, BindName: "", }, expectedError: &multierror.Error{ Errors: []error{ errors.New("bind name is missing"), }, }, }, { name: "valid role type rule", inputACLBindingRule: &ACLBindingRule{ Description: "some short description", AuthMethod: "auth0", Selector: "group_name in list.groups", BindType: ACLBindingRuleBindTypeRole, BindName: "some-role-name", }, expectedError: nil, }, { name: "invalid role type rule", inputACLBindingRule: &ACLBindingRule{ Description: "some short description", AuthMethod: "auth0", Selector: "group_name in list.groups", BindType: ACLBindingRuleBindTypeRole, BindName: "", }, expectedError: &multierror.Error{ Errors: []error{ errors.New("bind name is missing"), }, }, }, { name: "valid management type rule", inputACLBindingRule: &ACLBindingRule{ Description: "some short description", AuthMethod: "auth0", Selector: "group_name in list.groups", BindType: ACLBindingRuleBindTypeManagement, BindName: "", }, expectedError: nil, }, { name: "invalid management type rule", inputACLBindingRule: &ACLBindingRule{ Description: "some short description", AuthMethod: "auth0", Selector: "group_name in list.groups", BindType: ACLBindingRuleBindTypeManagement, BindName: "some-name", }, expectedError: &multierror.Error{ Errors: []error{ errors.New("bind name should be empty"), }, }, }, { name: "invalid selector", inputACLBindingRule: &ACLBindingRule{ Description: "some short description", AuthMethod: "auth0", Selector: "group-name in list.groups", BindType: ACLBindingRuleBindTypePolicy, BindName: "some-policy-name", }, expectedError: &multierror.Error{ Errors: []error{ errors.New("selector is invalid: 1:6 (5): no match found, expected: \"!=\", \".\", \"==\", \"[\", [ \\t\\r\\n] or [a-zA-Z0-9_/]"), }, }, }, { name: "invalid all", inputACLBindingRule: &ACLBindingRule{ Description: uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate() + uuid.Generate(), AuthMethod: "", Selector: "group-name in list.groups", BindType: "", BindName: "", }, expectedError: &multierror.Error{ Errors: []error{ errors.New("auth method is missing"), errors.New("description longer than 256"), errors.New("bind type is missing"), errors.New("selector is invalid: 1:6 (5): no match found, expected: \"!=\", \".\", \"==\", \"[\", [ \\t\\r\\n] or [a-zA-Z0-9_/]"), }, }, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { must.Eq(t, tc.expectedError, tc.inputACLBindingRule.Validate()) }) } } func TestACLBindingRule_Merge(t *testing.T) { ci.Parallel(t) id := uuid.Short() br := &ACLBindingRule{ ID: id, Description: "old description", AuthMethod: "example-acl-auth-method", BindType: "rule", BindName: "bind name", CreateTime: time.Now().UTC(), CreateIndex: 10, ModifyIndex: 10, } // make a description update br_description_update := &ACLBindingRule{ ID: id, Description: "new description", } br_description_update.Merge(br) must.Eq(t, br_description_update.Description, "new description") must.Eq(t, br_description_update.BindType, "rule") } func TestACLBindingRule_SetHash(t *testing.T) { ci.Parallel(t) bindingRule := &ACLBindingRule{ ID: uuid.Generate(), AuthMethod: "okta", } out1 := bindingRule.SetHash() must.NotNil(t, out1) must.NotNil(t, bindingRule.Hash) must.Eq(t, out1, bindingRule.Hash) bindingRule.Description = "my lovely rule" out2 := bindingRule.SetHash() must.NotNil(t, out2) must.NotNil(t, bindingRule.Hash) must.Eq(t, out2, bindingRule.Hash) must.NotEq(t, out1, out2) } func TestACLBindingRule_Equal(t *testing.T) { ci.Parallel(t) aclBindingRule1 := &ACLBindingRule{ ID: uuid.Short(), Description: "mocked-acl-binding-rule", AuthMethod: "auth0", Selector: "engineering in list.roles", BindType: "role-id", BindName: "eng-ro", CreateTime: time.Now().UTC(), ModifyTime: time.Now().UTC(), CreateIndex: 10, ModifyIndex: 10, } aclBindingRule1.SetHash() // Create a second binding rule, and modify this from the first. aclBindingRule2 := aclBindingRule1.Copy() aclBindingRule2.Description = "" aclBindingRule2.SetHash() testCases := []struct { name string method1 *ACLBindingRule method2 *ACLBindingRule want bool }{ {"one nil", aclBindingRule1, &ACLBindingRule{}, false}, {"both nil", &ACLBindingRule{}, &ACLBindingRule{}, true}, {"not equal", aclBindingRule1, aclBindingRule2, false}, {"equal", aclBindingRule2, aclBindingRule2.Copy(), true}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { got := tc.method1.Equal(tc.method2) must.Eq(t, got, tc.want) }) } } func TestACLBindingRule_Copy(t *testing.T) { ci.Parallel(t) aclBindingRule1 := &ACLBindingRule{ ID: uuid.Short(), Description: "mocked-acl-binding-rule", AuthMethod: "auth0", Selector: "engineering in list.roles", BindType: "role-id", BindName: "eng-ro", CreateTime: time.Now().UTC(), ModifyTime: time.Now().UTC(), CreateIndex: 10, ModifyIndex: 10, } aclBindingRule1.SetHash() aclBindingRule2 := aclBindingRule1.Copy() must.Eq(t, aclBindingRule1, aclBindingRule2) aclBindingRule3 := aclBindingRule1.Copy() aclBindingRule3.Description = "" aclBindingRule3.SetHash() must.NotEq(t, aclBindingRule1, aclBindingRule3) } func TestACLBindingRule_Stub(t *testing.T) { ci.Parallel(t) aclBindingRule := ACLBindingRule{ ID: "some-uuid", Description: "my-binding-rule", AuthMethod: "auth0", Selector: "some selector.pattern", BindType: "role", BindName: "some-role-id-or-name", CreateTime: time.Now().UTC(), ModifyTime: time.Now().UTC(), CreateIndex: 1309, ModifyIndex: 9031, } aclBindingRule.SetHash() must.Eq(t, &ACLBindingRuleListStub{ ID: "some-uuid", Description: "my-binding-rule", AuthMethod: "auth0", Hash: aclBindingRule.Hash, CreateIndex: 1309, ModifyIndex: 9031, }, aclBindingRule.Stub()) } func Test_ACLBindingRulesUpsertRequest(t *testing.T) { ci.Parallel(t) req := ACLBindingRulesUpsertRequest{} require.False(t, req.IsRead()) } func Test_ACLBindingRulesDeleteRequest(t *testing.T) { ci.Parallel(t) req := ACLBindingRulesDeleteRequest{} require.False(t, req.IsRead()) } func Test_ACLBindingRulesListRequest(t *testing.T) { ci.Parallel(t) req := ACLBindingRulesListRequest{} require.True(t, req.IsRead()) } func Test_ACLBindingRulesRequest(t *testing.T) { ci.Parallel(t) req := ACLBindingRulesRequest{} require.True(t, req.IsRead()) } func Test_ACLBindingRuleRequest(t *testing.T) { ci.Parallel(t) req := ACLBindingRuleRequest{} require.True(t, req.IsRead()) } func TestACLOIDCAuthURLRequest(t *testing.T) { ci.Parallel(t) req := &ACLOIDCAuthURLRequest{} must.False(t, req.IsRead()) } func TestACLOIDCAuthURLRequest_Validate(t *testing.T) { ci.Parallel(t) testRequest := &ACLOIDCAuthURLRequest{} err := testRequest.Validate() must.Error(t, err) must.StrContains(t, err.Error(), "missing auth method name") must.StrContains(t, err.Error(), "missing client nonce") must.StrContains(t, err.Error(), "missing redirect URI") } func TestACLOIDCCompleteAuthRequest(t *testing.T) { ci.Parallel(t) req := &ACLOIDCCompleteAuthRequest{} must.False(t, req.IsRead()) } func TestACLOIDCCompleteAuthRequest_Validate(t *testing.T) { ci.Parallel(t) testRequest := &ACLOIDCCompleteAuthRequest{} err := testRequest.Validate() must.Error(t, err) must.StrContains(t, err.Error(), "missing auth method name") must.StrContains(t, err.Error(), "missing client nonce") must.StrContains(t, err.Error(), "missing state") must.StrContains(t, err.Error(), "missing code") must.StrContains(t, err.Error(), "missing redirect URI") }