Update some Nomulus documentation (#2970)

This doesn't update everything -- it leaves out some of the more
complicated changes (architecture, code-structure, configuration,
install, and proxy-setup). Those will require more complete rewrites, so
I'm punting them to a future PR.
This commit is contained in:
gbrodman
2026-02-26 19:05:22 +00:00
committed by GitHub
parent 8a3ab00e58
commit c52983fb61
16 changed files with 629 additions and 872 deletions
+106 -188
View File
@@ -8,61 +8,41 @@ requests will be authorized to invoke the action.
## Authentication and authorization properties
The `auth` attribute is an enumeration. Each value of the enumeration
corresponds to a triplet of properties:
corresponds to a pair of properties:
* the *authentication methods* allowed by the action
* the *minimum authentication level* which is authorized to run the action
* the *user policy* for the action
* the *minimum authentication level* which is authorized to run the action
* the *user policy* for the action
### Authentication methods
### Authentication Levels
Authentication methods are ways whereby the request can authenticate itself to
the system. In the code, an *authentication mechanism* is a class which handles
a particular authentication method. There are currently three methods:
There exist three levels of authentication level:
* `INTERNAL`: used by requests generated from App Engine task queues; these
requests do not have a user, because they are system-generated, so
authentication consists solely of verifying that the request did indeed
come from a task queue
* `NONE`: no authentication was found
* `APP`: the request was authenticated, but no user was present
* `USER`: the request was authenticated with a specific user
* `API`: authentication using an API; the Nomulus release ships with one API
authentication mechanism, OAuth 2, but you can write additional custom
mechanisms to handle other protocols if needed
`NONE` and `USER` are fairly straightforward results (either no authentication
was present, or a user was present), but `APP` is a bit of a special case. It
exists for requests coming from service accounts, Cloud Scheduler, or the
proxy -- requests which are authenticated but don't necessarily come from any
one particular "user" per se. That being said, authorized users *can* manually
run these tasks; it's just that service accounts can too.
* `LEGACY`: authentication using the standard App Engine `UserService` API,
which authenticates based on cookies and XSRF tokens
The details of the associated authentication mechanism classes are given later.
### Authentication levels
Each authentication method listed above can authenticate at one of three levels:
* `NONE`: no authentication was found
* `APP`: the request was authenticated, but no user was present
* `USER`: the request was authenticated with a specific user
For instance, `INTERNAL` authentication never returns an authentication level of
`USER`, because internal requests generated from App Engine task queues do not
execute as a particular end user account. `LEGACY` authentication, on the other
hand, never returns an authentication level of `APP`, because authentication is
predicated on identifying the user, so the only possible answers are `NONE` and
`USER`.
Each action has a minimum request authentication level. Some actions are
completely open to the public, and have a minimum level of `NONE`. Some require
authentication but not a user, and have a minimum level of `APP`. And some
cannot function properly without knowing the exact user, and have a minimum
level of `USER`.
Each action has a minimum request authentication level. Some actions (e.g. RDAP)
are completely open to the public, and have a minimum level of `NONE`. Some
require authentication but not necessarily a user, and have a minimum level of
`APP`. And some cannot function properly without knowing the exact user, and
have a minimum level of `USER`.
### User policy
The user policy indicates what kind of user is authorized to execute the action.
There are three possible values:
There are two possible values:
* `IGNORED`: the user information is ignored
* `PUBLIC`: an authenticated user is required, but any user will do
* `ADMIN`: there must be an authenticated user with admin privileges
* `PUBLIC`: an authenticated user is required, but any user will do
(authorization is done at a later state)
* `ADMIN`: there must be an authenticated user with admin privileges (this
includes service accounts)
Note that the user policy applies only to the automatic checking done by the
framework before invoking the action. The action itself may do more checking.
@@ -73,10 +53,6 @@ whether a user was found. If not, it issues a redirect to the login page.
Likewise, other pages of the registrar console have a user policy of `PUBLIC`,
meaning that any logged-in user can access the page. However, the code then
looks up the user to make sure he or she is associated with a registrar.
Admins can be granted permission to the registrar console by configuring a
special registrar for internal admin use, using the `registryAdminClientId`
setting. See the [global configuration
guide](./configuration.md#global-configuration) for more details.
Also note that the user policy only applies when there is actually a user. Some
actions can be executed either by an admin user or by an internal request coming
@@ -87,64 +63,41 @@ require that there be a user, set the minimum authentication level to `USER`.
### Allowed authentication and authorization values
Not all triplets of the authentication method, minimum level and user policy
make sense. A master enumeration lists all the valid triplets. They are:
There are three pairs of authentication level + user policy that are used in
Nomulus (or even make sense). These are:
* `AUTH_PUBLIC_ANONYMOUS`: Allow all access, and don't attempt to authenticate.
The only authentication method is `INTERNAL`, with a minimum level of
`NONE`. Internal requests will be flagged as such, but everything else
passes the authorization check with a value of `NOT_AUTHENTICATED`.
* `AUTH_PUBLIC`: Allow all access, but attempt to authenticate the user. All
three authentication methods are specified, with a minimum level of `NONE`
and a user policy of `PUBLIC`. If the user can be authenticated by any
means, the identity is passed to the request. But if not, the request still
passes the authorization check, with a value of `NOT_AUTHENTICATED`.
* `AUTH_PUBLIC_LOGGED_IN`: Allow access only by authenticated users. The
`API` and `LEGACY` authentication methods are supported, but not `INTERNAL`,
because that does not identify a user. The minimum level is `USER`, with a
user policy of `PUBLIC`. Only requests with a user authenticated via either
the legacy, cookie-based method or an API method (e.g. OAuth 2) are
authorized to run the action.
* `AUTH_INTERNAL_OR_ADMIN`: Allow access only by admin users or internal
requests. This is appropriate for actions that should only be accessed by
someone trusted (as opposed to anyone with a Google login). This currently
allows only the `INTERNAL` and `API` methods, meaning that an admin user
cannot authenticate themselves via the legacy authentication mechanism,
which is used only for the registrar console. The minimum level is `APP`,
because we don't require a user for internal requests, but the user policy
is `ADMIN`, meaning that if there *is* a user, it needs to be an admin.
* `AUTH_PUBLIC_OR_INTERNAL`: Allows anyone access, as long as they use OAuth to
authenticate. Also allows access from App Engine task-queue. Note that OAuth
client ID still needs to be allow-listed in the config file for OAuth-based
authentication to succeed. This is mainly used by the proxy.
* `AUTH_PUBLIC`: Allow all access and don't attempt to authenticate. This is
used for completely public endpoints such as RDAP.
* `AUTH_PUBLIC_LOGGED_IN`: Allow access only by users authenticated with some
type of OAuth token. This allows all users (`UserPolicy.PUBLIC`) but
requires that a particular user exists and is logged in (`AuthLevel.USER`).
This is used primarily for the registrar console.
* `AUTH_ADMIN`: Allow access only by admin users or internal requests
(including Cloud Scheduler tasks). This is appropriate for actions that
should only be accessed by someone trusted (as opposed to anyone with a
Google login). This permits app-internal authentication (`AuthLevel.APP`)
but if a user is present, it must be an admin (`UserPolicy.ADMIN`). This is
used by many automated requests, as well as the proxy.
### Action setting golden files
To make sure that the authentication and authorization settings are correct for
all actions, a unit test uses reflection to compare all defined actions for a
specific service to a golden file containing the correct settings. These files
are:
To make sure that the authentication and authorization settings are correct and
expected for all actions, a unit test uses reflection to compare all defined
actions for a specific service to a
[golden file](https://github.com/google/nomulus/blob/master/core/src/test/resources/google/registry/module/routing.txt)
containing the correct settings.
* `frontend_routing.txt` for the default (frontend) service
* `backend_routing.txt` for the backend service
* `tools_routing.txt` for the tools service
Each line in the file lists a path, the class that handles that path, the
allowable HTTP methods (meaning GET and POST, as opposed to the authentication
methods described above), the value of the `automaticallyPrintOk` attribute (not
relevant for purposes of this document), and the two authentication and
authorization settings described above. Whenever actions are added, or their
attributes are modified, the golden file needs to be updated.
Each of these files consists of lines listing a path, the class that handles
that path, the allowable HTTP methods (meaning GET and POST, as opposed to the
authentication methods described above), the value of the `automaticallyPrintOk`
attribute (not relevant for purposes of this document), and the three
authentication and authorization settings described above. Whenever actions are
added, or their attributes are modified, the golden files need to be updated.
The golden files also serve as a convenient place to check out how things are
set up. For instance, the tools actions are, for the most part, accessible to
admins and internal requests only. The backend actions are mostly accessible
only to internal requests. And the frontend actions are a grab-bag; some are
open to the public, some to any user, some only to admins, etc.
The golden file also serves as a convenient place to check out how things are
set up. For instance, the backend actions are accessible to admins and internal
requests only, the pubapi requests are open to the public, and console requests
require an authenticated user.
### Example
@@ -156,11 +109,12 @@ body rather than the URL itself (which could be logged). Therefore, the class
definition looks like:
```java
@Action(
path = "/_dr/epp",
method = Method.POST,
auth = Auth.AUTH_INTERNAL_OR_ADMIN
)
service = Action.Service.FRONTEND,
path = "/_dr/epp",
method = Method.POST,
auth = Auth.AUTH_ADMIN)
public class EppTlsAction implements Runnable {
...
```
@@ -169,8 +123,8 @@ and the corresponding line in frontend_routing.txt (including the header line)
is:
```shell
PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
/_dr/epp EppTlsAction POST n INTERNAL,API APP ADMIN
SERVICE PATH CLASS METHODS OK MIN USER_POLICY
FRONTEND /_dr/epp EppTlsAction POST n APP ADMIN
```
## Implementation
@@ -178,16 +132,12 @@ PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
The code implementing the authentication and authorization framework is
contained in the `google.registry.request.auth` package. The main method is
`authorize()`, in `RequestAuthenticator`. This method takes the auth settings
and an HTTP request, and tries to authenticate and authorize the request using
any of the specified methods, returning the result of its attempts. Note that
failed authorization (in which case `authorize()` returns `Optional.absent()`)
is different from the case where nothing can be authenticated, but the action
does not require any; in that case, `authorize()` succeeds, returning the
special result AuthResult.NOT_AUTHENTICATED.
There are separate classes (described below) for the mechanism which handles
each authentication method. The list of allowable API authentication mechanisms
(by default, just OAuth 2) is configured in `AuthModule`.
and an HTTP request, and tries to authenticate and authorize the request,
returning the result of its attempts. Note that failed authorization (in which
case `authorize()` returns `Optional.absent()`) is different from the case where
nothing can be authenticated, but the action does not require any; in that case,
`authorize()` succeeds, returning the special result
AuthResult.NOT_AUTHENTICATED.
The ultimate caller of `authorize()` is
`google.registry.request.RequestHandler`, which is responsible for routing
@@ -196,83 +146,51 @@ appropriate action, and making sure that the incoming HTTP method is appropriate
for the action, it calls `authorize()`, and rejects the request if authorization
fails.
### `LegacyAuthenticationMechanism`
### Authentication methods
Legacy authentication is straightforward, because the App Engine `UserService`
API does all the work. Because the protocol might be vulnerable to an XSRF
attack, the authentication mechanism issues and checks XSRF tokens as part
of the process if the HTTP method is not GET or HEAD.
Nomulus requests are authenticated via OIDC token authentication, though these
tokens can be created and validated in two ways. In each case, the
authentication mechanism converts an HTTP request to an authentication result,
which consists of an authentication level, a possible user object, and a
possible service account email.
### `OAuthAuthenticationMechanism`
#### `IapOidcAuthenticationMechanism`
OAuth 2 authentication is performed using the App Engine `OAuthService` API.
There are three Nomulus configuration values involved:
Most requests, e.g. the registrar console or Nomulus CLI requests) are routed
through GCP's
[Identity-Aware Proxy](https://docs.cloud.google.com/iap/docs/concepts-overview).
This forces the user to log in to some GAIA account (specifically, one that is
given access to the project). We attempt to validate a provided IAP OIDC token
with the IAP issuer URL (`https://cloud.google.com/iap`) and the proper IAP
audience (`/projects/{projectId}/global/backendServices/{serviceId}`), where
`projectId` refers to the GCP project, and `serviceId` refers to the service ID
retrievable from the
[IAP configuration page](https://pantheon.corp.google.com/security/iap).
Ideally, this service ID corresponds to the HTTPS load balancer that distributes
requests to the GKE pods.
* `availableOauthScopes` is the set of OAuth scopes passed to the service to
be checked for their presence.
Note: the local Nomulus CLI's
[LoginCommand](https://github.com/google/nomulus/blob/master/core/src/main/java/google/registry/tools/LoginCommand.java)
uses a special-case form of this where it saves long-lived IAP credentials
locally.
* `requiredOauthScopes` is the set of OAuth scopes which must be present. This
should be a subset of the available scopes. All scopes in this set must be
present for authentication to succeed.
#### `RegularOidcAuthenticationMechanism`
* `allowedOauthClientIds` is the set of allowable OAuth client IDs. Any client
ID in this set is sufficient for successful authentication.
Service account requests ( e.g.
[Cloud Scheduler jobs](https://docs.cloud.google.com/scheduler/docs/schedule-run-cron-job))
or requests coming through the proxy use a non-IAP OIDC token provided by the
caller. These requests have a different issuer URL (
`https://accounts.google.com`) and use the fairly standard OAuth bearer token
architecture -- an `Authorization` HTTP header of the form "Bearer: XXXX".
The code looks for an `Authorization` HTTP header of the form "BEARER XXXX...",
containing the access token. If it finds one, it calls `OAuthService` to
validate the token, check that the scopes and client ID match, and retrieve the
flag indicating whether the user is an admin.
### Configuration
### `AppEngineInternalAuthenticationMechanism`
Detection of internal requests is a little hacky. App Engine uses a special HTTP
header, `X-AppEngine-QueueName`, to indicate the queue from which the request
originates. If this header is present, internal authentication succeeds. App
Engine normally strips this header from external requests, so only internal
requests will be authenticated.
App Engine has a special carve-out for admin users, who are allowed to specify
headers which do not get stripped. So an admin user can use a command-line
utility like `curl` to craft a request which appears to Nomulus to be an
internal request. This has proven to be useful, facilitating the testing of
actions which otherwise could only be run via a dummy cron job.
However, it only works if App Engine can authenticate the user as an admin via
the `UserService` API. OAuth won't work, because authentication is performed by
the Nomulus code, and the headers will already have been stripped by App Engine
before the request is executed. Only the legacy, cookie-based method will work.
Be aware that App Engine defines an "admin user" as anyone with access to the
App Engine project, even those with read-only access.
## Other topics
### OAuth 2 not supported for the registry console
Currently, OAuth 2 is only supported for requests which specify the
`Authorization` HTTP header. The OAuth code reads this header and passes it to
the Google OAuth server (no other authentication servers are currently
supported) to verify the user's identity. This works fine for the `nomulus`
command-line tool.
It doesn't work for browser-based interactions such as the registrar console.
For that, we will (we think) need to redirect the user to the authentication
server, and upon receiving the user back, fish out the code and convert it to a
token which we store in a cookie. None of this is particularly hard, but for the
moment it seems easier to stick with the legacy App Engine UserService API. Of
course, contributions from the open-source community are welcome. :)
### Authorization via `web.xml`
Before the modern authentication and authorization framework described in this
document was put in place, Nomulus used to be protected by directives in the
`web.xml` file which allowed only logged-in users to access most endpoints. This
had the advantage of being very easy to implement, but it came with some
drawbacks, the primary one being lack of support for OAuth 2. App Engine's
standard login detection works fine when using a browser, but does not handle
cases where the request is coming from a standalone program such as the
`nomulus` command-line tool. By moving away from the `web.xml` approach, we
gained more flexibility to support an array of authentication and authorization
schemes, including custom ones developed by the Nomulus community, at the
expense of having to perform the authentication and authorization ourselves in
the code.
The `auth` block of the configuration requires two fields: *
`allowedServiceAccountEmails` is the list of service accounts that should be
allowed to run tasks when internally authenticated. This will likely include
whatever service account runs Nomulus in Google Kubernetes Engine, as well as
the Cloud Scheduler service account. * `oauthClientId` is the OAuth client ID
associated with IAP. This is retrievable from the
[Clients page](https://pantheon.corp.google.com/auth/clients) of GCP after
enabling the Identity-Aware Proxy. It should look something like
`someNumbers-someNumbersAndLetters.apps.googleusercontent.com`