diff --git a/api/acl.go b/api/acl.go index fb80d7d24..f4ebd2ed1 100644 --- a/api/acl.go +++ b/api/acl.go @@ -442,6 +442,38 @@ func (a *ACLBindingRules) Get(bindingRuleID string, q *QueryOptions) (*ACLBindin return &resp, qm, nil } +// ACLOIDC is used to query the ACL OIDC endpoints. +type ACLOIDC struct { + client *Client +} + +// ACLOIDC returns a new handle on the ACL auth-methods API client. +func (c *Client) ACLOIDC() *ACLOIDC { + return &ACLOIDC{client: c} +} + +// GetAuthURL generates the OIDC provider authentication URL. This URL should +// be visited in order to sign in to the provider. +func (a *ACLOIDC) GetAuthURL(req *ACLOIDCAuthURLRequest, q *WriteOptions) (*ACLOIDCAuthURLResponse, *WriteMeta, error) { + var resp ACLOIDCAuthURLResponse + wm, err := a.client.write("/v1/acl/oidc/auth-url", req, &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, wm, nil +} + +// CompleteAuth exchanges the OIDC provider token for a Nomad token with the +// appropriate claims attached. +func (a *ACLOIDC) CompleteAuth(req *ACLOIDCCompleteAuthRequest, q *WriteOptions) (*ACLToken, *WriteMeta, error) { + var resp ACLToken + wm, err := a.client.write("/v1/acl/oidc/complete-auth", req, &resp, q) + if err != nil { + return nil, nil, err + } + return &resp, wm, nil +} + // ACLPolicyListStub is used to for listing ACL policies type ACLPolicyListStub struct { Name string @@ -666,6 +698,7 @@ type ACLAuthMethodConfig struct { OIDCDiscoveryURL string OIDCClientID string OIDCClientSecret string + OIDCScopes []string BoundAudiences []string AllowedRedirectURIs []string DiscoveryCaPem []string @@ -816,3 +849,50 @@ type ACLBindingRuleListStub struct { CreateIndex uint64 ModifyIndex uint64 } + +// ACLOIDCAuthURLRequest is the request to make when starting the OIDC +// authentication login flow. +type ACLOIDCAuthURLRequest struct { + + // AuthMethodName is the OIDC auth-method to use. This is a required + // parameter. + AuthMethodName string + + // RedirectURI is the URL that authorization should redirect to. This is a + // required parameter. + RedirectURI string + + // ClientNonce is a randomly generated string to prevent replay attacks. It + // is up to the client to generate this and Go integrations should use the + // oidc.NewID function within the hashicorp/cap library. + ClientNonce string +} + +// ACLOIDCAuthURLResponse is the response when starting the OIDC authentication +// login flow. +type ACLOIDCAuthURLResponse struct { + + // AuthURL is URL to begin authorization and is where the user logging in + // should go. + AuthURL string +} + +// ACLOIDCCompleteAuthRequest is the request object to begin completing the +// OIDC auth cycle after receiving the callback from the OIDC provider. +type ACLOIDCCompleteAuthRequest struct { + + // AuthMethodName is the name of the auth method being used to login via + // OIDC. This will match AuthUrlArgs.AuthMethodName. This is a required + // parameter. + AuthMethodName string + + // ClientNonce, State, and Code are provided from the parameters given to + // the redirect URL. These are all required parameters. + ClientNonce string + State string + Code string + + // RedirectURI is the URL that authorization should redirect to. This is a + // required parameter. + RedirectURI string +} diff --git a/command/agent/acl_endpoint.go b/command/agent/acl_endpoint.go index 04c6c2742..1c3fc5868 100644 --- a/command/agent/acl_endpoint.go +++ b/command/agent/acl_endpoint.go @@ -829,3 +829,48 @@ func (s *HTTPServer) aclBindingRuleUpsertRequest( } return nil, nil } + +// ACLOIDCAuthURLRequest starts the OIDC login workflow. +func (s *HTTPServer) ACLOIDCAuthURLRequest(_ http.ResponseWriter, req *http.Request) (interface{}, error) { + + // The endpoint only supports PUT or POST requests. + if req.Method != http.MethodPost && req.Method != http.MethodPut { + return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod) + } + + var args structs.ACLOIDCAuthURLRequest + s.parseWriteRequest(req, &args.WriteRequest) + + if err := decodeBody(req, &args); err != nil { + return nil, CodedError(http.StatusBadRequest, err.Error()) + } + + var out structs.ACLOIDCAuthURLResponse + if err := s.agent.RPC(structs.ACLOIDCAuthURLRPCMethod, &args, &out); err != nil { + return nil, err + } + return out, nil +} + +// ACLOIDCCompleteAuthRequest completes the OIDC login workflow. +func (s *HTTPServer) ACLOIDCCompleteAuthRequest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + + // The endpoint only supports PUT or POST requests. + if req.Method != http.MethodPost && req.Method != http.MethodPut { + return nil, CodedError(http.StatusMethodNotAllowed, ErrInvalidMethod) + } + + var args structs.ACLOIDCCompleteAuthRequest + s.parseWriteRequest(req, &args.WriteRequest) + + if err := decodeBody(req, &args); err != nil { + return nil, CodedError(http.StatusBadRequest, err.Error()) + } + + var out structs.ACLOIDCCompleteAuthResponse + if err := s.agent.RPC(structs.ACLOIDCCompleteAuthRPCMethod, &args, &out); err != nil { + return nil, err + } + setIndex(resp, out.Index) + return out.ACLToken, nil +} diff --git a/command/agent/acl_endpoint_test.go b/command/agent/acl_endpoint_test.go index 59ae857d8..848ccdca6 100644 --- a/command/agent/acl_endpoint_test.go +++ b/command/agent/acl_endpoint_test.go @@ -4,9 +4,11 @@ import ( "fmt" "net/http" "net/http/httptest" + "net/url" "testing" "time" + capOIDC "github.com/hashicorp/cap/oidc" "github.com/hashicorp/nomad/ci" "github.com/hashicorp/nomad/helper/uuid" "github.com/hashicorp/nomad/nomad/mock" @@ -1213,10 +1215,10 @@ func TestHTTPServer_ACLAuthMethodSpecificRequest(t *testing.T) { must.NoError(t, srv.server.State().UpsertACLAuthMethods( 20, []*structs.ACLAuthMethod{mockACLAuthMethod})) - url := "/v1/acl/auth-method/" + mockACLAuthMethod.Name + authMethodURL := "/v1/acl/auth-method/" + mockACLAuthMethod.Name // Build the HTTP request. - req, err := http.NewRequest(http.MethodGet, url, nil) + req, err := http.NewRequest(http.MethodGet, authMethodURL, nil) must.NoError(t, err) respW := httptest.NewRecorder() @@ -1238,10 +1240,10 @@ func TestHTTPServer_ACLAuthMethodSpecificRequest(t *testing.T) { must.NoError(t, srv.server.State().UpsertACLAuthMethods( 20, []*structs.ACLAuthMethod{mockACLAuthMethod})) - url := "/v1/acl/auth-method/" + mockACLAuthMethod.Name + authMethodURL := "/v1/acl/auth-method/" + mockACLAuthMethod.Name // Build the HTTP request to read the auth-method. - req, err := http.NewRequest(http.MethodGet, url, nil) + req, err := http.NewRequest(http.MethodGet, authMethodURL, nil) must.NoError(t, err) respW := httptest.NewRecorder() @@ -1258,7 +1260,7 @@ func TestHTTPServer_ACLAuthMethodSpecificRequest(t *testing.T) { mockACLAuthMethod.MaxTokenTTL = 3600 * time.Hour mockACLAuthMethod.SetHash() - req, err = http.NewRequest(http.MethodPost, url, encodeReq(mockACLAuthMethod)) + req, err = http.NewRequest(http.MethodPost, authMethodURL, encodeReq(mockACLAuthMethod)) must.NoError(t, err) respW = httptest.NewRecorder() @@ -1270,7 +1272,7 @@ func TestHTTPServer_ACLAuthMethodSpecificRequest(t *testing.T) { must.NoError(t, err) // Delete the ACL auth-method. - req, err = http.NewRequest(http.MethodDelete, url, nil) + req, err = http.NewRequest(http.MethodDelete, authMethodURL, nil) must.NoError(t, err) respW = httptest.NewRecorder() @@ -1622,3 +1624,221 @@ func TestHTTPServer_ACLBindingRuleSpecificRequest(t *testing.T) { }) } } + +func TestHTTPServer_ACLOIDCAuthURLRequest(t *testing.T) { + ci.Parallel(t) + + testCases := []struct { + name string + testFn func(srv *TestAgent) + }{ + { + name: "incorrect method", + testFn: func(testAgent *TestAgent) { + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodConnect, "/v1/acl/oidc/auth-url", nil) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + obj, err := testAgent.Server.ACLOIDCAuthURLRequest(respW, req) + must.Error(t, err) + must.StrContains(t, err.Error(), "Invalid method") + must.Nil(t, obj) + }, + }, + { + name: "success", + testFn: func(testAgent *TestAgent) { + + // Set up the test OIDC provider. + oidcTestProvider := capOIDC.StartTestProvider(t) + defer oidcTestProvider.Stop() + + // Generate and upsert an ACL auth method for use. Certain values must be + // taken from the cap OIDC provider just like real world use. + mockedAuthMethod := mock.ACLAuthMethod() + mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"} + mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr() + mockedAuthMethod.Config.SigningAlgs = []string{"ES256"} + mockedAuthMethod.Config.DiscoveryCaPem = []string{oidcTestProvider.CACert()} + + must.NoError(t, testAgent.server.State().UpsertACLAuthMethods( + 10, []*structs.ACLAuthMethod{mockedAuthMethod})) + + // Generate the request body. + requestBody := structs.ACLOIDCAuthURLRequest{ + AuthMethodName: mockedAuthMethod.Name, + RedirectURI: mockedAuthMethod.Config.AllowedRedirectURIs[0], + ClientNonce: "fpSPuaodKevKfDU3IeXa", + WriteRequest: structs.WriteRequest{ + Region: "global", + }, + } + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodPost, "/v1/acl/oidc/auth-url", encodeReq(&requestBody)) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + obj, err := testAgent.Server.ACLOIDCAuthURLRequest(respW, req) + must.NoError(t, err) + + // The response URL comes encoded, so decode this and check we have each + // component we expect. + escapedURL, err := url.PathUnescape(obj.(structs.ACLOIDCAuthURLResponse).AuthURL) + must.NoError(t, err) + must.StrContains(t, escapedURL, "/authorize?client_id=mock") + must.StrContains(t, escapedURL, "&nonce=fpSPuaodKevKfDU3IeXa") + must.StrContains(t, escapedURL, "&redirect_uri=http://127.0.0.1:4649/oidc/callback") + must.StrContains(t, escapedURL, "&response_type=code") + must.StrContains(t, escapedURL, "&scope=openid") + must.StrContains(t, escapedURL, "&state=st_") + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + httpACLTest(t, nil, tc.testFn) + }) + } +} + +func TestHTTPServer_ACLOIDCCompleteAuthRequest(t *testing.T) { + ci.Parallel(t) + + testCases := []struct { + name string + testFn func(srv *TestAgent) + }{ + { + name: "incorrect method", + testFn: func(testAgent *TestAgent) { + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodConnect, "/v1/acl/oidc/complete-auth", nil) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + obj, err := testAgent.Server.ACLOIDCCompleteAuthRequest(respW, req) + must.Error(t, err) + must.StrContains(t, err.Error(), "Invalid method") + must.Nil(t, obj) + }, + }, + { + name: "success", + testFn: func(testAgent *TestAgent) { + + // Set up the test OIDC provider. + oidcTestProvider := capOIDC.StartTestProvider(t) + defer oidcTestProvider.Stop() + oidcTestProvider.SetAllowedRedirectURIs([]string{"http://127.0.0.1:4649/oidc/callback"}) + + // Generate and upsert an ACL auth method for use. Certain values must be + // taken from the cap OIDC provider just like real world use. + mockedAuthMethod := mock.ACLAuthMethod() + mockedAuthMethod.Config.BoundAudiences = []string{"mock"} + mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"} + mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr() + mockedAuthMethod.Config.SigningAlgs = []string{"ES256"} + mockedAuthMethod.Config.DiscoveryCaPem = []string{oidcTestProvider.CACert()} + mockedAuthMethod.Config.ClaimMappings = map[string]string{} + mockedAuthMethod.Config.ListClaimMappings = map[string]string{ + "http://nomad.internal/roles": "roles", + "http://nomad.internal/policies": "policies", + } + + must.NoError(t, testAgent.server.State().UpsertACLAuthMethods( + 10, []*structs.ACLAuthMethod{mockedAuthMethod})) + + // Set our custom data and some expected values, so we can make the RPC and + // use the test provider. + oidcTestProvider.SetExpectedAuthNonce("fpSPuaodKevKfDU3IeXa") + oidcTestProvider.SetExpectedAuthCode("codeABC") + oidcTestProvider.SetCustomAudience("mock") + oidcTestProvider.SetExpectedState("st_someweirdstateid") + oidcTestProvider.SetCustomClaims(map[string]interface{}{ + "azp": "mock", + "http://nomad.internal/policies": []string{"engineering"}, + "http://nomad.internal/roles": []string{"engineering"}, + }) + + // Generate the request body. + requestBody := structs.ACLOIDCCompleteAuthRequest{ + AuthMethodName: mockedAuthMethod.Name, + ClientNonce: "fpSPuaodKevKfDU3IeXa", + State: "st_someweirdstateid", + Code: "codeABC", + RedirectURI: mockedAuthMethod.Config.AllowedRedirectURIs[0], + WriteRequest: structs.WriteRequest{ + Region: "global", + }, + } + + // Build the HTTP request. + req, err := http.NewRequest(http.MethodPost, "/v1/acl/oidc/complete-auth", encodeReq(&requestBody)) + must.NoError(t, err) + respW := httptest.NewRecorder() + + // Send the HTTP request. + _, err = testAgent.Server.ACLOIDCCompleteAuthRequest(respW, req) + must.ErrorContains(t, err, "no role or policy bindings matched") + + // Upsert an ACL policy and role, so that we can reference this within our + // OIDC claims. + mockACLPolicy := mock.ACLPolicy() + must.NoError(t, testAgent.server.State().UpsertACLPolicies( + structs.MsgTypeTestSetup, 20, []*structs.ACLPolicy{mockACLPolicy})) + + mockACLRole := mock.ACLRole() + mockACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: mockACLPolicy.Name}} + must.NoError(t, testAgent.server.State().UpsertACLRoles( + structs.MsgTypeTestSetup, 30, []*structs.ACLRole{mockACLRole}, true)) + + // Generate and upsert two binding rules, so we can test both ACL Policy + // and Role claim mapping. + mockBindingRule1 := mock.ACLBindingRule() + mockBindingRule1.AuthMethod = mockedAuthMethod.Name + mockBindingRule1.BindType = structs.ACLBindingRuleBindTypePolicy + mockBindingRule1.Selector = "engineering in list.policies" + mockBindingRule1.BindName = mockACLPolicy.Name + + mockBindingRule2 := mock.ACLBindingRule() + mockBindingRule2.AuthMethod = mockedAuthMethod.Name + mockBindingRule2.BindName = mockACLRole.Name + + must.NoError(t, testAgent.server.State().UpsertACLBindingRules( + 40, []*structs.ACLBindingRule{mockBindingRule1, mockBindingRule2}, true)) + + // Build the HTTP request. + req, err = http.NewRequest(http.MethodPost, "/v1/acl/oidc/complete-auth", encodeReq(&requestBody)) + must.NoError(t, err) + respW = httptest.NewRecorder() + + // Send the HTTP request. + obj, err := testAgent.Server.ACLOIDCCompleteAuthRequest(respW, req) + must.NoError(t, err) + + aclTokenResp, ok := obj.(*structs.ACLToken) + must.True(t, ok) + must.NotNil(t, aclTokenResp) + must.Len(t, 1, aclTokenResp.Policies) + must.Eq(t, mockACLPolicy.Name, aclTokenResp.Policies[0]) + must.Len(t, 1, aclTokenResp.Roles) + must.Eq(t, mockACLRole.Name, aclTokenResp.Roles[0].Name) + must.Eq(t, mockACLRole.ID, aclTokenResp.Roles[0].ID) + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + httpACLTest(t, nil, tc.testFn) + }) + } +} diff --git a/command/agent/http.go b/command/agent/http.go index 47c970532..da3580e77 100644 --- a/command/agent/http.go +++ b/command/agent/http.go @@ -397,6 +397,10 @@ func (s *HTTPServer) registerHandlers(enableDebug bool) { s.mux.HandleFunc("/v1/acl/binding-rule", s.wrap(s.ACLBindingRuleRequest)) s.mux.HandleFunc("/v1/acl/binding-rule/", s.wrap(s.ACLBindingRuleSpecificRequest)) + // Register out ACL OIDC SSO provider handlers. + s.mux.HandleFunc("/v1/acl/oidc/auth-url", s.wrap(s.ACLOIDCAuthURLRequest)) + s.mux.HandleFunc("/v1/acl/oidc/complete-auth", s.wrap(s.ACLOIDCCompleteAuthRequest)) + s.mux.Handle("/v1/client/fs/", wrapCORS(s.wrap(s.FsRequest))) s.mux.HandleFunc("/v1/client/gc", s.wrap(s.ClientGCRequest)) s.mux.Handle("/v1/client/stats", wrapCORS(s.wrap(s.ClientStatsRequest))) diff --git a/internal/testing/apitests/acl_test.go b/internal/testing/apitests/acl_test.go new file mode 100644 index 000000000..ce5e0d146 --- /dev/null +++ b/internal/testing/apitests/acl_test.go @@ -0,0 +1,181 @@ +package apitests + +import ( + "net/url" + "testing" + "time" + + capOIDC "github.com/hashicorp/cap/oidc" + "github.com/hashicorp/nomad/api" + "github.com/hashicorp/nomad/ci" + "github.com/shoenig/test/must" +) + +func TestACLOIDC_GetAuthURL(t *testing.T) { + ci.Parallel(t) + + testClient, testServer, _ := makeACLClient(t, nil, nil) + defer testServer.Stop() + + // Set up the test OIDC provider. + oidcTestProvider := capOIDC.StartTestProvider(t) + defer oidcTestProvider.Stop() + oidcTestProvider.SetAllowedRedirectURIs([]string{"http://127.0.0.1:4649/oidc/callback"}) + + // Generate and upsert an ACL auth method for use. Certain values must be + // taken from the cap OIDC provider just like real world use. + mockedAuthMethod := api.ACLAuthMethod{ + Name: "api-test-auth-method", + Type: api.ACLAuthMethodTypeOIDC, + TokenLocality: api.ACLAuthMethodTokenLocalityGlobal, + MaxTokenTTL: 10 * time.Hour, + Default: true, + Config: &api.ACLAuthMethodConfig{ + OIDCDiscoveryURL: oidcTestProvider.Addr(), + OIDCClientID: "mock", + OIDCClientSecret: "verysecretsecret", + BoundAudiences: []string{"mock"}, + AllowedRedirectURIs: []string{"http://127.0.0.1:4649/oidc/callback"}, + DiscoveryCaPem: []string{oidcTestProvider.CACert()}, + SigningAlgs: []string{"ES256"}, + ClaimMappings: map[string]string{"foo": "bar"}, + ListClaimMappings: map[string]string{"foo": "bar"}, + }, + } + + createdAuthMethod, writeMeta, err := testClient.ACLAuthMethods().Create(&mockedAuthMethod, nil) + must.NoError(t, err) + must.NotNil(t, createdAuthMethod) + assertWriteMeta(t, writeMeta) + + // Generate and make the request. + authURLRequest := api.ACLOIDCAuthURLRequest{ + AuthMethodName: createdAuthMethod.Name, + RedirectURI: createdAuthMethod.Config.AllowedRedirectURIs[0], + ClientNonce: "fpSPuaodKevKfDU3IeXb", + } + + authURLResp, _, err := testClient.ACLOIDC().GetAuthURL(&authURLRequest, nil) + must.NoError(t, err) + + // The response URL comes encoded, so decode this and check we have each + // component we expect. + escapedURL, err := url.PathUnescape(authURLResp.AuthURL) + must.NoError(t, err) + must.StrContains(t, escapedURL, "/authorize?client_id=mock") + must.StrContains(t, escapedURL, "&nonce=fpSPuaodKevKfDU3IeXb") + must.StrContains(t, escapedURL, "&redirect_uri=http://127.0.0.1:4649/oidc/callback") + must.StrContains(t, escapedURL, "&response_type=code") + must.StrContains(t, escapedURL, "&scope=openid") + must.StrContains(t, escapedURL, "&state=st_") +} + +func TestACLOIDC_CompleteAuth(t *testing.T) { + ci.Parallel(t) + + testClient, testServer, _ := makeACLClient(t, nil, nil) + defer testServer.Stop() + + // Set up the test OIDC provider. + oidcTestProvider := capOIDC.StartTestProvider(t) + defer oidcTestProvider.Stop() + oidcTestProvider.SetAllowedRedirectURIs([]string{"http://127.0.0.1:4649/oidc/callback"}) + + // Generate and upsert an ACL auth method for use. Certain values must be + // taken from the cap OIDC provider just like real world use. + mockedAuthMethod := api.ACLAuthMethod{ + Name: "api-test-auth-method", + Type: api.ACLAuthMethodTypeOIDC, + TokenLocality: api.ACLAuthMethodTokenLocalityGlobal, + MaxTokenTTL: 10 * time.Hour, + Default: true, + Config: &api.ACLAuthMethodConfig{ + OIDCDiscoveryURL: oidcTestProvider.Addr(), + OIDCClientID: "mock", + OIDCClientSecret: "verysecretsecret", + BoundAudiences: []string{"mock"}, + AllowedRedirectURIs: []string{"http://127.0.0.1:4649/oidc/callback"}, + DiscoveryCaPem: []string{oidcTestProvider.CACert()}, + SigningAlgs: []string{"ES256"}, + ClaimMappings: map[string]string{}, + ListClaimMappings: map[string]string{ + "http://nomad.internal/roles": "roles", + "http://nomad.internal/policies": "policies", + }, + }, + } + + createdAuthMethod, writeMeta, err := testClient.ACLAuthMethods().Create(&mockedAuthMethod, nil) + must.NoError(t, err) + must.NotNil(t, createdAuthMethod) + assertWriteMeta(t, writeMeta) + + // Set our custom data and some expected values, so we can make the call + // and use the test provider. + oidcTestProvider.SetExpectedAuthNonce("fpSPuaodKevKfDU3IeXb") + oidcTestProvider.SetExpectedAuthCode("codeABC") + oidcTestProvider.SetCustomAudience("mock") + oidcTestProvider.SetExpectedState("st_someweirdstateid") + oidcTestProvider.SetCustomClaims(map[string]interface{}{ + "azp": "mock", + "http://nomad.internal/policies": []string{"engineering"}, + "http://nomad.internal/roles": []string{"engineering"}, + }) + + // Upsert an ACL policy and role, so that we can reference this within our + // OIDC claims. + mockedACLPolicy := api.ACLPolicy{ + Name: "api-oidc-login-test", + Rules: `namespace "default" { policy = "write"}`, + } + _, err = testClient.ACLPolicies().Upsert(&mockedACLPolicy, nil) + must.NoError(t, err) + + mockedACLRole := api.ACLRole{ + Name: "api-oidc-login-test", + Policies: []*api.ACLRolePolicyLink{{Name: mockedACLPolicy.Name}}, + } + createRoleResp, _, err := testClient.ACLRoles().Create(&mockedACLRole, nil) + must.NoError(t, err) + must.NotNil(t, createRoleResp) + + // Generate and upsert two binding rules, so we can test both ACL Policy + // and Role claim mapping. + mockedBindingRule1 := api.ACLBindingRule{ + AuthMethod: mockedAuthMethod.Name, + Selector: "engineering in list.policies", + BindType: api.ACLBindingRuleBindTypePolicy, + BindName: mockedACLPolicy.Name, + } + createBindingRole1Resp, _, err := testClient.ACLBindingRules().Create(&mockedBindingRule1, nil) + must.NoError(t, err) + must.NotNil(t, createBindingRole1Resp) + + mockedBindingRule2 := api.ACLBindingRule{ + AuthMethod: mockedAuthMethod.Name, + Selector: "engineering in list.roles", + BindType: api.ACLBindingRuleBindTypeRole, + BindName: mockedACLRole.Name, + } + createBindingRole2Resp, _, err := testClient.ACLBindingRules().Create(&mockedBindingRule2, nil) + must.NoError(t, err) + must.NotNil(t, createBindingRole2Resp) + + // Generate and make the request. + authURLRequest := api.ACLOIDCCompleteAuthRequest{ + AuthMethodName: createdAuthMethod.Name, + RedirectURI: createdAuthMethod.Config.AllowedRedirectURIs[0], + ClientNonce: "fpSPuaodKevKfDU3IeXb", + State: "st_someweirdstateid", + Code: "codeABC", + } + + completeAuthResp, _, err := testClient.ACLOIDC().CompleteAuth(&authURLRequest, nil) + must.NoError(t, err) + must.NotNil(t, completeAuthResp) + must.Len(t, 1, completeAuthResp.Policies) + must.Eq(t, mockedACLPolicy.Name, completeAuthResp.Policies[0]) + must.Len(t, 1, completeAuthResp.Roles) + must.Eq(t, mockedACLRole.Name, completeAuthResp.Roles[0].Name) + must.Eq(t, createRoleResp.ID, completeAuthResp.Roles[0].ID) +}