api: add OIDC HTTP API endpoints and SDK.
This commit is contained in:
parent
485d216ab3
commit
b3a6cfecc4
80
api/acl.go
80
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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
|
|
|
@ -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)
|
||||
}
|
Loading…
Reference in New Issue