Repository: pdm-project/pdm

Issue:
Multiple pypi.url
## Is your feature request related to a problem? Please describe.

I need to define multiple pypi mirrors. As far as I understand it, using ``pdm.config pypi.url``, I can set ONE mirror.

## Describe the solution you'd like

There should be a way to define multiple pypi mirrors.

Alternatively, when adding a package, there should be a parameter available that specifies the mirror that will be used to look for the package.


Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) Report
Base: **85.53**% // Head: **85.35**% // Decreases project coverage by **`-0.17%`** :warning:
> Coverage data is based on head [(`a587332`)](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) compared to base [(`62d50c1`)](https://codecov.io/gh/pdm-project/pdm/commit/62d50c16b1a8ba84fd89be2f8d2fec3012cc437e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).
> Patch coverage: 71.01% of modified lines in pull request are covered.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #1597      +/-   ##
==========================================
- Coverage   85.53%   85.35%   -0.18%     
==========================================
  Files          92       92              
  Lines        8412     8459      +47     
  Branches     1975     1991      +16     
==========================================
+ Hits         7195     7220      +25     
- Misses        822      835      +13     
- Partials      395      404       +9     
```

| Flag | Coverage Δ | |
|---|---|---|
| unittests | `85.17% <71.01%> (-0.18%)` | :arrow_down: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project) | Coverage Δ | |
|---|---|---|
| [src/pdm/project/config.py](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9wcm9qZWN0L2NvbmZpZy5weQ==) | `85.37% <55.88%> (-4.36%)` | :arrow_down: |
| [src/pdm/cli/commands/config.py](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9jbGkvY29tbWFuZHMvY29uZmlnLnB5) | `92.85% <60.00%> (-3.37%)` | :arrow_down: |
| [src/pdm/models/auth.py](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9tb2RlbHMvYXV0aC5weQ==) | `75.00% <82.35%> (ø)` | |
| [src/pdm/\_types.py](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9fdHlwZXMucHk=) | `85.71% <100.00%> (+1.09%)` | :arrow_up: |
| [src/pdm/models/environment.py](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9tb2RlbHMvZW52aXJvbm1lbnQucHk=) | `66.85% <100.00%> (+0.37%)` | :arrow_up: |
| [src/pdm/project/core.py](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9wcm9qZWN0L2NvcmUucHk=) | `87.46% <100.00%> (+0.03%)` | :arrow_up: |
| [src/pdm/models/repositories.py](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9tb2RlbHMvcmVwb3NpdG9yaWVzLnB5) | `78.19% <0.00%> (-0.76%)` | :arrow_down: |
| [src/pdm/termui.py](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS90ZXJtdWkucHk=) | `89.85% <0.00%> (-0.73%)` | :arrow_down: |
| [src/pdm/pytest.py](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project#diff-c3JjL3BkbS9weXRlc3QucHk=) | `92.62% <0.00%> (-0.65%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project)

</details>

[:umbrella: View full report at Codecov](https://codecov.io/gh/pdm-project/pdm/pull/1597?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).   
:loudspeaker: Do you have feedback about the report comment? [Let us know in this issue](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pdm-project).


Patch:
diff --git a/docs/docs/usage/configuration.md b/docs/docs/usage/configuration.md
index eb8cf9fb..2fc828c8 100644
--- a/docs/docs/usage/configuration.md
+++ b/docs/docs/usage/configuration.md
@@ -29,35 +29,38 @@ pdm config theme.success '#51c7bd'
 
 The following configuration items can be retrieved and modified by [`pdm config`](../usage/cli_reference.md#exec-0--config) command.
 
-| Config Item                       | Description                                                               | Default Value                                                         | Available in Project | Env var                  |
-| --------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------- | -------------------- | ------------------------ |
-| `build_isolation`                 | Isolate the build environment from the project environment                | Yes                                                                   | True                 | `PDM_BUILD_ISOLATION`    |
-| `cache_dir`                       | The root directory of cached files                                        | The default cache location on OS                                      | No                   |                          |
-| `check_update`                    | Check if there is any newer version available                             | True                                                                  | No                   |                          |
-| `global_project.fallback`         | Use the global project implicitly if no local project is found            | `False`                                                               | No                   |                          |
-| `global_project.fallback_verbose` | If True show message when global project is used implicitly               | `True`                                                                | No                   |                          |
-| `global_project.path`             | The path to the global project                                            | `<default config location on OS>/global-project`                      | No                   |                          |
-| `global_project.user_site`        | Whether to install to user site                                           | `False`                                                               | No                   |                          |
-| `install.cache`                   | Enable caching of wheel installations                                     | False                                                                 | Yes                  |                          |
-| `install.cache_method`            | Specify how to create links to the caches(`symlink` or `pth`)             | `symlink`                                                             | Yes                  |                          |
-| `install.parallel`                | Whether to perform installation and uninstallation in parallel            | `True`                                                                | Yes                  | `PDM_PARALLEL_INSTALL`   |
-| `project_max_depth`               | The max depth to search for a project through the parents                 | 5                                                                     | No                   | `PDM_PROJECT_MAX_DEPTH`  |
-| `python.path`                     | The Python interpreter path                                               |                                                                       | Yes                  | `PDM_PYTHON`             |
-| `python.use_pyenv`                | Use the pyenv interpreter                                                 | `True`                                                                | Yes                  |                          |
-| `python.use_venv`                 | Install packages into the activated venv site packages instead of PEP 582 | `True`                                                                | Yes                  | `PDM_USE_VENV`           |
-| `pypi.url`                        | The URL of PyPI mirror                                                    | `https://pypi.org/simple`                                             | Yes                  | `PDM_PYPI_URL`           |
-| `pypi.ca_certs`                   | Path to a PEM-encoded CA cert bundle (used for server cert verification)  | The CA certificates from [certifi](https://pypi.org/project/certifi/) | Yes                  |                          |
-| `pypi.client_cert`                | Path to a PEM-encoded client cert and optional key                        |                                                                       | Yes                  |                          |
-| `pypi.client_key`                 | Path to a PEM-encoded client cert private key, if not in pypi.client_cert |                                                                       | Yes                  |                          |
-| `pypi.verify_ssl`                 | Verify SSL certificate when query PyPI                                    | `True`                                                                | Yes                  |                          |
-| `pypi.json_api`                   | Consult PyPI's JSON API for package metadata                              | `False`                                                               | Yes                  | `PDM_PYPI_JSON_API`      |
-| `strategy.save`                   | Specify how to save versions when a package is added                      | `compatible`(can be: `exact`, `wildcard`, `minimum`)                  | Yes                  |                          |
-| `strategy.update`                 | The default strategy for updating packages                                | `reuse`(can be : `eager`)                                             | Yes                  |                          |
-| `strategy.resolve_max_rounds`     | Specify the max rounds of resolution process                              | 10000                                                                  | Yes                  | `PDM_RESOLVE_MAX_ROUNDS` |
-| `venv.location`                   | Parent directory for virtualenvs                                          | `<default data location on OS>/venvs`                                 | No                   |                          |
-| `venv.backend`                    | Default backend to create virtualenv                                      | `virtualenv`                                                          | Yes                  | `PDM_VENV_BACKEND`       |
-| `venv.prompt`                     | Formatted string to be displayed in the prompt when virtualenv is active  | `{project_name}-{python_version}`                                     | Yes                  | `PDM_VENV_PROMPT`        |
-| `venv.in-project`                 | Create virtualenv in `.venv` under project root                           | `False`                                                               | Yes                  | `PDM_VENV_IN_PROJECT`    |
-| `venv.with_pip`                   | Install pip when creating a new venv                                      | `False`                                                               | Yes                  | `PDM_VENV_WITH_PIP`      |
+| Config Item                       | Description                                                               | Default Value                                                         | Available in Project | Env var                   |
+| --------------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------- | -------------------- | ------------------------- |
+| `build_isolation`                 | Isolate the build environment from the project environment                | Yes                                                                   | True                 | `PDM_BUILD_ISOLATION`     |
+| `cache_dir`                       | The root directory of cached files                                        | The default cache location on OS                                      | No                   |                           |
+| `check_update`                    | Check if there is any newer version available                             | True                                                                  | No                   |                           |
+| `global_project.fallback`         | Use the global project implicitly if no local project is found            | `False`                                                               | No                   |                           |
+| `global_project.fallback_verbose` | If True show message when global project is used implicitly               | `True`                                                                | No                   |                           |
+| `global_project.path`             | The path to the global project                                            | `<default config location on OS>/global-project`                      | No                   |                           |
+| `global_project.user_site`        | Whether to install to user site                                           | `False`                                                               | No                   |                           |
+| `install.cache`                   | Enable caching of wheel installations                                     | False                                                                 | Yes                  |                           |
+| `install.cache_method`            | Specify how to create links to the caches(`symlink` or `pth`)             | `symlink`                                                             | Yes                  |                           |
+| `install.parallel`                | Whether to perform installation and uninstallation in parallel            | `True`                                                                | Yes                  | `PDM_PARALLEL_INSTALL`    |
+| `project_max_depth`               | The max depth to search for a project through the parents                 | 5                                                                     | No                   | `PDM_PROJECT_MAX_DEPTH`   |
+| `python.path`                     | The Python interpreter path                                               |                                                                       | Yes                  | `PDM_PYTHON`              |
+| `python.use_pyenv`                | Use the pyenv interpreter                                                 | `True`                                                                | Yes                  |                           |
+| `python.use_venv`                 | Install packages into the activated venv site packages instead of PEP 582 | `True`                                                                | Yes                  | `PDM_USE_VENV`            |
+| `pypi.url`                        | The URL of PyPI mirror                                                    | `https://pypi.org/simple`                                             | Yes                  | `PDM_PYPI_URL`            |
+| `pypi.username`                   | The username to access PyPI                                               |                                                                       | Yes                  | `PDM_PYPI_USERNAME`       |
+| `pypi.password`                   | The password to access PyPI                                               |                                                                       | Yes                  | `PDM_PYPI_PASSWORD`       |
+| `pypi.ignore_stored_index`        | Ignore the configured indexes                                             | `False`                                                               | Yes                  | `PDM_IGNORE_STORED_INDEX` |
+| `pypi.ca_certs`                   | Path to a PEM-encoded CA cert bundle (used for server cert verification)  | The CA certificates from [certifi](https://pypi.org/project/certifi/) | Yes                  |                           |
+| `pypi.client_cert`                | Path to a PEM-encoded client cert and optional key                        |                                                                       | Yes                  |                           |
+| `pypi.client_key`                 | Path to a PEM-encoded client cert private key, if not in pypi.client_cert |                                                                       | Yes                  |                           |
+| `pypi.verify_ssl`                 | Verify SSL certificate when query PyPI                                    | `True`                                                                | Yes                  |                           |
+| `pypi.json_api`                   | Consult PyPI's JSON API for package metadata                              | `False`                                                               | Yes                  | `PDM_PYPI_JSON_API`       |
+| `strategy.save`                   | Specify how to save versions when a package is added                      | `compatible`(can be: `exact`, `wildcard`, `minimum`)                  | Yes                  |                           |
+| `strategy.update`                 | The default strategy for updating packages                                | `reuse`(can be : `eager`)                                             | Yes                  |                           |
+| `strategy.resolve_max_rounds`     | Specify the max rounds of resolution process                              | 10000                                                                 | Yes                  | `PDM_RESOLVE_MAX_ROUNDS`  |
+| `venv.location`                   | Parent directory for virtualenvs                                          | `<default data location on OS>/venvs`                                 | No                   |                           |
+| `venv.backend`                    | Default backend to create virtualenv                                      | `virtualenv`                                                          | Yes                  | `PDM_VENV_BACKEND`        |
+| `venv.prompt`                     | Formatted string to be displayed in the prompt when virtualenv is active  | `{project_name}-{python_version}`                                     | Yes                  | `PDM_VENV_PROMPT`         |
+| `venv.in-project`                 | Create virtualenv in `.venv` under project root                           | `False`                                                               | Yes                  | `PDM_VENV_IN_PROJECT`     |
+| `venv.with_pip`                   | Install pip when creating a new venv                                      | `False`                                                               | Yes                  | `PDM_VENV_WITH_PIP`       |
 
 _If the corresponding env var is set, the value will take precedence over what is saved in the config file._
diff --git a/docs/docs/usage/project.md b/docs/docs/usage/project.md
index 3a1d30be..a690b2d5 100644
--- a/docs/docs/usage/project.md
+++ b/docs/docs/usage/project.md
@@ -98,6 +98,47 @@ If `-g/--global` option is used, the first item will be replaced by `<CONFIG_ROO
 
 You can find all available configuration items in [Configuration Page](configuration.md).
 
+## Configure PyPI indexes
+
+From 2.4.0, PDM supports multiple PyPI indexes in the configuration file. To add a new index, include the following in the configuration file:
+
+```toml
+[pypi.extra]
+url = "https://extra.pypi.org/simple"
+verify_ssl = false
+```
+
+The available options are:
+
+- `url`: The URL of the index
+- `verify_ssl`: (Optional)Whether to verify SSL certificates, default to `true`
+- `username`: (Optional)The username for the index
+- `password`: (Optional)The password for the index
+- `type`: (Optional) `index` or `find_links`, default to `index`
+
+To configure the **main** index, omit the name of the index:
+
+```toml
+[pypi]
+url = "https://pypi.org/simple"
+verify_ssl = true
+```
+
+The index config can be also retrieved or updated via `pdm config` command:
+
+```bash
+# Get the url of the index named "extra"
+pdm config pypi.extra.url
+# Set the username and password of the index named "extra"
+pdm config pypi.extra.username "foo"
+pdm config pypi.extra.password "password4foo"
+```
+
+!!! NOTE
+    Configured indexes will be tried **after** the sources in `pyproject.toml`, if you want to completely ignore the
+    locally configured indexes, including the main index, set the config value `pypi.ignore_stored_index`
+    to `False` and only the sources in `pyproject.toml` will be honored.
+
 ## Publish the project to PyPI
 
 With PDM, you can build and then upload your project to PyPI in one step.
@@ -140,9 +181,11 @@ ca_certs = "/path/to/custom-cacerts.pem"
 A PEM-encoded Certificate Authority bundle (`ca_certs`) can be used for local / custom PyPI repositories where the server certificate is not signed by the standard [certifi](https://github.com/certifi/python-certifi/blob/master/certifi/cacert.pem) CA bundle.
 
 !!! NOTE
-    You don't need to configure the `url` for `pypi` and `testpypi` repositories, they are filled by default values.
+    Repositories are different from indexes in the previous section. Repositories are for publishing while indexes are for locking
+    and resolving. They don't share the configuration.
 
 !!! TIP
+    You don't need to configure the `url` for `pypi` and `testpypi` repositories, they are filled by default values.
     The username, password, and certificate authority bundle can be passed in from the command line for `pdm publish` via `--username`, `--password`, and `--ca-certs`, respectively.
 
 To change the repository config from the command line, use the [`pdm config`](cli_reference.md#exec-0--config) command:
diff --git a/news/1310.feature.md b/news/1310.feature.md
new file mode 100644
index 00000000..364aa301
--- /dev/null
+++ b/news/1310.feature.md
@@ -0,0 +1,1 @@
+Support multiple PyPI indexes in the configuration. They will be tried after the sources in `pyproject.toml`.
diff --git a/src/pdm/_types.py b/src/pdm/_types.py
index 95b99500..cf3a322d 100644
--- a/src/pdm/_types.py
+++ b/src/pdm/_types.py
@@ -10,6 +10,8 @@ class Source(TypedDict, total=False):
     verify_ssl: bool
     name: str
     type: Literal["index", "find_links"]
+    username: str
+    password: str
 
 
 RequirementDict = Union[str, Dict[str, Union[str, bool]]]
diff --git a/src/pdm/cli/commands/config.py b/src/pdm/cli/commands/config.py
index 1ba88aa9..f51c6bb0 100644
--- a/src/pdm/cli/commands/config.py
+++ b/src/pdm/cli/commands/config.py
@@ -44,10 +44,10 @@ class Command(BaseCommand):
                 err=True,
             )
             options.key = project.project_config.deprecated[options.key]
-        if options.key.split(".")[0] == "repository":
+        try:
+            value = project.project_config[options.key]
+        except KeyError:
             value = project.global_config[options.key]
-        else:
-            value = project.config[options.key]
         project.core.ui.echo(value)
 
     def _set_config(self, project: Project, options: argparse.Namespace) -> None:
@@ -77,6 +77,8 @@ class Command(BaseCommand):
             elif key not in Config._config_map:
                 continue
             extra_style = "dim" if superseded else None
+            if canonical_key not in Config._config_map:
+                continue
             config_item = Config._config_map[canonical_key]
             self.ui.echo(
                 f"[warning]# {config_item.description}",
diff --git a/src/pdm/models/auth.py b/src/pdm/models/auth.py
index b9c5cf0f..35b5b67d 100644
--- a/src/pdm/models/auth.py
+++ b/src/pdm/models/auth.py
@@ -1,6 +1,9 @@
 from __future__ import annotations
 
-from unearth.auth import MultiDomainBasicAuth
+import urllib.parse
+
+from unearth.auth import MaybeAuth, MultiDomainBasicAuth
+from unearth.utils import split_auth_from_netloc
 
 from pdm._types import Source
 from pdm.exceptions import PdmException
@@ -21,11 +24,22 @@ class PdmBasicAuth(MultiDomainBasicAuth):
         - It shows an error message when credentials are not provided or correct.
     """
 
-    def __init__(
-        self, prompting: bool = True, index_urls: list[str] | None = None
-    ) -> None:
-        super().__init__(prompting=True, index_urls=index_urls)
+    def __init__(self, sources: list[Source], prompting: bool = True) -> None:
+        super().__init__(prompting=True)
         self._real_prompting = prompting
+        self.sources = sources
+
+    def _get_auth_from_index_url(self, netloc: str) -> tuple[MaybeAuth, str | None]:
+        if not self.sources:
+            return None, None
+        for source in self.sources:
+            parsed = urllib.parse.urlparse(source["url"])
+            auth, index_netloc = split_auth_from_netloc(parsed.netloc)
+            if index_netloc == netloc:
+                if not auth and "username" in source:
+                    auth = (source["username"], source.get("password"))
+                return auth, source["url"]
+        return None, None
 
     def _prompt_for_password(self, netloc: str) -> tuple[str | None, str | None, bool]:
         if not self._real_prompting:
@@ -45,10 +59,3 @@ class PdmBasicAuth(MultiDomainBasicAuth):
                 style="warning",
             )
         return super()._should_save_password_to_keyring()
-
-
-def make_basic_auth(sources: list[Source], prompting: bool) -> PdmBasicAuth:
-    return PdmBasicAuth(
-        prompting,
-        [source["url"] for source in sources if source.get("type", "index") == "index"],
-    )
diff --git a/src/pdm/models/environment.py b/src/pdm/models/environment.py
index 0a713055..67fd1083 100644
--- a/src/pdm/models/environment.py
+++ b/src/pdm/models/environment.py
@@ -15,8 +15,8 @@ import unearth
 
 from pdm import termui
 from pdm.compat import cached_property
-from pdm.exceptions import BuildError
-from pdm.models.auth import make_basic_auth
+from pdm.exceptions import BuildError, PdmUsageError
+from pdm.models.auth import PdmBasicAuth
 from pdm.models.in_process import (
     get_pep508_environment,
     get_python_abi_tag,
@@ -82,7 +82,7 @@ class Environment:
         self.python_requires = project.python_requires
         self.project = project
         self.interpreter = project.python
-        self.auth = make_basic_auth(
+        self.auth = PdmBasicAuth(
             self.project.sources,
             self.project.core.ui.verbosity >= termui.Verbosity.DETAIL,
         )
@@ -149,6 +149,12 @@ class Environment:
         """
         if sources is None:
             sources = self.project.sources
+        if not sources:
+            raise PdmUsageError(
+                "You must specify at least one index in pyproject.toml or config.\n"
+                "The 'pypi.ignore_stored_index' config value is "
+                f"{self.project.config['pypi.ignore_stored_index']}"
+            )
 
         index_urls, find_links, trusted_hosts = get_index_urls(sources)
 
@@ -307,7 +313,7 @@ class BareEnvironment(Environment):
         self.python_requires = project.python_requires
         self.project = project
         self.interpreter = PythonInfo.from_path(sys.executable)
-        self.auth = make_basic_auth(
+        self.auth = PdmBasicAuth(
             self.project.sources,
             self.project.core.ui.verbosity >= termui.Verbosity.DETAIL,
         )
diff --git a/src/pdm/project/config.py b/src/pdm/project/config.py
index 8de47567..23411638 100644
--- a/src/pdm/project/config.py
+++ b/src/pdm/project/config.py
@@ -4,16 +4,16 @@ import collections
 import dataclasses
 import os
 from pathlib import Path
-from typing import Any, Callable, Iterator, Mapping, MutableMapping, TypeVar
+from typing import Any, Callable, Iterator, Mapping, MutableMapping, cast
 
 import platformdirs
 import rich.theme
 import tomlkit
 
 from pdm import termui
+from pdm._types import Source
 from pdm.exceptions import NoConfigError, PdmUsageError
 
-T = TypeVar("T")
 ui = termui.UI()
 
 REPOSITORY = "repository"
@@ -50,9 +50,11 @@ def load_config(file_path: Path) -> dict[str, Any]:
     """
 
     def get_item(sub_data: Mapping[str, Any]) -> Mapping[str, Any]:
-        result = {}
+        result: dict[str, Any] = {}
         for k, v in sub_data.items():
-            if k != REPOSITORY and isinstance(v, Mapping):
+            if k == "pypi":
+                result.update((f"{k}.{sub_k}", sub_v) for sub_k, sub_v in v.items())
+            elif k != REPOSITORY and isinstance(v, Mapping):
                 result.update(
                     {f"{k}.{sub_k}": sub_v for sub_k, sub_v in get_item(v).items()}
                 )
@@ -197,10 +199,22 @@ class Config(MutableMapping[str, str]):
         "pypi.verify_ssl": ConfigItem(
             "Verify SSL certificate when query PyPI", True, coerce=ensure_boolean
         ),
+        "pypi.username": ConfigItem(
+            "The username to access PyPI", env_var="PDM_PYPI_USERNAME"
+        ),
+        "pypi.password": ConfigItem(
+            "The password to access PyPI", env_var="PDM_PYPI_PASSWORD"
+        ),
         "pypi.ca_certs": ConfigItem(
             "Path to a CA certificate bundle used for verifying the identity "
             "of the PyPI server",
         ),
+        "pypi.ignore_stored_index": ConfigItem(
+            "Ignore the configured indexes",
+            False,
+            env_var="PDM_IGNORE_STORED_INDEX",
+            coerce=ensure_boolean,
+        ),
         "pypi.client_cert": ConfigItem(
             "Path to client certificate file, or combined cert/key file",
         ),
@@ -285,6 +299,11 @@ class Config(MutableMapping[str, str]):
     def self_data(self) -> dict[str, Any]:
         return dict(self._file_data)
 
+    def iter_sources(self) -> Iterator[Source]:
+        for name, data in self._data.items():
+            if name.startswith("pypi.") and name not in self._config_map:
+                yield cast(Source, dict(data, name=name))
+
     def _save_config(self) -> None:
         """Save the changed to config file."""
         self.config_file.parent.mkdir(parents=True, exist_ok=True)
@@ -314,6 +333,12 @@ class Config(MutableMapping[str, str]):
             if len(parts) >= 3 and parts[2] == "password" and value:
                 return "<hidden>"
             return value
+        elif parts[0] == "pypi" and key not in self._config_map:
+            index_key = ".".join(parts[:2])
+            if index_key not in self._data:
+                raise KeyError(f"No PyPI index named {parts[1]}")
+            source = self._data[index_key]
+            return source[parts[2]] if len(parts) >= 3 else source
 
         if key not in self._config_map and key not in self.deprecated:
             raise NoConfigError(key)
@@ -343,6 +368,13 @@ class Config(MutableMapping[str, str]):
             ).setdefault(parts[2], value)
             self._save_config()
             return
+        if parts[0] == "pypi" and key not in self._config_map:
+            if len(parts) < 3:
+                raise PdmUsageError("Set index config with [success]pypi.{name}.{attr}")
+            index_key = ".".join(parts[:2])
+            self._file_data.setdefault(index_key, {})[parts[2]] = value
+            self._save_config()
+            return
         if key not in self._config_map and key not in self.deprecated:
             raise NoConfigError(key)
         config_key = self.deprecated.get(key, key)
@@ -371,10 +403,10 @@ class Config(MutableMapping[str, str]):
     def __iter__(self) -> Iterator[str]:
         keys: set[str] = set()
         for key in self._data:
-            if key in self._config_map:
-                keys.add(key)
-            elif key in self.deprecated:
+            if key in self.deprecated:
                 keys.add(self.deprecated[key])
+            elif key != REPOSITORY:
+                keys.add(key)
         return iter(keys)
 
     def __delitem__(self, key: str) -> None:
@@ -388,6 +420,15 @@ class Config(MutableMapping[str, str]):
                 del self._file_data.get(REPOSITORY, {})[parts[1]]
             self._save_config()
             return
+        if parts[0] == "pypi" and key not in self._config_map:
+            if len(parts) < 2:
+                raise PdmUsageError("Should specify the name of index")
+            if len(parts) >= 3:
+                del self._file_data.get("pypi", {}).get(parts[1], {})[parts[2]]
+            else:
+                del self._file_data.get("pypi", {})[parts[1]]
+            self._save_config()
+            return
         config_key = self.deprecated.get(key, key)
         config = self._config_map[config_key]
         self._file_data.pop(config_key, None)
@@ -406,7 +447,7 @@ class Config(MutableMapping[str, str]):
     def get_repository_config(self, name_or_url: str) -> RepositoryConfig | None:
         """Get a repository by name or url."""
         if not self.is_global:  # pragma: no cover
-            raise PdmUsageError("No repository config in project config.")
+            raise NoConfigError("repository")
         repositories: Mapping[str, Mapping[str, str | None]] = self._data.get(
             REPOSITORY, {}
         )
diff --git a/src/pdm/project/core.py b/src/pdm/project/core.py
index f167ed54..4c06b6e8 100644
--- a/src/pdm/project/core.py
+++ b/src/pdm/project/core.py
@@ -7,7 +7,6 @@ import shutil
 import sys
 from pathlib import Path
 from typing import TYPE_CHECKING, Any, Iterable, cast
-from urllib.parse import urlparse
 
 import platformdirs
 import tomlkit
@@ -357,22 +356,21 @@ class Project:
                 "url": self.config["pypi.url"],
                 "verify_ssl": self.config["pypi.verify_ssl"],
                 "name": "pypi",
+                "username": self.config.get("pypi.username"),
+                "password": self.config.get("pypi.password"),
             },
         )
 
     @property
     def sources(self) -> list[Source]:
         sources = list(self.pyproject.settings.get("source", []))
-        if all(source.get("name") != "pypi" for source in sources):
-            sources.insert(0, self.default_source)
-        expanded_sources: list[Source] = [
-            Source(
-                url=expand_env_vars_in_auth(s["url"]),
-                verify_ssl=s.get("verify_ssl", True),
-                name=s.get("name", urlparse(s["url"]).hostname),
-                type=s.get("type", "index"),
-            )
-            for s in sources
+        if not self.global_config["pypi.ignore_stored_index"]:
+            if all(source.get("name") != "pypi" for source in sources):
+                sources.insert(0, self.default_source)
+            sources.extend(self.global_config.iter_sources())
+        expanded_sources = [
+            cast("Source", {**source, "url": expand_env_vars_in_auth(source["url"])})
+            for source in sources
         ]
         return expanded_sources
 


Test patch:
diff --git a/tests/test_project.py b/tests/test_project.py
index c6573387..16b13ce2 100644
--- a/tests/test_project.py
+++ b/tests/test_project.py
@@ -282,3 +282,44 @@ def test_iter_project_venvs(project):
     dot_venv_python.touch()
     venv_keys = [key for key, _ in utils.iter_venvs(project)]
     assert sorted(venv_keys) == ["bar", "baz", "foo", "in-project"]
+
+
+def test_load_extra_sources(project):
+    project.pyproject.settings["source"] = [
+        {
+            "name": "custom",
+            "url": "https://custom.pypi.org/simple",
+        }
+    ]
+    project.global_config["pypi.extra.url"] = "https://extra.pypi.org/simple"
+    sources = project.sources
+    assert len(sources) == 3
+    assert [item["name"] for item in sources] == ["pypi", "custom", "pypi.extra"]
+
+    project.global_config["pypi.ignore_stored_index"] = True
+    sources = project.sources
+    assert len(sources) == 1
+    assert [item["name"] for item in sources] == ["custom"]
+
+
+def test_no_index_raise_error(project):
+    project.global_config["pypi.ignore_stored_index"] = True
+    with pytest.raises(Exception, match="You must specify at least one index"):
+        with project.environment.get_finder():
+            pass
+
+
+@pytest.mark.internet
+def test_access_index_with_auth(project):
+    url = "https://httpbin.org/basic-auth/foo/bar"
+    project.global_config.update(
+        {
+            "pypi.extra.url": "https://httpbin.org",
+            "pypi.extra.username": "foo",
+            "pypi.extra.password": "bar",
+        }
+    )
+    with project.environment.get_finder() as finder:
+        session = finder.session
+        resp = session.get(url)
+        assert resp.ok