identity/oidc: adds detailed listing capability for clients and providers (#16567)

* identity/oidc: adds detailed listing capability for clients and providers

* change approach to use ListResponseWithInfo

* adds changelog
This commit is contained in:
Austin Gebauer 2022-08-04 10:10:28 -07:00 committed by GitHub
parent 1820b771ce
commit e2d3846a25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 230 additions and 48 deletions

3
changelog/16567.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:improvement
identity/oidc: Adds support for detailed listing of clients and providers.
```

View File

@ -374,7 +374,8 @@ func oidcProviderPaths(i *IdentityStore) []*framework.Path {
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,
Default: "",
Query: true,
},
},
Operations: map[logical.Operation]framework.OperationHandler{
@ -1118,11 +1119,28 @@ func (i *IdentityStore) pathOIDCCreateUpdateClient(ctx context.Context, req *log
// pathOIDCListClient is used to list clients
func (i *IdentityStore) pathOIDCListClient(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
clients, err := req.Storage.List(ctx, clientPath)
clients, err := i.listClients(ctx, req.Storage)
if err != nil {
return nil, err
}
return logical.ListResponse(clients), nil
keys := make([]string, 0, len(clients))
keyInfo := make(map[string]interface{})
for _, client := range clients {
keys = append(keys, client.Name)
keyInfo[client.Name] = map[string]interface{}{
"redirect_uris": client.RedirectURIs,
"assignments": client.Assignments,
"key": client.Key,
"id_token_ttl": int64(client.IDTokenTTL.Seconds()),
"access_token_ttl": int64(client.AccessTokenTTL.Seconds()),
"client_type": client.Type.String(),
"client_id": client.ClientID,
// client_secret is intentionally omitted
}
}
return logical.ListResponseWithInfo(keys, keyInfo), nil
}
// pathOIDCReadClient is used to read an existing client
@ -1323,34 +1341,42 @@ func (i *IdentityStore) pathOIDCListProvider(ctx context.Context, req *logical.R
return nil, err
}
// Build a map from provider name to provider struct
providerMap := make(map[string]*provider)
for _, name := range providers {
provider, err := i.getOIDCProvider(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if provider == nil {
continue
}
providerMap[name] = provider
}
// 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)
if clientID := d.Get("allowed_client_id").(string); clientID != "" {
for name, provider := range providerMap {
if !provider.allowedClientID(clientID) {
delete(providerMap, name)
}
}
providers = filtered
}
return logical.ListResponse(providers), nil
keys := make([]string, 0, len(providerMap))
keyInfo := make(map[string]interface{})
for name, provider := range providerMap {
keys = append(keys, name)
keyInfo[name] = map[string]interface{}{
"issuer": provider.effectiveIssuer,
"allowed_client_ids": provider.AllowedClientIDs,
"scopes_supported": provider.ScopesSupported,
}
}
return logical.ListResponseWithInfo(keys, keyInfo), nil
}
// pathOIDCReadProvider is used to read an existing provider

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
"testing"
"time"
@ -2125,18 +2126,7 @@ func TestOIDC_Path_OIDC_ProviderClient_Update(t *testing.T) {
func TestOIDC_Path_OIDC_ProviderClient_List(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
ctx := namespace.RootContext(nil)
storage := &logical.InmemStorage{}
// Create a test key "test-key"
c.identityStore.HandleRequest(ctx, &logical.Request{
Path: "oidc/key/test-key",
Operation: logical.CreateOperation,
Data: map[string]interface{}{
"verification_ttl": "2m",
"rotation_period": "2m",
},
Storage: storage,
})
storage := c.identityStore.view
// Prepare two clients, test-client1 and test-client2
c.identityStore.HandleRequest(ctx, &logical.Request{
@ -2144,7 +2134,6 @@ func TestOIDC_Path_OIDC_ProviderClient_List(t *testing.T) {
Operation: logical.CreateOperation,
Storage: storage,
Data: map[string]interface{}{
"key": "test-key",
"id_token_ttl": "1m",
},
})
@ -2154,7 +2143,6 @@ func TestOIDC_Path_OIDC_ProviderClient_List(t *testing.T) {
Operation: logical.CreateOperation,
Storage: storage,
Data: map[string]interface{}{
"key": "test-key",
"id_token_ttl": "1m",
},
})
@ -2191,6 +2179,75 @@ func TestOIDC_Path_OIDC_ProviderClient_List(t *testing.T) {
expectStrings(t, respListClientAfterDelete.Data["keys"].([]string), expectedStrings)
}
func TestOIDC_Path_OIDC_Client_List_KeyInfo(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
ctx := namespace.RootContext(nil)
// Create clients with different parameters
clients := map[string]interface{}{
"c1": map[string]interface{}{
"id_token_ttl": "5m",
"access_token_ttl": "10m",
"assignments": []string{},
"redirect_uris": []string{"http://127.0.0.1:8250"},
"client_type": "confidential",
"key": "default",
},
"c2": map[string]interface{}{
"id_token_ttl": "24h",
"access_token_ttl": "5m",
"assignments": []string{allowAllAssignmentName},
"redirect_uris": []string{"https://localhost:9702/auth/oidc-callback"},
"client_type": "public",
"key": "default",
},
}
for name, client := range clients {
input := client.(map[string]interface{})
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
Path: "oidc/client/" + name,
Operation: logical.CreateOperation,
Storage: c.identityStore.view,
Data: input,
})
expectSuccess(t, resp, err)
}
// List clients
req := &logical.Request{
Path: "oidc/client",
Operation: logical.ListOperation,
Storage: c.identityStore.view,
Data: make(map[string]interface{}),
}
resp, err := c.identityStore.HandleRequest(ctx, req)
expectSuccess(t, resp, err)
require.NotNil(t, resp.Data["key_info"])
require.NotNil(t, resp.Data["keys"])
keys := resp.Data["keys"].([]string)
keyInfo := resp.Data["key_info"].(map[string]interface{})
require.Equal(t, len(keys), len(keyInfo))
// Assert the clients returned have additional key info
for name, details := range keyInfo {
actual, _ := details.(map[string]interface{})
require.NotNil(t, clients[name])
expected := clients[name].(map[string]interface{})
require.Contains(t, keys, name)
idTokenTTL, _ := time.ParseDuration(expected["id_token_ttl"].(string))
accessTokenTTL, _ := time.ParseDuration(expected["access_token_ttl"].(string))
require.EqualValues(t, idTokenTTL.Seconds(), actual["id_token_ttl"])
require.EqualValues(t, accessTokenTTL.Seconds(), actual["access_token_ttl"])
require.Equal(t, expected["redirect_uris"], actual["redirect_uris"])
require.Equal(t, expected["assignments"], actual["assignments"])
require.Equal(t, expected["key"], actual["key"])
require.Equal(t, expected["client_type"], actual["client_type"])
require.NotEmpty(t, actual["client_id"])
require.Empty(t, actual["client_secret"])
}
}
// TestOIDC_pathOIDCClientExistenceCheck tests pathOIDCClientExistenceCheck
func TestOIDC_pathOIDCClientExistenceCheck(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
@ -3352,6 +3409,76 @@ func TestOIDC_Path_OIDC_Provider_List(t *testing.T) {
expectStrings(t, respListProvidersAfterDelete.Data["keys"].([]string), expectedStrings)
}
func TestOIDC_Path_OIDC_Provider_List_KeyInfo(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
ctx := namespace.RootContext(nil)
// Create a custom scope
template := `{
"groups": {{identity.entity.groups.names}}
}`
resp, err := c.identityStore.HandleRequest(ctx, testScopeReq(c.identityStore.view,
"groups", template))
expectSuccess(t, resp, err)
// Create providers with different parameters
providers := map[string]interface{}{
"default": map[string]interface{}{
"allowed_client_ids": []string{"*"},
"scopes_supported": []string{},
"issuer": "http://127.0.0.1:8200",
},
"p0": map[string]interface{}{
"allowed_client_ids": []string{"abc", "def"},
"scopes_supported": []string{},
"issuer": "http://10.0.0.1:8200",
},
"p1": map[string]interface{}{
"allowed_client_ids": []string{"xyz"},
"scopes_supported": []string{"groups"},
"issuer": "https://myvault.com:8200",
},
}
for name, p := range providers {
input := p.(map[string]interface{})
resp, err := c.identityStore.HandleRequest(ctx, &logical.Request{
Path: "oidc/provider/" + name,
Operation: logical.CreateOperation,
Storage: c.identityStore.view,
Data: input,
})
expectSuccess(t, resp, err)
}
// List providers
resp, err = c.identityStore.HandleRequest(ctx, &logical.Request{
Path: "oidc/provider",
Operation: logical.ListOperation,
Storage: c.identityStore.view,
Data: make(map[string]interface{}),
})
expectSuccess(t, resp, err)
require.NotNil(t, resp.Data["key_info"])
require.NotNil(t, resp.Data["keys"])
keys := resp.Data["keys"].([]string)
keyInfo := resp.Data["key_info"].(map[string]interface{})
require.Equal(t, len(keys), len(keyInfo))
// Assert the providers returned have additional key info
for name, details := range keyInfo {
actual, _ := details.(map[string]interface{})
require.NotNil(t, providers[name])
expected := providers[name].(map[string]interface{})
require.Contains(t, keys, name)
expectedIssuer := fmt.Sprintf("%s%s%s", expected["issuer"],
"/v1/identity/oidc/provider/", name)
require.Equal(t, expectedIssuer, actual["issuer"])
require.Equal(t, expected["allowed_client_ids"], actual["allowed_client_ids"])
require.Equal(t, expected["scopes_supported"], actual["scopes_supported"])
}
}
func TestOIDC_Path_OIDC_Provider_List_Filter(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
ctx := namespace.RootContext(nil)
@ -3430,7 +3557,9 @@ func TestOIDC_Path_OIDC_Provider_List_Filter(t *testing.T) {
expectSuccess(t, resp, err)
// Assert the filtered set of providers is returned
require.Equal(t, tc.expectedProviders, resp.Data["keys"])
sort.Strings(tc.expectedProviders)
sort.Strings(resp.Data["keys"].([]string))
require.Equal(t, tc.expectedProviders, resp.Data["keys"].([]string))
})
}
}

View File

@ -103,10 +103,19 @@ $ curl \
```json
{
"data": {
"keys":[
"test-provider"
]
}
"key_info": {
"default": {
"allowed_client_ids": [
"*"
],
"issuer": "http://127.0.0.1:8200/v1/identity/oidc/provider/default",
"scopes_supported": []
}
},
"keys": [
"default"
]
}
}
```
@ -359,7 +368,7 @@ This endpoint returns a list of all configured clients.
| Method | Path |
| :----- | :------------------------------ |
| `LIST` | `/identity/oidc/client` |
| `LIST` | `/identity/oidc/client` |
### Sample Request
@ -375,10 +384,25 @@ $ curl \
```json
{
"data": {
"keys":[
"test-client"
]
}
"key_info": {
"my-app": {
"access_token_ttl": 86400,
"assignments": [
"allow_all"
],
"client_id": "wGr981oYLJbcr4zrUriYxjxSc80JL7HW",
"client_type": "confidential",
"id_token_ttl": 86400,
"key": "default",
"redirect_uris": [
"http://localhost:5555/callback"
]
}
},
"keys": [
"my-app"
]
}
}
```