mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 18:17:55 +00:00
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:
3
changelog/16567.txt
Normal file
3
changelog/16567.txt
Normal file
@@ -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,
|
Type: framework.TypeString,
|
||||||
Description: "Filters the list of OIDC providers to those " +
|
Description: "Filters the list of OIDC providers to those " +
|
||||||
"that allow the given client ID in their set of allowed_client_ids.",
|
"that allow the given client ID in their set of allowed_client_ids.",
|
||||||
Query: true,
|
Default: "",
|
||||||
|
Query: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Operations: map[logical.Operation]framework.OperationHandler{
|
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
|
// pathOIDCListClient is used to list clients
|
||||||
func (i *IdentityStore) pathOIDCListClient(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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
|
// 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
|
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
|
// 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
|
// returned OIDC providers to those that allow the given value in their set
|
||||||
// of allowed_client_ids.
|
// of allowed_client_ids.
|
||||||
if clientIDRaw, ok := d.GetOk("allowed_client_id"); ok {
|
if clientID := d.Get("allowed_client_id").(string); clientID != "" {
|
||||||
clientID := clientIDRaw.(string)
|
for name, provider := range providerMap {
|
||||||
if clientID == "" {
|
if !provider.allowedClientID(clientID) {
|
||||||
return logical.ListResponse(providers), nil
|
delete(providerMap, name)
|
||||||
}
|
|
||||||
|
|
||||||
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
|
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
|
// pathOIDCReadProvider is used to read an existing provider
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -2125,18 +2126,7 @@ func TestOIDC_Path_OIDC_ProviderClient_Update(t *testing.T) {
|
|||||||
func TestOIDC_Path_OIDC_ProviderClient_List(t *testing.T) {
|
func TestOIDC_Path_OIDC_ProviderClient_List(t *testing.T) {
|
||||||
c, _, _ := TestCoreUnsealed(t)
|
c, _, _ := TestCoreUnsealed(t)
|
||||||
ctx := namespace.RootContext(nil)
|
ctx := namespace.RootContext(nil)
|
||||||
storage := &logical.InmemStorage{}
|
storage := c.identityStore.view
|
||||||
|
|
||||||
// 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,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Prepare two clients, test-client1 and test-client2
|
// Prepare two clients, test-client1 and test-client2
|
||||||
c.identityStore.HandleRequest(ctx, &logical.Request{
|
c.identityStore.HandleRequest(ctx, &logical.Request{
|
||||||
@@ -2144,7 +2134,6 @@ func TestOIDC_Path_OIDC_ProviderClient_List(t *testing.T) {
|
|||||||
Operation: logical.CreateOperation,
|
Operation: logical.CreateOperation,
|
||||||
Storage: storage,
|
Storage: storage,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"key": "test-key",
|
|
||||||
"id_token_ttl": "1m",
|
"id_token_ttl": "1m",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -2154,7 +2143,6 @@ func TestOIDC_Path_OIDC_ProviderClient_List(t *testing.T) {
|
|||||||
Operation: logical.CreateOperation,
|
Operation: logical.CreateOperation,
|
||||||
Storage: storage,
|
Storage: storage,
|
||||||
Data: map[string]interface{}{
|
Data: map[string]interface{}{
|
||||||
"key": "test-key",
|
|
||||||
"id_token_ttl": "1m",
|
"id_token_ttl": "1m",
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -2191,6 +2179,75 @@ func TestOIDC_Path_OIDC_ProviderClient_List(t *testing.T) {
|
|||||||
expectStrings(t, respListClientAfterDelete.Data["keys"].([]string), expectedStrings)
|
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
|
// TestOIDC_pathOIDCClientExistenceCheck tests pathOIDCClientExistenceCheck
|
||||||
func TestOIDC_pathOIDCClientExistenceCheck(t *testing.T) {
|
func TestOIDC_pathOIDCClientExistenceCheck(t *testing.T) {
|
||||||
c, _, _ := TestCoreUnsealed(t)
|
c, _, _ := TestCoreUnsealed(t)
|
||||||
@@ -3352,6 +3409,76 @@ func TestOIDC_Path_OIDC_Provider_List(t *testing.T) {
|
|||||||
expectStrings(t, respListProvidersAfterDelete.Data["keys"].([]string), expectedStrings)
|
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) {
|
func TestOIDC_Path_OIDC_Provider_List_Filter(t *testing.T) {
|
||||||
c, _, _ := TestCoreUnsealed(t)
|
c, _, _ := TestCoreUnsealed(t)
|
||||||
ctx := namespace.RootContext(nil)
|
ctx := namespace.RootContext(nil)
|
||||||
@@ -3430,7 +3557,9 @@ func TestOIDC_Path_OIDC_Provider_List_Filter(t *testing.T) {
|
|||||||
expectSuccess(t, resp, err)
|
expectSuccess(t, resp, err)
|
||||||
|
|
||||||
// Assert the filtered set of providers is returned
|
// 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
|
```json
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"keys":[
|
"key_info": {
|
||||||
"test-provider"
|
"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 |
|
| Method | Path |
|
||||||
| :----- | :------------------------------ |
|
| :----- | :------------------------------ |
|
||||||
| `LIST` | `/identity/oidc/client` |
|
| `LIST` | `/identity/oidc/client` |
|
||||||
|
|
||||||
### Sample Request
|
### Sample Request
|
||||||
|
|
||||||
@@ -375,10 +384,25 @@ $ curl \
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"data": {
|
"data": {
|
||||||
"keys":[
|
"key_info": {
|
||||||
"test-client"
|
"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"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user