identity/oidc: allow filtering the list providers response by an allowed_client_id (#16181)

* identity/oidc: allow filtering the list providers response by an allowed_client_id

* adds changelog

* adds api documentation

* use identity store view in list provider test
This commit is contained in:
Austin Gebauer 2022-07-28 09:47:53 -07:00 committed by GitHub
parent b04d6e6720
commit b3f138679c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 145 additions and 6 deletions

3
changelog/16181.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
identity/oidc: allows filtering the list providers response by an allowed_client_id
```

View File

@ -178,6 +178,8 @@ func buildLogicalRequestNoAuth(perfStandby bool, w http.ResponseWriter, r *http.
path += "/"
}
data = parseQuery(r.URL.Query())
case "OPTIONS", "HEAD":
default:
return nil, nil, http.StatusMethodNotAllowed, nil

View File

@ -135,6 +135,19 @@ type provider struct {
effectiveIssuer string
}
// allowedClientID returns true if the given client ID is in
// the provider's set of allowed client IDs or its allowed client
// IDs contains the wildcard "*" char.
func (p *provider) allowedClientID(clientID string) bool {
for _, allowedID := range p.AllowedClientIDs {
switch allowedID {
case "*", clientID:
return true
}
}
return false
}
type providerDiscovery struct {
Issuer string `json:"issuer"`
Keys string `json:"jwks_uri"`
@ -356,6 +369,14 @@ func oidcProviderPaths(i *IdentityStore) []*framework.Path {
},
{
Pattern: "oidc/provider/?$",
Fields: map[string]*framework.FieldSchema{
"allowed_client_id": {
Type: framework.TypeString,
Description: "Filters the list of OIDC providers to those " +
"that allow the given client ID in their set of allowed_client_ids.",
Query: true,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
logical.ListOperation: &framework.PathOperation{
Callback: i.pathOIDCListProvider,
@ -1301,6 +1322,34 @@ func (i *IdentityStore) pathOIDCListProvider(ctx context.Context, req *logical.R
if err != nil {
return nil, err
}
// If allowed_client_id is provided as a query parameter, filter the set of
// returned OIDC providers to those that allow the given value in their set
// of allowed_client_ids.
if clientIDRaw, ok := d.GetOk("allowed_client_id"); ok {
clientID := clientIDRaw.(string)
if clientID == "" {
return logical.ListResponse(providers), nil
}
filtered := make([]string, 0)
for _, name := range providers {
provider, err := i.getOIDCProvider(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if provider == nil {
continue
}
if provider.allowedClientID(clientID) {
filtered = append(filtered, name)
}
}
providers = filtered
}
return logical.ListResponse(providers), nil
}
@ -1592,8 +1641,7 @@ func (i *IdentityStore) pathOIDCAuthorize(ctx context.Context, req *logical.Requ
if client == nil {
return authResponse("", state, ErrAuthInvalidClientID, "client with client_id not found")
}
if !strutil.StrListContains(provider.AllowedClientIDs, "*") &&
!strutil.StrListContains(provider.AllowedClientIDs, clientID) {
if !provider.allowedClientID(clientID) {
return authResponse("", state, ErrAuthUnauthorizedClient, "client is not authorized to use the provider")
}
@ -1812,8 +1860,7 @@ func (i *IdentityStore) pathOIDCToken(ctx context.Context, req *logical.Request,
}
// Validate that the client is authorized to use the provider
if !strutil.StrListContains(provider.AllowedClientIDs, "*") &&
!strutil.StrListContains(provider.AllowedClientIDs, clientID) {
if !provider.allowedClientID(clientID) {
return tokenResponse(nil, ErrTokenInvalidClient, "client is not authorized to use the provider")
}
@ -2133,8 +2180,7 @@ func (i *IdentityStore) pathOIDCUserInfo(ctx context.Context, req *logical.Reque
}
// Validate that the client is authorized to use the provider
if !strutil.StrListContains(provider.AllowedClientIDs, "*") &&
!strutil.StrListContains(provider.AllowedClientIDs, clientID) {
if !provider.allowedClientID(clientID) {
return userInfoResponse(nil, ErrUserInfoAccessDenied, "client is not authorized to use the provider")
}

View File

@ -3350,6 +3350,89 @@ func TestOIDC_Path_OIDC_Provider_List(t *testing.T) {
expectStrings(t, respListProvidersAfterDelete.Data["keys"].([]string), expectedStrings)
}
func TestOIDC_Path_OIDC_Provider_List_Filter(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
ctx := namespace.RootContext(nil)
// Create providers with different allowed_client_ids values
providers := []struct {
name string
allowedClientIDs []string
}{
{name: "p0", allowedClientIDs: []string{"*"}},
{name: "p1", allowedClientIDs: []string{"abc"}},
{name: "p2", allowedClientIDs: []string{"abc", "def"}},
{name: "p3", allowedClientIDs: []string{"abc", "def", "ghi"}},
{name: "p4", allowedClientIDs: []string{"ghi"}},
{name: "p5", allowedClientIDs: []string{"jkl"}},
}
for _, p := range providers {
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
Path: "oidc/provider/" + p.name,
Operation: logical.CreateOperation,
Storage: c.identityStore.view,
Data: map[string]interface{}{
"allowed_client_ids": p.allowedClientIDs,
},
})
expectSuccess(t, resp, err)
}
tests := []struct {
name string
clientIDFilter string
expectedProviders []string
}{
{
name: "list providers with client_id filter subset 1",
clientIDFilter: "abc",
expectedProviders: []string{"default", "p0", "p1", "p2", "p3"},
},
{
name: "list providers with client_id filter subset 2",
clientIDFilter: "def",
expectedProviders: []string{"default", "p0", "p2", "p3"},
},
{
name: "list providers with client_id filter subset 3",
clientIDFilter: "ghi",
expectedProviders: []string{"default", "p0", "p3", "p4"},
},
{
name: "list providers with client_id filter subset 4",
clientIDFilter: "jkl",
expectedProviders: []string{"default", "p0", "p5"},
},
{
name: "list providers with client_id filter only matching glob",
clientIDFilter: "globmatch_only",
expectedProviders: []string{"default", "p0"},
},
{
name: "list providers with empty client_id filter returns all",
clientIDFilter: "",
expectedProviders: []string{"default", "p0", "p1", "p2", "p3", "p4", "p5"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
// List providers with the allowed_client_id query parameter
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
Path: "oidc/provider",
Operation: logical.ListOperation,
Storage: c.identityStore.view,
Data: map[string]interface{}{
"allowed_client_id": tc.clientIDFilter,
},
})
expectSuccess(t, resp, err)
// Assert the filtered set of providers is returned
require.Equal(t, tc.expectedProviders, resp.Data["keys"])
})
}
}
// TestOIDC_Path_OpenIDProviderConfig tests read operations for the
// openid-configuration path
func TestOIDC_Path_OpenIDProviderConfig(t *testing.T) {

View File

@ -84,6 +84,11 @@ This endpoint returns a list of all OIDC providers.
| :----- | :------------------------------ |
| `LIST` | `/identity/oidc/provider` |
### Query Parameters
- `allowed_client_id` `(string: <optional>)` Filters the list of OIDC providers to those
that allow the given client ID in their set of [allowed_client_ids](/api-docs/secret/identity/oidc-provider#allowed_client_ids).
### Sample Request
```shell-session