mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	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:
		
							
								
								
									
										3
									
								
								changelog/16181.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/16181.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | ```release-note:improvement | ||||||
|  | identity/oidc: allows filtering the list providers response by an allowed_client_id | ||||||
|  | ``` | ||||||
| @@ -178,6 +178,8 @@ func buildLogicalRequestNoAuth(perfStandby bool, w http.ResponseWriter, r *http. | |||||||
| 			path += "/" | 			path += "/" | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		data = parseQuery(r.URL.Query()) | ||||||
|  |  | ||||||
| 	case "OPTIONS", "HEAD": | 	case "OPTIONS", "HEAD": | ||||||
| 	default: | 	default: | ||||||
| 		return nil, nil, http.StatusMethodNotAllowed, nil | 		return nil, nil, http.StatusMethodNotAllowed, nil | ||||||
|   | |||||||
| @@ -135,6 +135,19 @@ type provider struct { | |||||||
| 	effectiveIssuer string | 	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 { | type providerDiscovery struct { | ||||||
| 	Issuer                string   `json:"issuer"` | 	Issuer                string   `json:"issuer"` | ||||||
| 	Keys                  string   `json:"jwks_uri"` | 	Keys                  string   `json:"jwks_uri"` | ||||||
| @@ -356,6 +369,14 @@ func oidcProviderPaths(i *IdentityStore) []*framework.Path { | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			Pattern: "oidc/provider/?$", | 			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{ | 			Operations: map[logical.Operation]framework.OperationHandler{ | ||||||
| 				logical.ListOperation: &framework.PathOperation{ | 				logical.ListOperation: &framework.PathOperation{ | ||||||
| 					Callback: i.pathOIDCListProvider, | 					Callback: i.pathOIDCListProvider, | ||||||
| @@ -1301,6 +1322,34 @@ func (i *IdentityStore) pathOIDCListProvider(ctx context.Context, req *logical.R | |||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, err | 		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 | 	return logical.ListResponse(providers), nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -1592,8 +1641,7 @@ func (i *IdentityStore) pathOIDCAuthorize(ctx context.Context, req *logical.Requ | |||||||
| 	if client == nil { | 	if client == nil { | ||||||
| 		return authResponse("", state, ErrAuthInvalidClientID, "client with client_id not found") | 		return authResponse("", state, ErrAuthInvalidClientID, "client with client_id not found") | ||||||
| 	} | 	} | ||||||
| 	if !strutil.StrListContains(provider.AllowedClientIDs, "*") && | 	if !provider.allowedClientID(clientID) { | ||||||
| 		!strutil.StrListContains(provider.AllowedClientIDs, clientID) { |  | ||||||
| 		return authResponse("", state, ErrAuthUnauthorizedClient, "client is not authorized to use the provider") | 		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 | 	// Validate that the client is authorized to use the provider | ||||||
| 	if !strutil.StrListContains(provider.AllowedClientIDs, "*") && | 	if !provider.allowedClientID(clientID) { | ||||||
| 		!strutil.StrListContains(provider.AllowedClientIDs, clientID) { |  | ||||||
| 		return tokenResponse(nil, ErrTokenInvalidClient, "client is not authorized to use the provider") | 		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 | 	// Validate that the client is authorized to use the provider | ||||||
| 	if !strutil.StrListContains(provider.AllowedClientIDs, "*") && | 	if !provider.allowedClientID(clientID) { | ||||||
| 		!strutil.StrListContains(provider.AllowedClientIDs, clientID) { |  | ||||||
| 		return userInfoResponse(nil, ErrUserInfoAccessDenied, "client is not authorized to use the provider") | 		return userInfoResponse(nil, ErrUserInfoAccessDenied, "client is not authorized to use the provider") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3350,6 +3350,89 @@ 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_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 | // TestOIDC_Path_OpenIDProviderConfig tests read operations for the | ||||||
| // openid-configuration path | // openid-configuration path | ||||||
| func TestOIDC_Path_OpenIDProviderConfig(t *testing.T) { | func TestOIDC_Path_OpenIDProviderConfig(t *testing.T) { | ||||||
|   | |||||||
| @@ -84,6 +84,11 @@ This endpoint returns a list of all OIDC providers. | |||||||
| | :----- | :------------------------------ | | | :----- | :------------------------------ | | ||||||
| | `LIST` | `/identity/oidc/provider`           | | | `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 | ### Sample Request | ||||||
|  |  | ||||||
| ```shell-session | ```shell-session | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Austin Gebauer
					Austin Gebauer