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:
parent
1820b771ce
commit
e2d3846a25
|
@ -0,0 +1,3 @@
|
|||
```release-note:improvement
|
||||
identity/oidc: Adds support for detailed listing of clients and providers.
|
||||
```
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
Loading…
Reference in New Issue