Add support for Direct Workload Identity auth (#348)

This adds a new authentication mode, Direct Workload Identity
Federation. This new mode permits authenticating to Google Cloud
directly using the GitHub Actions OIDC token instead of proxying through
a Google Cloud Service Account.
This commit is contained in:
Seth Vargo 2023-11-28 10:41:10 -05:00 committed by GitHub
parent f105ef0cdb
commit fe9207673e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 2186 additions and 1311 deletions

View File

@ -8,4 +8,9 @@ module.exports = {
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
// We have many situations where we accept and expect arbitrary JSON payloads.
rules: {
'@typescript-eslint/no-explicit-any': 'off',
},
};

View File

@ -27,6 +27,10 @@ concurrency:
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
cancel-in-progress: true
defaults:
run:
shell: 'bash'
jobs:
unit:
name: 'unit'
@ -48,9 +52,13 @@ jobs:
- name: 'npm test'
run: 'npm run test'
credentials_json:
#
# Direct Workload Identity Federation
#
direct_workload_identity_federation:
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'credentials_json'
name: 'direct_workload_identity_federation'
runs-on: '${{ matrix.os }}'
strategy:
fail-fast: false
@ -60,6 +68,9 @@ jobs:
- 'windows-latest'
- 'macos-latest'
permissions:
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
@ -74,56 +85,33 @@ jobs:
name: 'auth-default'
uses: './'
with:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
project_id: '${{ vars.PROJECT_ID }}'
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
- id: 'setup-gcloud'
name: 'setup-gcloud'
uses: 'google-github-actions/setup-gcloud@main'
- uses: 'google-github-actions/setup-gcloud@main'
with:
version: '>= 363.0.0'
- id: 'gcloud'
name: 'gcloud'
shell: 'bash'
- name: 'gcloud'
run: |-
gcloud secrets versions access "latest" --secret "${{ vars.SECRET_NAME }}"
- id: 'auth-access-token'
name: 'auth-access-token'
uses: './'
with:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
token_format: 'access_token'
- id: 'access-token'
name: 'access-token'
shell: 'bash'
- id: 'oauth-federated-token'
name: 'oauth-federated-token'
run: |-
curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/${{ vars.SECRET_NAME }}/versions/latest:access \
curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-default.outputs.project_id }}/secrets/${{ vars.SECRET_NAME }}/versions/latest:access \
--silent \
--show-error \
--fail \
--header "Authorization: Bearer ${{ steps.auth-access-token.outputs.access_token }}"
--header "Authorization: Bearer ${{ steps.auth-default.outputs.auth_token }}"
- id: 'auth-id-token'
name: 'auth-id-token'
uses: './'
with:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
token_format: 'id_token'
id_token_audience: 'https://secretmanager.googleapis.com/'
id_token_include_email: true
- id: 'auth-sa-retries'
name: 'auth-sa-retries'
uses: './'
with:
retries: '2'
backoff: '200'
backoff_limit: '1000'
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
workload_identity_federation:
#
# Workload Identity Federation through a Service Account
#
workload_identity_federation_through_service_account:
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'workload_identity_federation'
name: 'workload_identity_federation_through_service_account'
runs-on: '${{ matrix.os }}'
strategy:
fail-fast: false
@ -153,13 +141,11 @@ jobs:
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
- id: 'setup-gcloud'
name: 'setup-gcloud'
uses: 'google-github-actions/setup-gcloud@main'
- uses: 'google-github-actions/setup-gcloud@main'
with:
version: '>= 363.0.0'
- id: 'gcloud'
name: 'gcloud'
shell: 'bash'
- name: 'gcloud'
run: |-
gcloud secrets versions access "latest" --secret "${{ vars.SECRET_NAME }}"
@ -171,9 +157,74 @@ jobs:
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
token_format: 'access_token'
- id: 'oauth-token'
name: 'oauth-token'
run: |-
curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/${{ vars.SECRET_NAME }}/versions/latest:access \
--silent \
--show-error \
--fail \
--header "Authorization: Bearer ${{ steps.auth-access-token.outputs.access_token }}"
- id: 'id-token'
name: 'id-token'
uses: './'
with:
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
token_format: 'id_token'
id_token_audience: 'https://secretmanager.googleapis.com/'
id_token_include_email: true
#
# Service Account Key JSON
#
credentials_json:
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'credentials_json'
runs-on: '${{ matrix.os }}'
strategy:
fail-fast: false
matrix:
os:
- 'ubuntu-latest'
- 'windows-latest'
- 'macos-latest'
steps:
- uses: 'actions/checkout@v4'
- uses: 'actions/setup-node@v4'
with:
node-version: '20.x'
- name: 'npm build'
run: 'npm ci && npm run build'
- id: 'auth-default'
name: 'auth-default'
uses: './'
with:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
- uses: 'google-github-actions/setup-gcloud@main'
with:
version: '>= 363.0.0'
- name: 'gcloud'
run: |-
gcloud secrets versions access "latest" --secret "${{ vars.SECRET_NAME }}"
- id: 'auth-access-token'
name: 'auth-access-token'
uses: './'
with:
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
token_format: 'access_token'
- id: 'access-token'
name: 'access-token'
shell: 'bash'
run: |-
curl https://secretmanager.googleapis.com/v1/projects/${{ steps.auth-access-token.outputs.project_id }}/secrets/${{ vars.SECRET_NAME }}/versions/latest:access \
--silent \
@ -185,26 +236,26 @@ jobs:
name: 'auth-id-token'
uses: './'
with:
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
token_format: 'id_token'
id_token_audience: 'https://secretmanager.googleapis.com/'
id_token_include_email: true
- id: 'auth-wif-retries'
name: 'auth-wif-retries'
- id: 'auth-sa-retries'
name: 'auth-sa-retries'
uses: './'
with:
retries: '2'
backoff: '200'
backoff_limit: '1000'
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
#
# This test ensures that the GOOGLE_APPLICATION_CREDENTIALS environment
# variable is shared with the container and that the path of the file is on
# the shared filesystem with the container and that the USER for the container
# has permissions to read the file.
#
docker:
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
name: 'docker'

780
README.md
View File

@ -1,4 +1,4 @@
# auth
# Authenticate to Google Cloud from GitHub Actions
This GitHub Action authenticates to Google Cloud. It supports authentication via
a Google Cloud Service Account Key JSON and authentication via [Workload
@ -7,20 +7,12 @@ Identity Federation][wif].
Workload Identity Federation is recommended over Service Account Keys as it
obviates the need to export a long-lived credential and establishes a trust
delegation relationship between a particular GitHub Actions workflow invocation
and permissions on Google Cloud.
and permissions on Google Cloud. There are three ways to set up this GitHub
Action to authenticate to Google Cloud:
#### With Service Account Key JSON
1. Create a Google Cloud service account and grant IAM permissions
1. Export the long-lived JSON service account key
1. Upload the JSON service account key to a GitHub secret
#### With Workload Identity Federation
1. Create a Google Cloud service account and grant IAM permissions
1. Create and configure a Workload Identity Provider for GitHub
1. Exchange the GitHub Actions OIDC token for a short-lived Google Cloud access
token
1. [(Preferred) Direct Workload Identity Federation](#direct-wif)
1. [Workload Identity Federation through a Service Account](#indirect-wif)
1. [Service Account Key JSON](#sake)
**This is not an officially supported Google product, and it is not covered by a
Google Cloud support contract. To report bugs or request features in a Google
@ -30,30 +22,25 @@ support](https://cloud.google.com/support).**
## Prerequisites
- For authenticating via Google Cloud Service Account Keys, you must create and
export a Google Cloud Service Account Key in JSON format.
- For authenticating via Workload Identity Federation, you must create and
configure a Google Cloud Workload Identity Provider. See [setup](#setup)
for instructions.
- You must run the `actions/checkout@v4` step _before_ this action. Omitting
the checkout step or putting it after `auth` will cause future steps to be
- Run the `actions/checkout@v4` step _before_ this action. Omitting the
checkout step or putting it after `auth` will cause future steps to be
unable to authenticate.
- If you plan to create binaries, containers, pull requests, or other
releases, add the following to your `.gitignore` to prevent accidentially
committing credentials to your release artifact:
- To create binaries, containers, pull requests, or other releases, add the
following to your `.gitignore` to prevent accidentially committing
credentials to your release artifact:
```text
# Ignore generated credentials from google-github-actions/auth
gha-creds-*.json
```
- This action runs using Node 20. If you are using self-hosted GitHub Actions
runners, you must use a [runner
- To use the `bq` or `gsutil` tools, use the Google Cloud SDK version 390.0.0
or newer.
- This action runs using Node 20. Use a [runner
version](https://github.com/actions/virtual-environments) that supports this
version or newer.
version of Node or newer.
## Usage
@ -61,99 +48,102 @@ support](https://cloud.google.com/support).**
```yaml
jobs:
job_id:
# ...
# Add "id-token" with the intended permissions.
permissions:
contents: 'read'
id-token: 'write'
steps:
# actions/checkout MUST come before auth
- uses: 'actions/checkout@v4'
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
- uses: 'google-github-actions/auth@v2'
with:
project_id: 'my-project'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
# ... further steps are automatically authenticated
```
Note that changing the `permissions` block may remove some default permissions.
See the [permissions documentation][github-perms] for more information.
> **⚠️ NOTE!** Changing the `permissions` block may remove some default
> permissions. See the [permissions documentation][github-perms] for more
> information.
See [Examples](#examples) for more examples. For help debugging common errors, see [Troubleshooting](docs/TROUBLESHOOTING.md)
For more usage options, see the [examples](docs/EXAMPLES.md).
## Inputs
### Authenticating via Workload Identity Federation
### Inputs: Workload Identity Federation
> **⚠️ WARNING!** This option is [not supported by Firebase Admin
> SDK](https://github.com/firebase/firebase-admin-node/issues/1377). Use
> Service Account Key JSON authentication instead.
The following inputs are for _authenticating_ to Google Cloud via Workload
Identity Federation.
**⚠️ You must use the Cloud SDK version 390.0.0 or later to authenticate the
`bq` and `gsutil` tools.**
**⚠️ Firebase users:** If you're using this Action to authenticate the Firebase
Admin Node.js SDK, you must authenticate with a service account key since
Workload Identity Federation is not yet supported. See
[#1377](https://github.com/firebase/firebase-admin-node/issues/1377)
for the status of WLIF support.
- `workload_identity_provider`: (Required) The full identifier of the Workload Identity
Provider, including the project number, pool name, and provider name. If
provided, this must be the full identifier which includes all parts:
- `workload_identity_provider`: (Required) The full identifier of the Workload
Identity Provider, including the project number, pool name, and provider
name. If provided, this must be the full identifier which includes all
parts:
```text
projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider
```
- `service_account`: (Required) Email address or unique identifier of the Google Cloud
service account for which to generate credentials. For example:
- `service_account`: (Optional) Email address or unique identifier of the
Google Cloud service account for which to impersonate and generate
credentials. For example:
```text
my-service-account@my-project.iam.gserviceaccount.com
```
Without this input, the GitHub Action will use [Direct Workload Identity
Federation](#direct-wif). If this input is provided, the GitHub Action will use
[Workload Identity Federation through a Service Account](#indirect-wif).
- `audience`: (Optional) The value for the audience (`aud`) parameter in the
generated GitHub Actions OIDC token. This value defaults to the value of
`workload_identity_provider`, which is also the default value Google Cloud
expects for the audience parameter on the token. We do not recommend
changing this value.
expects for the audience parameter on the token.
### Authenticating via Service Account Key JSON
### Inputs: Service Account Key JSON
> **⚠️ WARNING!** Service Account Key JSON credentials are long-lived
> credentials and must be treated like a password.
The following inputs are for _authenticating_ to Google Cloud via a Service
Account Key JSON. **We recommend using Workload Identity Federation instead as
exporting a long-lived Service Account Key JSON credential poses a security
risk.**
Account Key JSON.
- `credentials_json`: (Required) The Google Cloud JSON service account key to
use for authentication. To generate access tokens or ID tokens using this
service account, you must grant the underlying service account
`roles/iam.serviceAccountTokenCreator` permissions on itself.
- `credentials_json`: (Required) The Google Cloud Service Account Key JSON to
use for authentication.
Note: we strongly advise that you "compress" your JSON into a single line
string before storing it in a GitHub Secret. When a GitHub Secret is used in
a GitHub Actions workflow, _each line_ of the secret is masked in log
output. This can lead to aggressive sanitization of benign characters like
curly braces (`{}`) and brackets (`[]`). See
[Troubleshooting](docs/TROUBLESHOOTING.md#aggressive-replacement) for more
information.
We advise minifying your JSON into a single line string before storing it in
a GitHub Secret. When a GitHub Secret is used in a GitHub Actions workflow,
_each line_ of the secret is masked in log output. This can lead to
aggressive sanitization of benign characters like curly braces (`{}`) and
brackets (`[]`).
### Generating OAuth 2.0 access tokens
To generate access tokens or ID tokens using this service account, you must
grant the underlying service account `roles/iam.serviceAccountTokenCreator`
permissions on itself.
### Inputs: Generating OAuth 2.0 access tokens
The following inputs are for _generating_ OAuth 2.0 access tokens for
authenticating to Google Cloud as an output for use in future steps in the
workflow. These options only apply to access tokens generated by this action. By
default, this action does not generate any tokens.
- `token_format`: This value must be `"access_token"` to generate OAuth 2.0
access tokens. To skip token generation, omit or set to the empty string "".
- `service_account`: (Required) Email address or unique identifier of the
Google Cloud service account for which to generate the access token. For
example:
```text
my-service-account@my-project.iam.gserviceaccount.com
```
- `token_format`: (Required) This value must be `"access_token"` to generate
OAuth 2.0 access tokens.
- `access_token_lifetime`: (Optional) Desired lifetime duration of the access
token, in seconds. This must be specified as the number of seconds with a
@ -188,15 +178,22 @@ default, this action does not generate any tokens.
You will also need to customize the `access_token_scopes` value to
correspond to the OAuth scopes required for the API(s) you will access.
### Generating ID tokens
### Inputs: Generating ID tokens
The following inputs are for _generating_ ID tokens for authenticating to Google
Cloud as an output for use in future steps in the workflow. These options only
apply to ID tokens generated by this action. By default, this action does not
generate any tokens.
- `token_format`: This value must be `"id_token"` to generate ID tokens. To
skip token generation, omit or set to the empty string "".
- `service_account`: (Required) Email address or unique identifier of the
Google Cloud service account for which to generate the ID token. For
example:
```text
my-service-account@my-project.iam.gserviceaccount.com
```
- `token_format`: This value must be `"id_token"` to generate ID tokens.
- `id_token_audience`: (Required) The audience for the generated ID Token.
@ -205,7 +202,11 @@ generate any tokens.
will contain "email" and "email_verified" claims. This is only valid when
"token_format" is "id_token". The default value is false.
### Retry inputs
### Inputs: Retry options
The following inputs are for controlling retry behavior. By default, this GitHub
Action will retry API calls in an attempt to reduce transient failures. You can
control and disable the retry behavior with these inputs.
- `retries`: (Optional) Number of times to retry a failed authentication
attempt. This is useful for automated pipelines that may execute before IAM
@ -219,7 +220,7 @@ generate any tokens.
- `backoff_limit`: (Optional) Limits the retry backoff to the specified value.
The default value is no limit.
### Other inputs
### Inputs: Miscellaneous
The following inputs are for controlling the behavior of this GitHub Actions,
regardless of the authentication mechanism.
@ -233,18 +234,18 @@ regardless of the authentication mechanism.
and Google Cloud SDKs in other steps in the workflow. The default is true.
The credentials file is exported into `$GITHUB_WORKSPACE`, which makes it
available to all future steps and filesystems (including Docker-based
GitHub Actions). The file is automatically removed at the end of the job
via a post action. In order to use exported credentials, you **must** add
the `actions/checkout` step before calling `auth`. This is due to how
GitHub Actions creates `$GITHUB_WORKSPACE`:
available to all future steps and filesystems (including Docker-based GitHub
Actions). The file is automatically removed at the end of the job via a post
action. In order to use exported credentials, you **must** add the
`actions/checkout` step before calling `auth`. This is due to how GitHub
Actions creates `$GITHUB_WORKSPACE`:
```yaml
jobs:
job_id:
steps:
- uses: 'actions/checkout@v4' # Must come first!
- uses: 'google-github-actions/auth@v1'
- uses: 'google-github-actions/auth@v2'
```
- `export_environment_variables`: (Optional) If true, the action will export
@ -257,7 +258,7 @@ regardless of the authentication mechanism.
- `GCLOUD_PROJECT`
- `GOOGLE_CLOUD_PROJECT`
If "create_credentials_file" is true, additional environment variables are
If `create_credentials_file` is true, additional environment variables are
exported:
- `CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE`
@ -284,318 +285,88 @@ regardless of the authentication mechanism.
credentials file resides. This is only available if
"create_credentials_file" was set to true.
- `auth_token`: The Google Cloud federated token (for Workload Identity
Federation) or self-signed JWT (for a Service Account Key JSON). This output
is always available.
- `access_token`: The Google Cloud access token for calling other Google Cloud
APIs. This is only available when "token_format" is "access_token".
- `access_token_expiration`: The RFC3339 UTC "Zulu" format timestamp for the
access token. This is only available when "token_format" is "access_token".
- `id_token`: The Google Cloud ID token. This is only available when
"token_format" is "id_token".
## Examples
### Authenticating via Workload Identity Federation
This example demonstrates authenticating via Workload Identity Federation. For
more information on how to setup and configure Workload Identity Federation, see
[#setup](#setup).
```yaml
jobs:
job_id:
# ...
# Add "id-token" with the intended permissions.
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
```
### Authenticating via Service Account Key JSON
This example demonstrates authenticating via a Google Cloud Service Account Key
JSON. **We recommend using Workload Identity Federation instead as exporting a
long-lived Service Account Key JSON credential poses a security risk.**
This example assumes you have created a GitHub Secret named 'GOOGLE_CREDENTIALS'
with the contents being an export Google Cloud Service Account Key JSON. See
[Creating and managing Google Cloud Service Account
Keys](https://cloud.google.com/iam/docs/creating-managing-service-account-keys)
for more information.
```yaml
jobs:
job_id:
# ...
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}'
```
### Authenticating to Container Registry and Artifact Registry
This example demonstrates authenticating to Google Container Registry (GCR) or
Google Artifact Registry (GAR). The most common way to authenticate to these
services is via a gcloud docker proxy. However, you can authenticate to these
registries directly using the `auth` action:
- **Username:** `oauth2accesstoken`
- **Password:** `${{ steps.auth.outputs.access_token }}`
You must set `token_format: access_token` in your Action YAML. Here are a few
examples:
```yaml
jobs:
job_id:
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
token_format: 'access_token'
# Either user Workload Identity Federation or Service Account Keys. See
# above more more examples
# This example uses the docker login action
- uses: 'docker/login-action@v1'
with:
registry: 'gcr.io' # or REGION-docker.pkg.dev
username: 'oauth2accesstoken'
password: '${{ steps.auth.outputs.access_token }}'
# This example runs "docker login" directly to Artifact Registry.
- run: |-
echo '${{ steps.auth.outputs.access_token }}' | docker login -u oauth2accesstoken --password-stdin https://REGION-docker.pkg.dev
# This example runs "docker login" directly to Container Registry.
- run: |-
echo '${{ steps.auth.outputs.access_token }}' | docker login -u oauth2accesstoken --password-stdin https://gcr.io
```
### Configuring gcloud
This example demonstrates using this GitHub Action to configure authentication
for the `gcloud` CLI tool.
**Warning!** Workload Identity Federation requires Cloud SDK (`gcloud`) version
[363.0.0](https://cloud.google.com/sdk/docs/release-notes#36300_2021-11-02) or
later.
```yaml
jobs:
job_id:
# ...
# Add "id-token" with the intended permissions.
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
# Configure Workload Identity Federation via a credentials file.
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
# Install gcloud, `setup-gcloud` automatically picks up authentication from `auth`.
- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'
# Now you can run gcloud commands authenticated as the impersonated service account.
- id: 'gcloud'
name: 'gcloud'
run: |-
gcloud secrets versions access "latest" --secret "my-secret"
```
### Generating an OAuth 2.0 Access Token
This example demonstrates using this GitHub Action to generate an OAuth 2.0
Access Token for authenticating to Google Cloud. Most Google Cloud APIs accept
this access token as authentication.
The default lifetime is 1 hour, but you can request up to 12 hours if you set
the [`constraints/iam.allowServiceAccountCredentialLifetimeExtension` organization policy][orgpolicy-creds-lifetime].
Note: If you authenticate via `credentials_json`, the service account must have
`roles/iam.serviceAccountTokenCreator` on itself.
```yaml
jobs:
job_id:
# ...
# Add "id-token" with the intended permissions.
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
# Configure Workload Identity Federation and generate an access token.
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
token_format: 'access_token' # <--
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
access_token_lifetime: '300s' # optional, default: '3600s' (1 hour)
# Example of using the output. The token is usually provided as a Bearer
# token.
- id: 'access-secret'
run: |-
curl https://secretmanager.googleapis.com/v1/projects/my-project/secrets/my-secret/versions/1:access \
--header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}"
```
### Generating an ID Token (JWT)
This example demonstrates using this GitHub Action to generate a Google Cloud ID
Token for authenticating to Google Cloud. This is most commonly used when
invoking a Cloud Run service.
Note: If you authenticate via `credentials_json`, the service account must have
`roles/iam.serviceAccountTokenCreator` on itself.
```yaml
jobs:
job_id:
# ...
# Add "id-token" with the intended permissions.
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
# Configure Workload Identity Federation and generate an access token.
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v1'
with:
token_format: 'id_token' # <--
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
id_token_audience: 'https://myapp-uvehjacqzq.a.run.app' # required, value depends on target
id_token_include_email: true # optional
# Example of using the output. The token is usually provided as a Bearer
# token.
- id: 'invoke-service'
run: |-
curl https://myapp-uvehjacqzq.a.run.app \
--header "Authorization: Bearer ${{ steps.auth.outputs.id_token }}"
```
<a id="setup"></a>
## Setup
## Setting up Workload Identity Federation
This section describes the three configuration options:
To exchange a GitHub Actions OIDC token for a Google Cloud access token, you
must create and configure a Workload Identity Provider. These instructions use
the [gcloud][gcloud] command-line tool.
1. [(Preferred) Direct Workload Identity Federation](#direct-wif)
1. [Workload Identity Federation through a Service Account](#indirect-wif)
1. [Service Account Key JSON](#sake)
Alternatively, you can also use the [gh-oidc](https://github.com/terraform-google-modules/terraform-google-github-actions-runners/tree/master/modules/gh-oidc)
Terraform module to automate your infrastructure provisioning. See [examples](https://github.com/terraform-google-modules/terraform-google-github-actions-runners/tree/master/examples/oidc-simple) for usage.
> **⚠️ NOTE!** It can take up to 5 minutes for Workload Identity Pools, Workload
> Identity Providers, and IAM permissions to propagate. Please wait at least
> five minutes and follow all [Troubleshooting steps](docs/TROUBLESHOOTING.md)
> before opening an issue.
1. Create or use an existing Google Cloud project. You must have privileges to
create Workload Identity Pools, Workload Identity Providers, and to manage
Service Accounts and IAM permissions. Save your project ID as an environment
variable. The rest of these steps assume this environment variable is set:
```sh
export PROJECT_ID="my-project" # update with your value
```
<a name="direct-wif" id="direct-wif"></a>
### (Preferred) Direct Workload Identity Federation
1. (Optional) Create a Google Cloud Service Account. If you already have a
Service Account, take note of the email address and skip this step.
In this setup, the Workload Identity Pool has direct IAM permissions on Google
Cloud resources; there are no intermediate service accounts or keys. This is
preferred since it directly authenticates GitHub Actions to Google Cloud without
a proxy resource. However, not all Google Cloud resources support `principalSet`
identities. Please see the documentation for your Google Cloud service for more
information.
```sh
gcloud iam service-accounts create "my-service-account" \
--project "${PROJECT_ID}"
```
[![Authenticate to Google Cloud from GitHub Actions with Direct Workload Identity Federation](docs/google-github-actions-auth-direct-workload-identity-federation.svg)](docs/google-github-actions-auth-direct-workload-identity-federation.svg)
1. (Optional) Grant the Google Cloud Service Account permissions to access
Google Cloud resources. This step varies by use case. For demonstration
purposes, you could grant access to a Google Secret Manager secret or Google
Cloud Storage object.
> **⚠️ NOTE!** To generate OAuth 2.0 access tokens or ID tokens, you _must_
> provide a service account email, and the Workload Identity Pool must have
> `roles/iam.workloadIdentityUser` permissions on the target Google Cloud
> Service Account. Follow the steps for Workload Identity Federation through a
> Service Account instead.
1. Enable the IAM Credentials API:
<details>
<summary>Click here to show detailed instructions for configuring GitHub authentication to Google Cloud via a direct Workload Identity Federation.</summary>
```sh
gcloud services enable iamcredentials.googleapis.com \
--project "${PROJECT_ID}"
```
These instructions use the [gcloud][gcloud] command-line tool.
1. Create a Workload Identity Pool:
```sh
gcloud iam workload-identity-pools create "my-pool" \
gcloud iam workload-identity-pools create "github" \
--project="${PROJECT_ID}" \
--location="global" \
--display-name="Demo pool"
--display-name="GitHub Actions Pool"
```
1. Get the full ID of the Workload Identity **Pool**:
```sh
gcloud iam workload-identity-pools describe "my-pool" \
gcloud iam workload-identity-pools describe "github" \
--project="${PROJECT_ID}" \
--location="global" \
--format="value(name)"
```
Save this value as an environment variable:
This value should be of the format:
```sh
export WORKLOAD_IDENTITY_POOL_ID="..." # value from above
# This should look like:
#
# projects/123456789/locations/global/workloadIdentityPools/my-pool
#
```text
projects/123456789/locations/global/workloadIdentityPools/github
```
1. Create a Workload Identity **Provider** in that pool:
```sh
gcloud iam workload-identity-pools providers create-oidc "my-provider" \
gcloud iam workload-identity-pools providers create-oidc "my-repo" \
--project="${PROJECT_ID}" \
--location="global" \
--workload-identity-pool="my-pool" \
--display-name="Demo provider" \
--workload-identity-pool="github" \
--display-name="My GitHub repo Provider" \
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
--issuer-uri="https://token.actions.githubusercontent.com"
```
@ -605,22 +376,140 @@ Terraform module to automate your infrastructure provisioning. See [examples](ht
the principal invoking the GitHub Action). These can be used to further
restrict the authentication using `--attribute-condition` flags.
The example above only maps the `actor` and `repository` values. To map
additional values, add them to the attribute map:
> **❗️ NOTE!** You must map any claims in the incoming token to attributes
> before you can assert on those attributes in a CEL expression or IAM
> policy!**
1. Extract the Workload Identity **Provider** resource name:
```sh
--attribute-mapping="google.subject=assertion.sub,attribute.repository_owner=assertion.repository_owner"
gcloud iam workload-identity-pools providers describe "my-repo" \
--project="${PROJECT_ID}" \
--location="global" \
--workload-identity-pool="github" \
--format="value(name)"
```
**You must map any claims in the incoming token to attributes before you can
assert on those attributes in a CEL expression or IAM policy!**
Use this value as the `workload_identity_provider` value in the GitHub
Actions YAML:
1. Allow authentications from the Workload Identity Provider originating from
your repository to impersonate the Service Account created above:
```yaml
- uses: 'google-github-actions/auth@v2'
with:
project_id: 'my-project'
workload_identity_provider: '...' # "projects/123456789/locations/global/workloadIdentityPools/github/providers/my-repo"
```
> **⚠️ NOTE!** The `project_id` input is optional, but may be required by
> downstream authentication systems such as the `gcloud` CLI. Unfortunately
> we cannot extract the project ID from the Workload Identity Provider,
> since it requires the project _number_.
>
> It is technically possible to convert a project _number_ into a project
> _ID_, but it requires permissions to call Cloud Resource Manager, and we
> cannot guarantee that the Workload Identity Pool has those permissions.
1. As needed, allow authentications from the Workload Identity Pool to Google
Cloud resources. These can be any Google Cloud resources that support
federated ID tokens, and it can be done after the GitHub Action is
configured.
The following example shows granting access from a GitHub Action in a
specific repository a secret in Google Secret Manager.
```sh
# TODO(developer): Update this value to your GitHub repository.
export REPO="username/name" # e.g. "google/chrome"
export WORKLOAD_IDENTITY_POOL_ID="value/from/above" # e.g. "projects/123456789/locations/global/workloadIdentityPools/github"
gcloud secrets add-iam-policy-binding "my-secret" \
--project="${PROJECT_ID}" \
--role="roles/secretmanager.secretAccessor" \
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"
```
Review the [GitHub documentation][github-oidc] for a complete list of
options and values. This GitHub repository does not seek to enumerate every
possible combination.
</details>
<a name="indirect-wif" id="indirect-wif"></a>
### Workload Identity Federation through a Service Account
In this setup, the Workload Identity Pool impersonates a Google Cloud Service
Account which has IAM permissions on Google Cloud resources. This exchanges the
GitHub Actions OIDC token with a Google Cloud OAuth 2.0 access token by granting
GitHub Actions permissions to mint tokens for the given Service Account. Thus
GitHub Actions inherits that Service Account's permissions by proxy.
[![Authenticate to Google Cloud from GitHub Actions with Workload Identity Federation through a Service Account](docs/google-github-actions-auth-workload-identity-federation-through-service-account.svg)](docs/google-github-actions-auth-workload-identity-federation-through-service-account.svg)
<details>
<summary>Click here to show detailed instructions for configuring GitHub authentication to Google Cloud via a Workload Identity Federation through a Service Account.</summary>
These instructions use the [gcloud][gcloud] command-line tool.
1. (Optional) Create a Google Cloud Service Account. If you already have a
Service Account, take note of the email address and skip this step.
```sh
gcloud iam service-accounts create "my-service-account" \
--project "${PROJECT_ID}"
```
1. Create a Workload Identity Pool:
```sh
gcloud iam workload-identity-pools create "github" \
--project="${PROJECT_ID}" \
--location="global" \
--display-name="GitHub Actions Pool"
```
1. Get the full ID of the Workload Identity **Pool**:
```sh
gcloud iam workload-identity-pools describe "github" \
--project="${PROJECT_ID}" \
--location="global" \
--format="value(name)"
```
This value should be of the format:
```text
projects/123456789/locations/global/workloadIdentityPools/github
```
1. Create a Workload Identity **Provider** in that pool:
```sh
gcloud iam workload-identity-pools providers create-oidc "my-repo" \
--project="${PROJECT_ID}" \
--location="global" \
--workload-identity-pool="github" \
--display-name="My GitHub repo Provider" \
--attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository" \
--issuer-uri="https://token.actions.githubusercontent.com"
```
The attribute mappings map claims in the GitHub Actions JWT to assertions
you can make about the request (like the repository or GitHub username of
the principal invoking the GitHub Action). These can be used to further
restrict the authentication using `--attribute-condition` flags.
> **❗️ NOTE!** You must map any claims in the incoming token to attributes
> before you can assert on those attributes in a CEL expression or IAM
> policy!**
1. Allow authentications from the Workload Identity Pool to your Google Cloud
Service Account.
```sh
# TODO(developer): Update this value to your GitHub repository.
export REPO="username/name" # e.g. "google/chrome"
export WORKLOAD_IDENTITY_POOL_ID="value/from/above" # e.g. "projects/123456789/locations/global/workloadIdentityPools/github"
gcloud iam service-accounts add-iam-policy-binding "my-service-account@${PROJECT_ID}.iam.gserviceaccount.com" \
--project="${PROJECT_ID}" \
@ -628,130 +517,93 @@ Terraform module to automate your infrastructure provisioning. See [examples](ht
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"
```
If you want to admit all repos of an owner (user or organization), map on `attribute.repository_owner`:
```sh
--member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository_owner/${OWNER}"
```
For this to work, you need to make sure that `attribute.repository_owner` is mapped in your attribute mapping (see previous step).
Note that `$WORKLOAD_IDENTITY_POOL_ID` should be the **full** Workload
Identity Pool resource ID, like:
```text
projects/123456789/locations/global/workloadIdentityPools/my-pool
```
Review the [GitHub documentation][github-oidc] for a complete list of
options and values. This GitHub repository does not seek to enumerate every
possible combination.
1. Extract the Workload Identity **Provider** resource name:
```sh
gcloud iam workload-identity-pools providers describe "my-provider" \
gcloud iam workload-identity-pools providers describe "my-repo" \
--project="${PROJECT_ID}" \
--location="global" \
--workload-identity-pool="my-pool" \
--workload-identity-pool="github" \
--format="value(name)"
```
Use this value as the `workload_identity_provider` value in your GitHub
Actions YAML.
Use this value as the `workload_identity_provider` value in the GitHub
Actions YAML:
1. Use this GitHub Action with the Workload Identity Provider ID and Service
Account email. The GitHub Action will mint a GitHub OIDC token and exchange
the GitHub token for a Google Cloud access token (assuming the authorization
is correct). This all happens without exporting a Google Cloud service
account key JSON!
```yaml
- uses: 'google-github-actions/auth@v2'
with:
workload_identity_provider: '...' # "projects/123456789/locations/global/workloadIdentityPools/github/providers/my-repo"
```
Note: It can take **up to 5 minutes** from when you configure the Workload
Identity Pool mapping until the permissions are available.
1. As needed, grant the Google Cloud Service Account permissions to access
Google Cloud resources. This step varies by use case. The following example
shows granting access to a secret in Google Secret Manager.
```sh
gcloud secrets add-iam-policy-binding "my-secret" \
--project="${PROJECT_ID}" \
--role="roles/secretmanager.secretAccessor" \
--member="serviceAccount:my-service-account@${PROJECT_ID}.iam.gserviceaccount.com"
```
</details>
#### Organizational Policy Constraints
<a name="sake" id="sake"></a>
### Service Account Key JSON
By default, Google Cloud allows you to create Workload Identity Pools and
Workload Identity Providers for any endpoints. Your organization may restrict
which external identity providers are permitted on your Google Cloud account. To
enable GitHub Actions as a Workload Identity Pool and Provider, add the
`https://token.actions.githubusercontent.com` to the allowed
`iam.workloadIdentityPoolProviders` Org Policy constraint.
In this setup, a Service Account has direct IAM permissions on Google Cloud
resources. You download a Service Account Key JSON file and upload it to GitHub
as a secret.
```shell
gcloud resource-manager org-policies allow "constraints/iam.workloadIdentityPoolProviders" \
https://token.actions.githubusercontent.com
```
[![Authenticate to Google Cloud from GitHub Actions with a Service Account Key](docs/google-github-actions-auth-service-account-key-export.svg)](docs/google-github-actions-auth-service-account-key-export.svg)
You can specify a `--folder` or `--organization`. If you do not have permission
to manage these Org Policies, please contact your Google Cloud administrator.
> **❗️ WARNING!** Google Cloud Service Account Key JSON files must be secured
> and treated like a password. Anyone with acess to the JSON key can
> authenticate to Google Cloud as the underlying Service Account. By default,
> these credentials never expire, which is why the former authentication options
> are much preferred.
For GitHub Enterprise Server, the endpoint will be your server URL:
<details>
<summary>Click here to show detailed instructions for configuring GitHub authentication to Google Cloud via a Service Account Key JSON.</summary>
```shell
gcloud resource-manager org-policies allow "constraints/iam.workloadIdentityPoolProviders" \
https://my.github.company
```
These instructions use the [gcloud][gcloud] command-line tool.
1. (Optional) Create a Google Cloud Service Account. If you already have a
Service Account, take note of the email address and skip this step.
## GitHub Token Format
```sh
gcloud iam service-accounts create "my-service-account" \
--project "${PROJECT_ID}"
```
Below is a sample GitHub Token for reference for attribute mappings. For a list of all
mappings, see the [GitHub OIDC token documentation](https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token).
1. Create a Service Account Key JSON for the Service Account.
```json
{
"jti": "...",
"sub": "repo:username/reponame:ref:refs/heads/main",
"aud": "https://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider",
"ref": "refs/heads/main",
"sha": "d11880f4f451ee35192135525dc974c56a3c1b28",
"repository": "username/reponame",
"repository_owner": "username",
"repository_visibility": "private",
"repository_id": "74",
"repository_owner_id": "65",
"run_id": "1238222155",
"run_number": "18",
"run_attempt": "1",
"actor": "username",
"actor_id": "12",
"workflow": "oidc",
"head_ref": "",
"base_ref": "",
"event_name": "push",
"ref_type": "branch",
"job_workflow_ref": "username/reponame/.github/workflows/token.yml@refs/heads/main",
"iss": "https://token.actions.githubusercontent.com",
"nbf": 1631718827,
"exp": 1631719727,
"iat": 1631719427
}
```
```sh
gcloud iam service-accounts keys create "key.json" \
--iam-account "my-service-account@${PROJECT_ID}.iam.gserviceaccount.com"
```
1. Upload the contents of this file as a [GitHub Actions
Secret](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions).
## Versioning
Use the name of the GitHub Actios secret as the `credentials_json` value in
the GitHub Actions YAML:
We recommend pinning to the latest available major version:
```yaml
- uses: 'google-github-actions/auth@v2'
with:
credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}' # Replace with the name of your GitHub Actions secret
```
</details>
```yaml
- uses: 'google-github-actions/auth@v1'
```
While this action attempts to follow semantic versioning, but we're ultimately
human and sometimes make mistakes. To prevent accidental breaking changes, you
can also pin to a specific version:
```yaml
- uses: 'google-github-actions/auth@v1.0.0'
```
However, you will not get automatic security updates or new features without
explicitly updating your version number. Note that we only publish `MAJOR` and
`MAJOR.MINOR.PATCH` versions. There is **not** a floating alias for
`MAJOR.MINOR`.
[wif]: https://cloud.google.com/iam/docs/workload-identity-federation
[gcloud]: https://cloud.google.com/sdk
[map-external]: https://cloud.google.com/iam/docs/access-resources-oidc#impersonate
[github-perms]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#permissions
[dwd]: https://developers.google.com/admin-sdk/directory/v1/guides/delegation
[orgpolicy-creds-lifetime]: https://cloud.google.com/resource-manager/docs/organization-policy/org-policy-constraints
[gcloud]: https://cloud.google.com/sdk
[github-oidc]: https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#understanding-the-oidc-token
[github-perms]: https://docs.github.com/en/actions/learn-github-actions/workflow-syntax-for-github-actions#permissions
[map-external]: https://cloud.google.com/iam/docs/access-resources-oidc#impersonate
[wif]: https://cloud.google.com/iam/docs/workload-identity-federation

View File

@ -168,14 +168,14 @@ outputs:
description: |-
Path on the local filesystem where the generated credentials file resides.
This is only available if "create_credentials_file" was set to true.
auth_token:
description: |-
The intermediate authentication token, which could be used to call other
Google Cloud APIs, depending on how you configured IAM.
access_token:
description: |-
The Google Cloud access token for calling other Google Cloud APIs. This is
only available when "token_format" is "access_token".
access_token_expiration:
description: |-
The RFC3339 UTC "Zulu" format timestamp for the access token. This is only
available when "token_format" is "access_token".
id_token:
description: |-
The Google Cloud ID token. This is only available when "token_format" is

6
dist/main/index.js vendored

File diff suppressed because one or more lines are too long

6
dist/post/index.js vendored

File diff suppressed because one or more lines are too long

187
docs/EXAMPLES.md Normal file
View File

@ -0,0 +1,187 @@
# Examples for Authenticating to Google Cloud from GitHub Actions
> Consider using the [Markdown TOC][github-markdown-toc] to make browsing these
> samples easier.
These examples assume you have completed all corresponding [Setup
Instructions](../README.md#setup).
## Direct Workload Identity Federation
This example shows authenticating directly with Workload Identity Federation.
Google Cloud Resources must have the Workload Identity Pool as a `principalSet`
as an IAM permission.
```yaml
jobs:
job_id:
permissions:
contents: 'read'
id-token: 'write'
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
project_id: 'my-project'
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
# Use 'steps.auth.outputs.auth_token' in subsequent steps as a bearer token.
#
# - run: |-
# curl -H 'Bearer: ${{ steps.auth.outputs.auth_token }}' https://...
#
```
## Workload Identity Federation through a Service Account
This example shows authenticating to Google Cloud by proxying through a Service
Account. Future authentication calls will be made with the Service Account's
OAuth 2.0 Access token.
```yaml
jobs:
job_id:
permissions:
contents: 'read'
id-token: 'write'
- uses: 'google-github-actions/auth@v2'
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
# NOTE: 'steps.auth.outputs.auth_token' will be a federated authentication
# token, it does not correspond to the service account. To get a token for
# the service account, specify the 'token_format' parameter and use the
# 'accesss_token' output.
#
# - uses: 'google-github-actions/auth@v2'
# with:
# workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
# service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
# token_format: 'access_token'
#
# - run: |-
# curl -H 'Bearer: ${{ steps.auth.outputs.access_token }}' https://...
#
```
## Service Account Key JSON
This example demonstrates authenticating via a Google Cloud Service Account Key
JSON. After you [export a Google Cloud Service Account Key][sake], insert the
value into a GitHub Secret named 'GOOGLE_CREDENTIALS'.
```yaml
jobs:
job_id:
steps:
- uses: 'actions/checkout@v4'
- uses: 'google-github-actions/auth@v2'
with:
credentials_json: '${{ secrets.GOOGLE_CREDENTIALS }}'
```
### Configuring gcloud
This example demonstrates using this GitHub Action to configure authentication
for the `gcloud` CLI tool.
```yaml
jobs:
job_id:
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
- name: 'Set up Cloud SDK'
uses: 'google-github-actions/setup-gcloud@v1'
```
### Generating an OAuth 2.0 Access Token
This example demonstrates using this GitHub Action to generate an OAuth 2.0
Access Token for authenticating to Google Cloud.
> **⚠️ NOTE!** The default lifetime is 1 hour, but you can request up to 12
> hours if you set the
> [`constraints/iam.allowServiceAccountCredentialLifetimeExtension` organization
> policy][orgpolicy-creds-lifetime].
> **⚠️ NOTE!** If you authenticate via `credentials_json`, the service account
> must have `roles/iam.serviceAccountTokenCreator` on itself.
```yaml
jobs:
job_id:
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
token_format: 'access_token' # <--
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
access_token_lifetime: '300s' # optional, default: '3600s' (1 hour)
# Example of using the output. The token is usually provided as a Bearer
# token.
- id: 'access-secret'
run: |-
curl https://secretmanager.googleapis.com/v1/projects/my-project/secrets/my-secret/versions/1:access \
--header "Authorization: Bearer ${{ steps.auth.outputs.access_token }}"
```
### Generating an ID Token (JWT)
This example demonstrates using this GitHub Action to generate a Google Cloud ID
Token for authenticating to Google Cloud. This is commonly used when invoking a
Cloud Run service.
> **⚠️ NOTE!** If you authenticate via `credentials_json`, the service account
> must have `roles/iam.serviceAccountTokenCreator` on itself.
```yaml
jobs:
job_id:
permissions:
contents: 'read'
id-token: 'write'
steps:
- uses: 'actions/checkout@v4'
- id: 'auth'
uses: 'google-github-actions/auth@v2'
with:
token_format: 'id_token' # <--
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
id_token_audience: 'https://myapp-uvehjacqzq.a.run.app' # required, value depends on target
id_token_include_email: true
# Example of using the output. The token is usually provided as a Bearer
# token.
- id: 'invoke-service'
run: |-
curl https://myapp-uvehjacqzq.a.run.app \
--header "Authorization: Bearer ${{ steps.auth.outputs.id_token }}"
```
[github-markdown-toc]: https://github.blog/changelog/2021-04-13-table-of-contents-support-in-markdown-files/
[orgpolicy-creds-lifetime]: https://cloud.google.com/resource-manager/docs/organization-policy/org-policy-constraints
[sake]: https://cloud.google.com/iam/docs/creating-managing-service-account-keys

View File

@ -2,46 +2,33 @@
## Permission denied
When troubleshooting "permission denied" errors from `auth` for Workload
Identity, the first step is to ask the `auth` plugin to generate an OAuth access
token. Do this by adding `token_format: 'access_token'` to your YAML:
```yaml
- uses: 'google-github-actions/auth@v1'
with:
# ...
token_format: 'access_token'
```
If your workflow _succeeds_ after adding the step to generate an access token,
it means Workload Identity Federation is configured correctly and the issue is
in subsequent actions. You can remove the `token_format` from your YAML. To
further debug:
1. Enable [GitHub Actions debug logging][debug-logs] and re-run the workflow to
see exactly which step is failing. Ensure you are using the latest version
of that GitHub Action.
of the GitHub Action.
1. Make sure you use `actions/checkout@v4` **before** the `auth` action in your
> **❗️ WARNING!** Enabling debug logging increases the chances of a secret
> being accidentially logged. While GitHub Actions will scrub secrets,
> please take extra caution when sharing these debug logs in publicly
> accessible places like GitHub issues.
>
> If you do not feel comfortable attaching the debug logs to a GitHub issue,
> please create the issue and then email the debug logs to
> google-github-actions@google.com, including the GitHub issue number in the
> subject line and email body.
1. Ensure you have waited at least 5 minutes between making changes to the
Workload Identity Pool, Workload Identity Provider, or IAM policies. Changes
to these resources are eventually consistent. Usually they happen
immediately, but sometimes they can take up to 5 minutes to propagate.
1. Ensure `actions/checkout@v4` is **before** the `auth` action in your
workflow.
1. If the failing action is from `google-github-action/*`, please file an issue
in the corresponding repository.
1. If the failing action is from an external action, please file an issue
against that repository. The `auth` action exports Google Application
Default Credentials (ADC). Ask the action author to ensure they are
processing ADC correctly and using the latest versions of the Google client
libraries. Please note that we do not have control over actions outside of
`google-github-actions`.
If your workflow _fails_ after adding the step to generate an access token,
it likely means there is a misconfiguration with Workload Identity. Here are
some common sources of errors:
1. Enable [GitHub Actions debug logging][debug-logs] and re-run the workflow to
see exactly which step is failing. Ensure you are using the latest version
of that GitHub Action.
```yaml
steps:
- uses: 'actions/checkout@v4'
- uses: 'google-github-actions/auth@v2'
```
1. Ensure the value for `workload_identity_provider` is the full _Provider_
name, **not** the _Pool_ name:
@ -55,8 +42,13 @@ some common sources of errors:
**number**. Workload Identity Federation does not accept Google Cloud
Project IDs.
1. Ensure that you have the correct `permissions:` for the job in your workflow, per
the [usage](../README.md#usage) docs, i.e.
```diff
- projects/my-project/locations/global/workloadIdentityPools/my-pool/providers/my-provider
+ projects/1234567890/locations/global/workloadIdentityPools/my-pool/providers/
```
1. Ensure that you have the correct `permissions:` for the job in your
workflow, per the [usage](../README.md#usage) docs:
```yaml
permissions:
@ -67,43 +59,47 @@ some common sources of errors:
1. Ensure you have created an **Attribute Mapping** for any **Attribute
Conditions** or **Service Account Impersonation** principals. You cannot
create an Attribute Condition unless you map that value from the incoming
GitHub OIDC token. You cannot grant permissions to impersonate a Service
Account on an attribute unless you map that value from the incoming GitHub
OIDC token.
GitHub OIDC token. You cannot grant permissions on an attribute unless you
map that value from the incoming GitHub OIDC token.
You can use the [GitHub Actions OIDC Debugger][oidc-debugger] to print the
list of token claims and compare them to your Attribute Mappings and
Attribute Conditions.
> **📝 TIP!** Use the [GitHub Actions OIDC Debugger][oidc-debugger] to print
> the list of token claims and compare them to your Attribute Mappings and
> Attribute Conditions.
1. Ensure you have the correct casing and capitalization. GitHub does not
distinguish between "foobar" and "FooBar", but Google Cloud does. Ensure any
**Attribute Conditions** use the correct capitalization.
1. Ensure you have the correct character casing and capitalization. GitHub does
not distinguish between "foobar" and "FooBar", but Google Cloud does. Ensure
any **Attribute Conditions** use the correct capitalization. The
capitalization must match what is in the GitHub Actions OIDC token.
1. Check the specific error message that is returned.
- If the error message includes "failed to generate Google Cloud federated
- If the error message includes "Failed to generate Google Cloud federated
token", it means admission into the Workload Identity Pool failed. Check
your [**Attribute Conditions**][attribute-conditions].
- If the error message inclues "failed to generate Google Cloud access
token", it means Service Account Impersonation failed. Check your
- If the error message inclues "Failed to generate OAuth 2.0 Access
Token", it means Service Account Impersonation failed. Check your
[**Service Account Impersonation**][sa-impersonation] settings and
ensure the principalSet is correct.
1. Enable `Admin Read`, `Data Read`, and `Data Write` [Audit Logging][cal] for
Identity and Access Management (IAM) in your Google Cloud project.
**Warning!** This will increase log volume which may increase costs. To keep
costs low, you can disable this audit logging after you have debugged the
issue.
> **❗️ WARNING!** This will increase log volume which may increase costs.
> You can disable this audit logging after you have debugged the issue.
Try to authenticate again, and then explore the logs for your Workload
Identity Provider and Workload Identity Pool. Sometimes these error messages
are helpful in identifying the root cause.
1. Ensure you have waited at least 5 minutes between making changes to the
Workload Identity Pool and Workload Identity Provider. Changes to these
resources are eventually consistent.
1. If failures are coming from a different GitHub Action step, please file an
issue against that repository. The `auth` action exports Google Application
Default Credentials (ADC). Ask the action author to ensure they are
processing ADC correctly and using the latest versions of the Google client
libraries.
> **⚠️ NOTE!** We do not have control over GitHub Actions outside of the
> `google-github-actions` GitHub organization.
## Subject exceeds the 127 byte limit
@ -234,6 +230,33 @@ tool like `jq`:
cat credentials.json | jq -r tostring
```
## Organizational Policy Constraints
**⚠️ NOTE!** Your Google Cloud organization administrator controls these
policies. You must work with your internal IT department to resolve OrgPolicy
violations and constraints.
### Workload Identity Providers
Your organization may restrict which external identity providers are permitted
on your Google Cloud account. To enable GitHub Actions as a Workload Identity
Pool and Provider, add the `https://token.actions.githubusercontent.com` to the
allowed `iam.workloadIdentityPoolProviders` Org Policy constraint.
```shell
gcloud resource-manager org-policies allow "constraints/iam.workloadIdentityPoolProviders" \
https://token.actions.githubusercontent.com
```
### Service Account Key Export
Your organization may restrict exporting Service Account Keys. To enable Service
Account Key export, set the `iam.disableServiceAccountCreation` to false.
```shell
gcloud resource-manager org-policies disable-enforce "constraints/iam.disableServiceAccountCreation"
```
[attribute-conditions]: https://cloud.google.com/iam/docs/workload-identity-federation#conditions
[sa-impersonation]: https://cloud.google.com/iam/docs/workload-identity-federation#impersonation

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,111 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 771 365">
<style>
path {
fill: #333;
stroke: #333;
stroke-width: 0.5;
}
@media (prefers-color-scheme: dark) {
path {
fill: #ccc;
stroke: #ccc;
}
}
</style>
<path d="M2.9 13.3V6.4h3.8v1h-3v6z"/>
<path d="M2.9 26.3v-14h.8v14z"/>
<path d="M2.9 39.3v-14h.8v14z"/>
<path d="M2.9 52.3v-14h.8v14z"/>
<path d="M2.9 65.3v-14h.8v14z"/>
<path d="M2.9 78.3v-14h.8v14z"/>
<path d="M2.9 91.3v-14h.8v14z"/>
<path d="M2.9 104.3v-14h.8v14z"/>
<path d="M2.9 117.3v-14h.8v14z"/>
<path d="M2.9 130.3v-14h.8v14z"/>
<path d="M2.9 143.3v-14h.8v14z"/>
<path d="M2.9 156.3v-14h.8v14z"/>
<path d="M2.9 169.3v-14h.8v14z"/>
<path d="M2.9 182.3v-14h.8v14z"/>
<path d="M2.9 195.3v-14h.8v14z"/>
<path d="M2.9 208.3v-14h.8v14z"/>
<path d="M2.9 221.3v-14h.8v14z"/>
<path d="M2.9 234.3v-14h.8v14z"/>
<path d="M2.9 247.3v-14h.8v14z"/>
<path d="M2.9 260.3v-14h.8v14z"/>
<path d="M2.9 273.3v-14h.8v14z"/>
<path d="M2.9 286.3v-14h.8v14z"/>
<path d="M2.9 299.3v-14h.8v14z"/>
<path d="M2.9 312.3v-14h.8v14z"/>
<path d="M2.9 325.3v-14h.8v14z"/>
<path d="M2.9 338.3v-14h.8v14z"/>
<path d="M2.9 351.3v-14h.8v14z"/>
<path d="M2.9 358.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm38 0v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm38 0v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zM20 347.3l-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1H20zm4.3-4.6q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm7 .9q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm8.3 3.2q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm5.3-1.8q0 .7.2 1 .3.4.7.4H48v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm9.1-1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm14.8 3.5-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8T65 344q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4H76v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V342h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V342h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7z"/>
<path d="M233.9 351.3v-14h.8v14zm35 0v-14h.8v14zm17.1-4-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1h2.2zm2.7-5.3h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1.6v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm4.4-.3h1.1v3h3v-3h1v8h-1v-4h-3v4h-1zm7.3 5.7V342h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V342h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11-.7q0-1.2-.4-1.7-.4-.6-1.1-.6-.7 0-1.1.6-.4.6-.4 1.7t.4 1.7q.4.6 1 .6.8 0 1.2-.6.3-.6.3-1.7zm-3-2.2q.3-.5.7-.7.4-.3 1-.3 1 0 1.6.9.7.8.7 2.3 0 1.5-.7 2.3-.6.9-1.7.9-.5 0-1-.3l-.6-.7v.8h-1v-8.4h1zm182.9 8.5v-14h.8v14zm35 0v-14h.8v14zm17.1-4-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1h2.2zm4.3-4.6q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm7 .9q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm8.3 3.2q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm5.3-1.8q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm9.1-1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm14.8 3.5-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V342h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V342h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm141.2 6.3v-14h.8v14zm-735-312v-6.9h3.8v1h-3v6z"/>
<path d="M30.9 52.3v-14h.8v14z"/>
<path d="M30.9 65.3v-14h.8v14z"/>
<path d="M30.9 78.3v-14h.8v14z"/>
<path d="M30.9 91.3v-14h.8v14z"/>
<path d="M30.9 104.3v-14h.8v14z"/>
<path d="M30.9 117.3v-14h.8v14z"/>
<path d="M30.9 130.3v-14h.8v14z"/>
<path d="M30.9 137.3v-8h.8v7.1h3v1zm0 84v-6.9h3.8v1h-3v6z"/>
<path d="M30.9 234.3v-14h.8v14z"/>
<path d="M30.9 247.3v-14h.8v14z"/>
<path d="M30.9 260.3v-14h.8v14z"/>
<path d="M30.9 273.3v-14h.8v14z"/>
<path d="M30.9 286.3v-14h.8v14z"/>
<path d="M30.9 299.3v-14h.8v14z"/>
<path d="M30.9 312.3v-14h.8v14z"/>
<path d="M30.9 319.3v-8h.8v7.1h3v1zm203 19v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm-731-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M233.9 325.3v-14h.8v14zm35 0v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm35 0v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm-711.5-63v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6L51 270q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm151.5 24v-14h.8v14z"/>
<path d="M205.9 299.3v-14h.8v14z"/>
<path d="M205.9 312.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M233.9 299.3v-14h.8v14z"/>
<path d="M233.9 312.3v-14h.8v14zm35-26v-14h.8v14z"/>
<path d="M268.9 299.3v-14h.8v14z"/>
<path d="M268.9 312.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M296.9 299.3v-14h.8v14z"/>
<path d="M296.9 312.3v-14h.8v14zm175-26v-14h.8v14z"/>
<path d="M471.9 299.3v-14h.8v14z"/>
<path d="M471.9 312.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M499.9 299.3v-14h.8v14z"/>
<path d="M499.9 312.3v-14h.8v14zm14-19v-8h.8v7.1h3v1zm21 19v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-241-19v-.9h6.8v1z"/>
<path d="M534.9 299.3v-14h.8v14zm4-6v-.9h6.8v1zm14.1 3.1V290l6.6 3.2zm9.9 2.9v-14h.8v14zm76.1-5.5h2v2.2h-2zm7 0h2v2.2h-2zm7 0h2v2.2h-2zm84.9 5.5v-14h.8v14zm28 0v-14h.8v14z"/>
<path d="M534.9 286.3v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zM62 266.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm12.3-3.2-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H107l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7 0-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm4.6-5q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V264h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V264h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.6-1.4v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm4.7-4v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm11.4-.3h1.1v3.5l3.4-3.5h1.3l-3.1 3.3 3.2 4.7h-1.3l-2.6-4-.9.9v3.1h-1zm12.3 4.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm6.6 1.8-.6 1.7-.7 1.8q-.3.4-.7.6-.3.2-.8.2h-.8v-.8h.6q.4 0 .7-.3l.6-1.3-2.3-6h1l1.8 4.8 1.7-4.7h1zm26.3 5.3v-14h.8v14zm4-6v-.9h6.8v1zm14 0v-.9h6.8v1zm10 6v-14h.8v14zm4-6v-.9h6.8v1zm14 0v-.9h6.8v1zm17.8 0v6h-.8v-6h-3v-.9h3v-7.1h.8v7.1h3v1zm10.2 0v-.9h6.8v1zm7.1 3.1V264l6.6 3.2zm9.9 2.9v-14h.8v14zm45.1-4-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1h2.2zm2.7-5.3h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1.6v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm4.4-.3h1.1v3h3v-3h1v8h-1v-4h-3v4h-1zm7.3 5.7V264h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V264h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11-.7q0-1.2-.4-1.7-.4-.6-1.1-.6-.7 0-1.1.6-.4.6-.4 1.7t.4 1.7q.4.6 1 .6.8 0 1.2-.6.3-.6.3-1.7zm-3-2.2q.3-.5.7-.7.4-.3 1-.3 1 0 1.6.9.7.8.7 2.3 0 1.5-.7 2.3-.6.9-1.7.9-.5 0-1-.3l-.6-.7v.8h-1v-8.4h1zm17.4-2.5v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm7.6 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.7 3.5-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.5-4.5-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm6.8 1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm5.3-3.9v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm8.9 1.9v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zm39.7 9.1v-14h.8v14zm28 0v-14h.8v14zm14 0v-14h.8v14z"/>
<path d="M534.9 273.3v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm-560-39v-14h.8v14z"/>
<path d="M205.9 247.3v-14h.8v14z"/>
<path d="M205.9 260.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M233.9 247.3v-14h.8v14z"/>
<path d="M233.9 260.3v-14h.8v14zm35-26v-14h.8v14z"/>
<path d="M268.9 247.3v-14h.8v14z"/>
<path d="M268.9 260.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M296.9 247.3v-14h.8v14z"/>
<path d="M296.9 260.3v-14h.8v14zm175-26v-14h.8v14z"/>
<path d="M471.9 247.3v-14h.8v14z"/>
<path d="M471.9 260.3v-14h.8v14zm28-26v-14h.8v14z"/>
<path d="M499.9 247.3v-14h.8v14z"/>
<path d="M499.9 260.3v-14h.8v14zm14-13v-14h.8v14zm21 13v-14h.8v14zm231 0v-14h.8v14z"/>
<path d="M534.9 247.3v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14z"/>
<path d="M534.9 234.3v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-731-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm35 0v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm14 0v-14h.8v7.1h3v1h-3v6zm11-6v-.9h6.8v1z"/>
<path d="M534.9 221.3v-14h.8v14zm4-6v-.9h6.8v1zm14.1 3.1V212l6.6 3.2zm9.9 2.9v-14h.8v14zm23.9-3.6-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V212h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V212h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm17.7-4.7v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm4.9 0v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm7 2.4q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.9 1.4-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm4.6 1.8h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6v-.2zm1.9-.4v3.4h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3v-.2q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm5.9.4q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm7.4-3.1v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm11-.2v3.1h1.3q1 0 1.3-.3.4-.4.4-1.1 0-1-.4-1.4-.4-.3-1.3-.3zm0-3.1v2.2h1.2q.8 0 1.2-.3.3-.3.3-.8 0-.6-.3-.9-.4-.2-1.2-.2zm-1.1-1h2.4q1.2 0 1.9.6.6.5.6 1.5 0 .5-.3 1-.4.4-1 .5.7.1 1.2.7.4.5.4 1.6 0 1-.7 1.6-.7.6-2.1.6h-2.4zm7.1 5.8V212h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V212h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.7 2-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm2.6-8.1h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7v2.4h-1zm11.7 4.9v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm5.3-3.9v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm20.6 11v-14h.8v14zm28 0v-14h.8v14zm-644-65v-14h.8v14zm0 26v-14h.8v14zm0 26v-14h.8v14zm112 0v-14h.8v14zm35 0v-14h.8v14zm119.4-2.9L385 199h6.6zm111.6 2.9v-14h.8v14z"/>
<path d="M534.9 208.3v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14zm119 0v-14h.8v14zm112 0v-14h.8v14zm14 0v-14h.8v14z"/>
<path d="M534.9 195.3v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14zm117.6-4.3h1.6v-6l-2 1v-1l2-1h1.1v7h1.7v1h-4.4zm113.4 4.3v-14h.8v14z"/>
<path d="M534.9 182.3v-14h.8v14zm231 0v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14zm119 0v-14h.8v14zm112 0v-14h.8v14zm14 0v-14h.8v14z"/>
<path d="M534.9 169.3v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14zm116.1-2.9 3.3-6.5 3.3 6.5zm114.9 2.9v-14h.8v14z"/>
<path d="M534.9 156.3v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-731-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm35 0v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm14 0v-14h.8v7.1h3v1h-3v6zm11-6v-.9h6.8v1z"/>
<path d="M534.9 143.3v-14h.8v14zm4-6v-.9h6.8v1zm14.1 3.1V134l6.6 3.2zm9.9 2.9v-14h.8v14zm30.9-3.6-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V134h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V134h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm16.3-.8q.5.1.8.4.3.3.7 1.2l1.1 2.2h-1.1l-1-2q-.4-.9-.7-1.1-.4-.3-.9-.3h-1v3.4h-1.1v-8h2.2q1.3 0 2 .6t.7 1.7q0 .8-.4 1.3t-1.2.6zm-2-3.2v2.8h1.1q.8 0 1.2-.3.4-.4.4-1.1 0-.7-.4-1-.4-.4-1.2-.4zm6 4.7V134h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V134h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.6-1.4v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm13.8-4v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm7.6 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm32.9 7.1v-14h.8v14zm28 0v-14h.8v14zm-697.5-63v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6L65 88q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm137.5 24v-14h.8v14z"/>
<path d="M205.9 117.3v-14h.8v14z"/>
<path d="M205.9 130.3v-14h.8v14zm4-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 19v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14z"/>
<path d="M234.7 111.3v6h-.8v-6h-3v-.9h3v-7.1h.8v7.1h3v1zm3.2 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm11.3-1.6q.8.2 1.2.8.4.5.4 1.3 0 1-.7 1.7-.8.7-2.1.7l-1.1-.1-1.2-.3v-1.1l1.1.4 1 .1q1 0 1.4-.4.5-.4.5-1.1 0-.7-.5-1.1-.4-.4-1.3-.4h-.8v-1h.8q.8 0 1.2-.3.4-.3.4-.9 0-.6-.4-1-.3-.3-1-.3l-1 .2q-.6 0-1.1.3v-1l1.1-.3h1q1.1 0 1.8.5.7.6.7 1.6 0 .6-.4 1-.3.5-1 .7zm2.7 1.6v-.9h6.8v1zm7 0v-.9h6.8v1zm7.1 3.1V108l6.6 3.2zm9.9 2.9v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm14 0v-14h.8v14zm21 0v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14z"/>
<path d="M233.9 104.3v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zM76 84.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm12.3-3.2-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H121l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7 0-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm4.6-5q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V82h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V82h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.6-1.4V88h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V88h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm4.7-4V82h2.2v.8h-2.2V86q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V82h1.6v-1.7zm41.6 11v-14h.8v14z"/>
<path d="M233.9 91.3v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zM332.3 81l-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H331l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm4.6-7.4V82h2.2v.8h-2.2V86q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V82h1.6v-1.7zm5.4 1.7h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1 3q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.3 2.5V88h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V88h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm6.6-2.1v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zM385 80h1l.8 6.5 1-4.3h1l1 4.3.8-6.5h1l-1.2 8h-1l-1-4.8-1.2 4.8h-1zm10.3 2.7q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.9 1.4-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm2.1-3.6h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7V88h-1zm11.4 0v.9h-1.1q-.5 0-.8.2-.2.2-.2.8v.5h2.1v.8h-2V88h-1v-5.2H414V82h1.7v-.4q0-1 .4-1.5.5-.5 1.4-.5zm5.2 6.2q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm3.7.2h1l1 4.8.9-3h.8l.9 3 1-4.8h1l-1.4 6h-1l-.9-3.3-1 3.3h-.9zm37.9 9.3v-14h.8v14zm11-6v-.9h6.8v1zM501 81l-2.6 4.2h2.5zm-.2-1h1.2v5.2h1.1v.9h-1V88h-1.2v-2h-3.4v-1zm13.1 11.3v-6h-3v-.9h3v-7.1h.8v14zm21 0v-14h.8v14zm28-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm31 6v-14h.8v14zm-560-39v-14h.8v14z"/>
<path d="M205.9 65.3v-14h.8v14z"/>
<path d="M205.9 78.3v-14h.8v14zm4.1-19.2 6.6-3.2v6.5zm23.9 19.2v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-549-19v-.9h6.8v1zm7 0v-.9h6.8v1zM233 61h3.7v1h-4.9v-1l1.8-1.8 1-1.1.7-1 .2-1q0-.6-.4-1t-1-.4l-1 .2-1.2.6v-1.1l1-.4 1.1-.2q1.2 0 2 .7.6.6.6 1.6l-.2 1q-.2.5-.8 1.2l-.9 1L233 61zm4.9-1.7v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1z"/>
<path d="M269.7 59.3v6h-.8v-6h-3v-.9h3v-7.1h.8v7.1h3v1zm3.2 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm14 0v-6.9h3.8v1h-3v6zm11-6v-.9h6.8v1zm10 6v-14h.8v14zm4-6v-.9h6.8v1zm14.1 3.1V56l6.6 3.2zm9.9 2.9v-14h.8v14zm35.4-4.3 1.9-7h1.1l-2.3 8h-1.3l-2.4-8h1.1zm5.4-5h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm10 3.5-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm4.1-2.9V56h2.2v.8h-2.2V60q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V56h1.6v-1.7zm4.7 5.4V56h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V56h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm9.8-.7h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6V59zm1.9-.4V62h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3V58q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm5.2 1.2q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm10.3-5.8h1.4l1.4 4 1.4-4h1.5v8h-1v-7l-1.5 4.1h-.8l-1.4-4.2V62h-1zm10.6 5h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6V59zm1.9-.4V62h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3V58q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm7 3.1-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm6.9-3.4V62h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V62h-1v-8.4h1V57q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm3.1-2.3h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.4 4.6V62h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V62h-1v-6h1v.9q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm7.4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm39.9 7.1v-14h.8v14zm28 0v-14h.8v14zm-532-13v-14h.8v14z"/>
<path d="M268.9 52.3v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm35 0v-14h.8v14zm28 0v-14h.8v14zm175 0v-14h.8v14zm28 0v-14h.8v14zm-731-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14z"/>
<path d="M268.9 39.3v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm35 0v-14h.8v14zm28 0v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm28 0v-14h.8v14zm-532-13v-14h.8v14z"/>
<path d="M268.9 26.3v-14h.8v14zm231 0v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm-759-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M268.9 13.3V6.4h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm35 0V6.4h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
</svg>

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -0,0 +1,241 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 806 430">
<style>
path {
fill: #333;
stroke: #333;
stroke-width: 0.5;
}
@media (prefers-color-scheme: dark) {
path {
fill: #ccc;
stroke: #ccc;
}
}
</style>
<path d="M1.3 171H6v.9H2.3v2.1h3.3v1H2.3v4h-1z"/>
<path d="M37.9 195.3v-14h.8v14z"/>
<path d="M37.9 208.3v-14h.8v14z"/>
<path d="M37.9 221.3v-14h.8v14z"/>
<path d="M37.9 234.3v-14h.8v14z"/>
<path d="M37.9 247.3v-14h.8v14z"/>
<path d="M37.9 260.3v-14h.8v14z"/>
<path d="M37.9 273.3v-14h.8v14z"/>
<path d="M37.9 286.3v-14h.8v14z"/>
<path d="M37.9 299.3v-14h.8v14z"/>
<path d="M37.9 312.3v-14h.8v14z"/>
<path d="M37.9 325.3v-14h.8v14z"/>
<path d="M37.9 338.3v-14h.8v14z"/>
<path d="M37.9 351.3v-14h.8v14z"/>
<path d="M37.9 364.3v-14h.8v14z"/>
<path d="M37.9 377.3v-14h.8v14z"/>
<path d="M37.9 390.3v-14h.8v14z"/>
<path d="M37.9 403.3v-14h.8v14z"/>
<path d="M37.9 416.3v-14h.8v14z"/>
<path d="M37.9 423.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="m55 412.3-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1H55zm4.3-4.6q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm7 .9q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm8.3 3.2q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm5.3-1.8q0 .7.2 1 .3.4.7.4H83v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm9.1-1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm14.8 3.5-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V407h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V407h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7z"/>
<path d="M268.9 416.3v-14h.8v14z"/>
<path d="M65.9 221.3v-6.9h3.8v1h-3v6z"/>
<path d="M65.9 234.3v-14h.8v14z"/>
<path d="M65.9 247.3v-14h.8v14z"/>
<path d="M65.9 260.3v-14h.8v14z"/>
<path d="M65.9 267.3v-8h.8v7.1h3v1zm0 71v-6.9h3.8v1h-3v6z"/>
<path d="M65.9 351.3v-14h.8v14z"/>
<path d="M65.9 364.3v-14h.8v14z"/>
<path d="M65.9 377.3v-14h.8v14z"/>
<path d="M65.9 384.3v-8h.8v7.1h3v1z"/>
<path d="M268.9 403.3v-14h.8v14z"/>
<path d="M69.9 384.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M268.9 390.3v-14h.8v14z"/>
<path d="M293.9 384.3v-.9h6.8v1zm14 0v-.9h6.8v1zm14.1 3.1V381l6.6 3.2zm18.9-4.4q0-1.8-.4-2.5-.4-.8-1.2-.8-.8 0-1.2.8-.3.7-.3 2.5t.3 2.5q.4.8 1.2.8.8 0 1.2-.8.4-.7.4-2.5zm1 0q0 2-.6 3.1-.6 1-2 1t-2-1q-.7-1-.7-3.1t.7-3.1q.7-1 2-1 1.4 0 2 1 .7 1 .7 3.1zm4.4-3-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H345l-.6 2h-1.1zm5.3 5.7V381h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V381h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm9.3-5.4v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm9.3 4v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-8.4h1v3.3q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zM380 386h3.7v1h-4.9v-1l1.8-1.8 1-1.1.7-1 .2-1q0-.6-.4-1t-1-.4l-1 .2-1.2.6v-1.1l1-.4 1.1-.2q1.2 0 2 .7.6.6.6 1.6l-.2 1q-.2.5-.8 1.2l-.9 1-1.7 1.8zm7-1.2h2v2.2h-2zm8.3-5.9q1.3 0 2 1 .6 1 .6 3.1 0 2-.6 3.1-.7 1-2 1t-2-1q-.6-1-.6-3 0-2.1.7-3.2.6-1 2-1zm0 7.4q.8 0 1.1-.8.4-.8.4-2.5v-1.6l-2.6 4q.4.9 1.1.9zm0-6.6q-.7 0-1.1.8-.4.8-.4 2.5l.1 1.4 2.5-4q-.4-.7-1-.7zm14 .3-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H408l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7 0-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.2-2v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zm7 0v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zm9.1-2.2h6v.9H459v7.1h-1.1v-7.1h-2.5zm10 2.7q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm5-2.2h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7v2.4h-1zm11.7 4.9v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.6.1v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm-385.2-30v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm137.5 24v-14h.8v14z"/>
<path d="M268.9 377.3v-14h.8v14z"/>
<path d="M289.9 377.3v-14h.8v14zM111 357.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm12.3-3.2-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H156l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7 0-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm4.6-5q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V355h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V355h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.6-1.4v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm4.7-4v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm41.6 11v-14h.8v14z"/>
<path d="M268.9 364.3v-14h.8v14zm35-6v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8zm38 0v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M240.9 351.3v-14h.8v14z"/>
<path d="M268.9 351.3v-14h.8v14z"/>
<path d="M289.9 351.3v-14h.8v14z"/>
<path d="M303.9 351.3v-14h.8v14z"/>
<path d="m321 347.3-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1h2.2zm2.7-5.3h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1.6v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm4.4-.3h1.1v3h3v-3h1v8h-1v-4h-3v4h-1zm7.3 5.7V342h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V342h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11-.7q0-1.2-.4-1.7-.4-.6-1.1-.6-.7 0-1.1.6-.4.6-.4 1.7t.4 1.7q.4.6 1 .6.8 0 1.2-.6.3-.6.3-1.7zm-3-2.2q.3-.5.7-.7.4-.3 1-.3 1 0 1.6.9.7.8.7 2.3 0 1.5-.7 2.3-.6.9-1.7.9-.5 0-1-.3l-.6-.7v.8h-1v-8.4h1z"/>
<path d="M534.9 351.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="m587 347.3-1 .6q-.6.3-1.2.3-1.6 0-2.4-1.1-.9-1.1-.9-3.1t1-3q.8-1.2 2.3-1.2l1 .2.9.4v1.1l-1-.6q-.4-.2-.9-.2-1 0-1.6.8-.5.9-.5 2.5 0 1.7.5 2.5t1.6.8l.6-.1q.3 0 .5-.3v-2.1h-1.2v-1h2.2zm4.3-4.6q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm7 .9q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm8.3 3.2q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm5.3-1.8q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm9.1-1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm14.8 3.5-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V342h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V342h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7z"/>
<path d="M800.9 351.3v-14h.8v14z"/>
<path d="M69.9 332.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M268.9 338.3v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14z"/>
<path d="M156.9 286.3v-14h.8v14zm1.1 2.7-2.6 4.2h2.5zm-.2-1h1.2v5.2h1.1v.9h-1v1.9h-1.2v-2h-3.4v-1zm-.9 24.3v-14h.8v14zm.4 10.1L154 316h6.6z"/>
<path d="M268.9 325.3v-14h.8v14z"/>
<path d="M289.9 325.3v-14h.8v14z"/>
<path d="M303.9 325.3v-14h.8v14z"/>
<path d="M331.9 319.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M534.9 325.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 319.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M800.9 325.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 312.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 312.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 312.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 312.3v-14h.8v14zm-532-13v-14h.8v14z"/>
<path d="M289.9 299.3v-14h.8v14z"/>
<path d="M303.9 299.3v-14h.8v14z"/>
<path d="M331.9 299.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 299.3v-14h.8v14z"/>
<path d="M548.9 293.3v-8h.8v7.1h3v1zm11 0v-.9h6.8v1z"/>
<path d="M569.9 299.3v-14h.8v14z"/>
<path d="M573.9 293.3v-.9h6.8v1zm14.1 3.1V290l6.6 3.2zm9.9 2.9v-14h.8v14zm76.1-5.5h2v2.2h-2zm7 0h2v2.2h-2zm7 0h2v2.2h-2zm84.9 5.5v-14h.8v14z"/>
<path d="M800.9 299.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 286.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 286.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 286.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 286.3v-14h.8v14z"/>
<path d="M69.9 267.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M268.9 273.3v-14h.8v14z"/>
<path d="M289.9 273.3v-14h.8v14z"/>
<path d="M303.9 273.3v-14h.8v14z"/>
<path d="M331.9 273.3v-14h.8v14zm51-7.3q0-1.8-.4-2.5-.4-.8-1.2-.8-.8 0-1.2.8-.3.7-.3 2.5t.3 2.5q.4.8 1.2.8.8 0 1.2-.8.4-.7.4-2.5zm1 0q0 2-.6 3.1-.6 1-2 1t-2-1q-.7-1-.7-3.1t.7-3.1q.7-1 2-1 1.4 0 2 1 .7 1 .7 3.1zm2.1-4h4.5v.9H389v6.2h1.6v.9h-4.4v-1h1.7v-6H386zm8.3 7.1q1.4 0 2-.7.5-.6.5-2.4 0-1.8-.5-2.5-.6-.6-2-.6h-.5v6.2zm0-7.1q1.9 0 2.8 1 .8 1 .8 3t-.8 3q-.9 1-2.7 1h-1.7v-8zm10.5 7.7-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm13.6-7.4v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm7.6 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm46.9 7.1v-14h.8v14z"/>
<path d="M534.9 273.3v-14h.8v14z"/>
<path d="M548.9 273.3v-14h.8v14z"/>
<path d="M569.9 273.3v-14h.8v14z"/>
<path d="M597.9 273.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M800.9 273.3v-14h.8v14z"/>
<path d="M85 236h4.5v.9H88v6.2h1.6v.9h-4.4v-1h1.7v-6H85zm155.9 24.3v-14h.8v14z"/>
<path d="M268.9 260.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 260.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 260.3v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14z"/>
<path d="m94.3 237-1.1 4h2.3zm-.6-1H95l2.4 8h-1.1l-.6-2H93l-.6 2h-1.1zm4.5 0h1.4l1.4 4 1.4-4h1.5v8h-1v-7l-1.5 4.1h-.8l-1.4-4.2v7.1h-1zm19.6 7.7-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm7.4-4.5-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm6.8 1.3v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm6.6-1.4v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm11.3-.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.6.1v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm4.7-4v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm5.4 1.7h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.6 5.3h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6v-.2zm1.9-.4v3.4h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3v-.2q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm5.2 1.2q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm8.3-3.6v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zm12.1-1.2-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H198l-.6 2h-1.1zm6.4.9v3h1.3q.8 0 1.2-.4.4-.4.4-1.1 0-.7-.4-1.1-.4-.4-1.2-.4zm-1-1h2.3q1.3 0 2 .7.7.6.7 1.8 0 1.2-.7 1.8-.7.6-2 .6h-1.3v3.2h-1zm6.9.1h4.5v.9H214v6.2h1.6v.9h-4.4v-1h1.7v-6H211zm29.9 11.3v-14h.8v14z"/>
<path d="M268.9 247.3v-14h.8v14z"/>
<path d="M289.9 247.3v-14h.8v14z"/>
<path d="M303.9 247.3v-14h.8v14z"/>
<path d="M331.9 247.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 247.3v-14h.8v14z"/>
<path d="M548.9 247.3v-14h.8v14z"/>
<path d="M569.9 247.3v-14h.8v14z"/>
<path d="M597.9 241.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M800.9 247.3v-14h.8v14z"/>
<path d="M240.9 234.3v-14h.8v14z"/>
<path d="M268.9 234.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 234.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 234.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 234.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 234.3v-14h.8v14z"/>
<path d="M69.9 215.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M268.9 221.3v-14h.8v14z"/>
<path d="M289.9 221.3v-14h.8v14z"/>
<path d="M303.9 221.3v-14h.8v14z"/>
<path d="M331.9 221.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M534.9 221.3v-14h.8v14z"/>
<path d="M548.9 221.3v-14h.8v7.1h3v1h-3v6zm11-6v-.9h6.8v1z"/>
<path d="M569.9 221.3v-14h.8v14z"/>
<path d="M573.9 215.3v-.9h6.8v1zm14.1 3.1V212l6.6 3.2zm9.9 2.9v-14h.8v14zm23.9-3.6-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V212h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V212h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm17.7-4.7v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm4.9 0v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm7 2.4q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.9 1.4-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm4.6 1.8h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6v-.2zm1.9-.4v3.4h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3v-.2q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm5.9.4q0-1.2-.4-1.7-.3-.6-1-.6t-1.1.6q-.4.5-.4 1.6 0 1.2.4 1.7.4.6 1.1.6.7 0 1-.6.4-.5.4-1.7zm1 2.6q0 1.4-.6 2-.7.8-2 .8l-.8-.1-.9-.2v-1l1 .4h.8q.8 0 1.1-.4.4-.4.4-1.3v-.8q-.2.5-.6.8-.4.2-1 .2-1 0-1.7-.8-.6-.8-.6-2.3 0-1.4.6-2.2.6-.9 1.7-.9.6 0 1 .3.4.2.6.7v-.8h1zm7.4-3.1v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm11-.2v3.1h1.3q1 0 1.3-.3.4-.4.4-1.1 0-1-.4-1.4-.4-.3-1.3-.3zm0-3.1v2.2h1.2q.8 0 1.2-.3.3-.3.3-.8 0-.6-.3-.9-.4-.2-1.2-.2zm-1.1-1h2.4q1.2 0 1.9.6.6.5.6 1.5 0 .5-.3 1-.4.4-1 .5.7.1 1.2.7.4.5.4 1.6 0 1-.7 1.6-.7.6-2.1.6h-2.4zm7.1 5.8V212h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V212h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.7 2-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm2.6-8.1h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7v2.4h-1zm11.7 4.9v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm5.3-3.9v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm20.6 11v-14h.8v14z"/>
<path d="M800.9 221.3v-14h.8v14z"/>
<path d="M156.9 195.3v-14h.8v14zm.4 10.1L154 199h6.6z"/>
<path d="M268.9 208.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M423.3 205.4 420 199h6.6z"/>
<path d="M534.9 208.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 208.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 208.3v-14h.8v14zm-532-13v-14h.8v14z"/>
<path d="M289.9 195.3v-14h.8v14z"/>
<path d="M303.9 195.3v-14h.8v14z"/>
<path d="M422.9 195.3v-14h.8v14z"/>
<path d="M534.9 195.3v-14h.8v14z"/>
<path d="M548.9 195.3v-14h.8v14z"/>
<path d="M569.9 195.3v-14h.8v14z"/>
<path d="M597.9 195.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M800.9 195.3v-14h.8v14z"/>
<path d="M13 175.5v.5H8.7q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm6.6-1.4v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm11.3-.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm4.6 1.8h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6v-.2zm1.9-.4v3.4h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3v-.2q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm4.6-4.3v1.7h2.2v.8h-2.2v3.2q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6v-.8h1.6v-1.7zm9.7 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm6.6-1.4v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm12.6-5h6v.9H74v7.1h-1.1v-7.1h-2.5zm10 2.7q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm5-2.2h1v4.9l2.6-2.5H90l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7v2.4h-1zm11.7 4.9v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.6.1v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm8.4.8 6.6-3.2v6.5zm6.9.2v-.9h6.8v1zm14 0v-.9h6.8v1zm25.3-1.6q.8.2 1.2.8.4.5.4 1.3 0 1-.7 1.7-.8.7-2.1.7l-1.1-.1-1.2-.3v-1.1l1.1.4 1 .1q1 0 1.4-.4.5-.4.5-1.1 0-.7-.5-1.1-.4-.4-1.3-.4h-.8v-1h.8q.8 0 1.2-.3.4-.3.4-.9 0-.6-.4-1-.3-.3-1-.3l-1 .2q-.6 0-1.1.3v-1l1.1-.3h1q1.1 0 1.8.5.7.6.7 1.6 0 .6-.4 1-.3.5-1 .7z"/>
<path d="M268.9 182.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M421.5 178h1.6v-6l-2 1v-1l2-1h1.1v7h1.7v1h-4.4z"/>
<path d="M534.9 182.3v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm-763-169V6.4h3.8v1h-3v6z"/>
<path d="M37.9 26.3v-14h.8v14z"/>
<path d="M37.9 39.3v-14h.8v14z"/>
<path d="M37.9 52.3v-14h.8v14z"/>
<path d="M37.9 65.3v-14h.8v14z"/>
<path d="M37.9 78.3v-14h.8v14z"/>
<path d="M37.9 91.3v-14h.8v14z"/>
<path d="M37.9 104.3v-14h.8v14z"/>
<path d="M37.9 117.3v-14h.8v14z"/>
<path d="M37.9 130.3v-14h.8v14z"/>
<path d="M37.9 143.3v-14h.8v14z"/>
<path d="M37.9 156.3v-14h.8v14z"/>
<path d="M37.9 169.3v-14h.8v14z"/>
<path d="M65.9 39.3v-6.9h3.8v1h-3v6z"/>
<path d="M65.9 52.3v-14h.8v14z"/>
<path d="M65.9 65.3v-14h.8v14z"/>
<path d="M65.9 78.3v-14h.8v14z"/>
<path d="M65.9 91.3v-14h.8v14z"/>
<path d="M65.9 104.3v-14h.8v14z"/>
<path d="M65.9 117.3v-14h.8v14z"/>
<path d="M65.9 130.3v-14h.8v14z"/>
<path d="M65.9 137.3v-8h.8v7.1h3v1zm88.1 16.1 3.3-6.5 3.3 6.5zm2.9 15.9v-14h.8v14z"/>
<path d="M268.9 169.3v-14h.8v14z"/>
<path d="M289.9 169.3v-14h.8v14z"/>
<path d="M303.9 169.3v-14h.8v14z"/>
<path d="M422.9 169.3v-14h.8v14z"/>
<path d="M534.9 169.3v-14h.8v14z"/>
<path d="M548.9 169.3v-14h.8v14z"/>
<path d="M569.9 169.3v-14h.8v14z"/>
<path d="M597.9 163.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M800.9 169.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="m420 153.4 3.3-6.5 3.3 6.5z"/>
<path d="M534.9 156.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 156.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 156.3v-14h.8v14z"/>
<path d="M69.9 137.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M268.9 143.3v-14h.8v14z"/>
<path d="M289.9 143.3v-14h.8v14z"/>
<path d="M303.9 143.3v-14h.8v14z"/>
<path d="M331.9 137.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M534.9 143.3v-14h.8v14z"/>
<path d="M548.9 143.3v-14h.8v7.1h3v1h-3v6zm11-6v-.9h6.8v1z"/>
<path d="M569.9 143.3v-14h.8v14z"/>
<path d="M573.9 137.3v-.9h6.8v1zm14.1 3.1V134l6.6 3.2zm9.9 2.9v-14h.8v14zm30.9-3.6-.9.3q-.4.2-1 .2-1.5 0-2.3-1.1-.9-1.1-.9-3.1t.9-3q.9-1.2 2.4-1.2l1 .1.8.4v1l-.9-.4q-.4-.2-1-.2-1 0-1.5.8t-.5 2.5q0 1.6.5 2.5.5.8 1.6.8l1-.2.8-.5zm5.1-1.9q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm4.7 3.9V134h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V134h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm10.6-2.9v-3.2h1v8.4h-1v-.8q-.2.5-.6.7-.5.3-1 .3-1 0-1.7-.9-.6-.8-.6-2.3 0-1.5.6-2.3.6-.9 1.7-.9.5 0 1 .3.4.2.6.7zm-2.9 2.2q0 1.1.4 1.7.3.6 1 .6.8 0 1.1-.6.4-.6.4-1.7t-.4-1.7q-.3-.6-1-.6-.8 0-1.1.6-.4.5-.4 1.7zm16.3-.8q.5.1.8.4.3.3.7 1.2l1.1 2.2h-1.1l-1-2q-.4-.9-.7-1.1-.4-.3-.9-.3h-1v3.4h-1.1v-8h2.2q1.3 0 2 .6t.7 1.7q0 .8-.4 1.3t-1.2.6zm-2-3.2v2.8h1.1q.8 0 1.2-.3.4-.4.4-1.1 0-.7-.4-1-.4-.4-1.2-.4zm6 4.7V134h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V134h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm11.6-1.4v3.7h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4v3.4h-1v-6h1v.9q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm13.8-4v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6l-1.2-.2q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm7.6 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm32.9 7.1v-14h.8v14z"/>
<path d="M800.9 143.3v-14h.8v14z"/>
<path d="M82.4 80.3v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6L79 88q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm158.5 24v-14h.8v14z"/>
<path d="M240.9 117.3v-14h.8v14z"/>
<path d="M240.9 130.3v-14h.8v14zm4-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1z"/>
<path d="M268.9 130.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 130.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 130.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 130.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 130.3v-14h.8v14z"/>
<path d="M269.7 111.3v6h-.8v-6h-3v-.9h3v-7.1h.8v7.1h3v1zm3.2 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h6.8v1h-3v6zm4-6v-.9h6.8v1zm8.2-5.3h4v.9h-3v2l.5-.2h.4q1.3 0 2 .7.7.8.7 2 0 1.3-.7 2-.8.8-2.1.8l-1.2-.1-1-.3v-1l1 .3 1 .1q1 0 1.5-.4.5-.5.5-1.4 0-.8-.6-1.3-.5-.5-1.3-.5l-.9.1-.8.3zm5.8 5.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7.1 3.1V108l6.6 3.2zm9.9 2.9v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 117.3v-14h.8v14z"/>
<path d="M548.9 117.3v-14h.8v14z"/>
<path d="M569.9 117.3v-14h.8v14z"/>
<path d="M597.9 117.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M800.9 117.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 104.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 104.3v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14z"/>
<path d="M90 84.5v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.7 3.5-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm2.3-2V82h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V82h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm12.2-2.5-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm2.5-1.2h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1.6V82h2.2v.8h-2.2V86q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V82h1.6v-1.7zm8.3 5.7-.6 1.7-.7 1.8q-.3.4-.7.6-.3.2-.8.2h-.8v-.8h.6q.4 0 .7-.3l.6-1.3-2.3-6h1l1.8 4.8L131 82h1zm9.7-6h6v.9H144V88h-1.1v-7.1h-2.5zm10 2.7q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm5-2.2h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7V88h-1zm11.7 4.9v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm7.6.1V88h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V88h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm13.8-4v1l-1-.4-1-.2q-.7 0-1.2.4-.4.3-.4 1 0 .5.3.7.3.3 1 .5l.6.1q1.2.3 1.7.9.5.5.5 1.5 0 1.2-.7 1.8-.7.6-2 .6L184 88q-.6 0-1.2-.3v-1.2l1.2.6 1.1.2q.8 0 1.3-.4t.5-1q0-.6-.4-1-.3-.3-1-.4l-.6-.2q-1.1-.2-1.6-.7-.6-.5-.6-1.4 0-1 .8-1.7.7-.7 1.9-.7l1 .1 1 .4zm7.6 4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm8.2-1-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm1.3-1.2h1l1.8 5 1.8-5h1l-2.2 6h-1.2zm8.2 0h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.5 8-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm7.3-3.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm11.9 7.1v-14h.8v14z"/>
<path d="M268.9 91.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 91.3v-14h.8v14zM367.3 81l-1.1 4h2.3zm-.6-1h1.3l2.4 8h-1.1l-.6-2H366l-.6 2h-1.1zm10 7.7-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm4.6-7.4V82h2.2v.8h-2.2V86q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V82h1.6v-1.7zm5.4 1.7h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm7.1 3q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.3 2.5V88h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V88h-1v-6h1v.9q.3-.5.7-.8.5-.3 1.1-.3 1 0 1.4.6.4.6.4 1.9zm6.6-2.1v1l-.8-.4-.9-.1q-.7 0-1 .2-.3.2-.3.6 0 .5.2.6l1.2.4.4.1q.8.2 1.2.6.3.4.3 1 0 1-.6 1.5t-1.8.5l-.9-.1-1-.3v-1l1 .4 1 .1q.6 0 1-.2.3-.3.3-.8 0-.7-1.3-1h-.4q-.9-.2-1.2-.6-.4-.4-.4-1 0-1 .6-1.4.5-.5 1.6-.5l1 .1.8.3zM420 80h1l.8 6.5 1-4.3h1l1 4.3.8-6.5h1l-1.2 8h-1l-1-4.8-1.2 4.8h-1zm10.3 2.7q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm9.9 1.4-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm2.1-3.6h1v4.9l2.6-2.5h1.2l-2.4 2.3 2.8 3.7h-1.3l-2.2-3.1-.7.7V88h-1zm11.4 0v.9h-1.1q-.5 0-.8.2-.2.2-.2.8v.5h2.1v.8h-2V88h-1v-5.2H449V82h1.7v-.4q0-1 .4-1.5.5-.5 1.4-.5zm5.2 6.2q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm6.4-3.1q-.7 0-1.1.6-.4.5-.4 1.7 0 1.1.4 1.7.4.6 1.1.6.8 0 1.2-.6.3-.6.3-1.7 0-1.2-.3-1.7-.4-.6-1.2-.6zm0-.9q1.3 0 2 .9.6.8.6 2.3 0 1.5-.7 2.3-.6.9-1.9.9-1.2 0-1.9-.9-.7-.8-.7-2.3 0-1.5.7-2.3.7-.9 2-.9zm3.7.2h1l1 4.8.9-3h.8l.9 3 1-4.8h1l-1.4 6h-1l-.9-3.3-1 3.3h-.9zm37.9 9.3v-14h.8v14zm11-6v-.9h6.8v1zm19.4-5.1v1l-.7-.3-.8-.2q-1 0-1.6.8-.5.8-.5 2.3.3-.5.7-.8.5-.3 1-.3 1.2 0 1.9.7.6.7.6 2t-.6 2q-.7.8-1.9.8-1.4 0-2-1-.7-1-.7-3.2 0-2 .8-3.1.8-1 2.3-1h.8l.7.3zm-2 3.3q-.6 0-1 .5t-.4 1.4q0 1 .4 1.4.4.5 1 .5.8 0 1.2-.5.3-.4.3-1.4 0-1-.3-1.4-.4-.5-1.1-.5zm13.6 7.8v-6h-3v-.9h3v-7.1h.8v14z"/>
<path d="M569.9 91.3v-14h.8v14z"/>
<path d="M597.9 85.3v-8h.8v7.1h3v1zm4 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h3v-7.1h.8v8z"/>
<path d="M800.9 91.3v-14h.8v14z"/>
<path d="M240.9 52.3v-14h.8v14z"/>
<path d="M240.9 65.3v-14h.8v14z"/>
<path d="M240.9 78.3v-14h.8v14zm4.1-19.2 6.6-3.2v6.5z"/>
<path d="M268.9 78.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 78.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 78.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 78.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 78.3v-14h.8v14z"/>
<path d="M251.9 59.3v-.9h6.8v1zm7 0v-.9h6.8v1zM268 61h3.7v1h-4.9v-1l1.8-1.8 1-1.1.7-1 .2-1q0-.6-.4-1t-1-.4l-1 .2-1.2.6v-1.1l1-.4 1.1-.2q1.2 0 2 .7.6.6.6 1.6l-.2 1q-.2.5-.8 1.2l-.9 1L268 61zm4.9-1.7v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10.8 0v6h-.8v-6h-3v-.9h3v-7.1h.8v7.1h3v1zm3.2 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 65.3v-14h.8v14z"/>
<path d="M548.9 65.3v-6.9h3.8v1h-3v6zm11-6v-.9h6.8v1z"/>
<path d="M569.9 65.3v-14h.8v14z"/>
<path d="M573.9 59.3v-.9h6.8v1zm14.1 3.1V56l6.6 3.2zm9.9 2.9v-14h.8v14zm35.4-4.3 1.9-7h1.1l-2.3 8h-1.3l-2.4-8h1.1zm5.4-5h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm10 3.5-.6-.3-.8-.1q-.9 0-1.4.5-.5.6-.5 1.7v3h-1v-6h1v1.2q.3-.7.8-1 .5-.4 1.2-.4l.7.1.6.3zm4.1-2.9V56h2.2v.8h-2.2V60q0 .7.3 1 .2.2.8.2h1.1v.8h-1.2q-1 0-1.5-.4-.5-.5-.5-1.6v-3.2h-1.6V56h1.6v-1.7zm4.7 5.4V56h1v3.7q0 .8.3 1.2.3.4 1 .4.6 0 1-.5t.4-1.4V56h1v6h-1v-.9q-.3.5-.8.8-.4.3-1 .3-1 0-1.4-.6-.5-.7-.5-1.9zm9.8-.7h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6V59zm1.9-.4V62h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3V58q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm5.2 1.2q0 .7.2 1 .3.4.7.4h1.2v.8h-1.3q-.9 0-1.3-.6-.5-.5-.5-1.6v-5.4h-1.6v-.8h2.6zm10.3-5.8h1.4l1.4 4 1.4-4h1.5v8h-1v-7l-1.5 4.1h-.8l-1.4-4.2V62h-1zm10.6 5h-.4q-.8 0-1.3.3-.4.3-.4.9 0 .5.3.8.4.3 1 .3.8 0 1.2-.5.5-.6.5-1.6V59zm1.9-.4V62h-1v-.9q-.3.5-.8.8-.5.3-1.2.3-.9 0-1.4-.6-.6-.5-.6-1.4 0-1 .7-1.5t2-.5h1.3V58q0-.7-.4-1-.3-.3-1.1-.3l-1 .1-1 .4v-1l1-.3h1q.7 0 1.3.2.5.2.8.6l.3.7v1.2zm7 3.1-.8.3q-.4.2-.9.2-1.4 0-2.2-.9-.8-.8-.8-2.3 0-1.5.8-2.3.8-.9 2.2-.9.5 0 .9.2.4 0 .8.3v1l-.8-.5-.9-.1q-1 0-1.4.6-.5.6-.5 1.7t.5 1.7q.5.6 1.4.6l1-.1.7-.5zm6.9-3.4V62h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V62h-1v-8.4h1V57q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm3.1-2.3h2.5v5.2h2v.8h-5v-.8h2v-4.4h-1.5zm1.5-2.3h1v1.2h-1zm9.4 4.6V62h-1v-3.7q0-.8-.2-1.2-.3-.4-1-.4-.6 0-1 .5t-.4 1.4V62h-1v-6h1v.9q.3-.5.8-.8.4-.3 1-.3 1 0 1.4.6.4.6.4 1.9zm7.4.2v.5h-4.3q0 1.2.5 1.8.5.5 1.5.5l1-.1 1-.5v1l-1 .3-1 .2q-1.5 0-2.2-.9-.8-.8-.8-2.3 0-1.4.7-2.3.8-.9 2.1-.9 1.1 0 1.8.8t.7 1.9zm-1-.3q0-.6-.4-1-.4-.5-1.1-.5-.8 0-1.2.4-.5.5-.5 1.1zm39.9 7.1v-14h.8v14z"/>
<path d="M800.9 65.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 52.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M534.9 52.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 52.3v-14h.8v14zm175 0v-14h.8v14z"/>
<path d="M800.9 52.3v-14h.8v14z"/>
<path d="M69.9 33.3v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M268.9 39.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M331.9 39.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<path d="M534.9 39.3v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M597.9 39.3v-6.9h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7z"/>
<g>
<path d="M800.9 39.3v-14h.8v14zm-532-13v-14h.8v14zm35 0v-14h.8v14zm231 0v-14h.8v14zm35 0v-14h.8v14z"/>
<path d="M800.9 26.3v-14h.8v14zm-759-19v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm35 0V6.4h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm10 6v-6h-3v-.9h3.8v7zm35 0V6.4h3.8v1h-3v6zm4-6v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1zm7 0v-.9h6.8v1z"/>
<path d="M800.9 13.3v-6h-3v-.9h3.8v7z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 46 KiB

144
package-lock.json generated
View File

@ -14,16 +14,16 @@
"@google-github-actions/actions-utils": "^0.4.10"
},
"devDependencies": {
"@types/node": "^20.9.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@vercel/ncc": "^0.38.1",
"eslint": "^8.53.0",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
"typescript": "^5.3.2"
}
},
"node_modules/@aashutoshrathi/word-wrap": {
@ -113,9 +113,9 @@
}
},
"node_modules/@eslint/js": {
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.53.0.tgz",
"integrity": "sha512-Kn7K8dx/5U6+cT1yEhpX1w4PCSg0M+XyRILPgvwcEBjerFWCwQj5sbr3/VmxqV0JGHCBCzyd6LxypEuehypY1w==",
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.54.0.tgz",
"integrity": "sha512-ut5V+D+fOoWPgGGNj83GGjnntO39xDy6DWxO0wb7Jp3DcMX0TfIqdzHF85VTQkerdyGmuuMD9AKAo5KiNlf/AQ==",
"dev": true,
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
@ -281,31 +281,31 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.9.0.tgz",
"integrity": "sha512-nekiGu2NDb1BcVofVcEKMIwzlx4NjHlcjhoxxKBNLtz15Y1z7MYf549DFvkHSId02Ax6kGwWntIBPC3l/JZcmw==",
"version": "20.10.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.0.tgz",
"integrity": "sha512-D0WfRmU9TQ8I9PFx9Yc+EBHw+vSpIub4IDvQivcp26PtPrdMGAq5SDcpXEo/epqa/DXotVpekHiLNTg3iaKXBQ==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/@types/semver": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.5.tgz",
"integrity": "sha512-+d+WYC1BxJ6yVOgUgzK8gWvp5qF8ssV5r4nsDcZWKRWcDQLQ619tvWAxJQYGgBrO1MnLJC7a5GtiYsAoQ47dJg==",
"version": "7.5.6",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.6.tgz",
"integrity": "sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.11.0.tgz",
"integrity": "sha512-uXnpZDc4VRjY4iuypDBKzW1rz9T5YBBK0snMn8MaTSNd2kMlj50LnLBABELjJiOL5YHk7ZD8hbSpI9ubzqYI0w==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.12.0.tgz",
"integrity": "sha512-XOpZ3IyJUIV1b15M7HVOpgQxPPF7lGXgsfcEIu3yDxFPaf/xZKt7s9QO/pbk7vpWQyVulpJbu4E5LwpZiQo4kA==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.5.1",
"@typescript-eslint/scope-manager": "6.11.0",
"@typescript-eslint/type-utils": "6.11.0",
"@typescript-eslint/utils": "6.11.0",
"@typescript-eslint/visitor-keys": "6.11.0",
"@typescript-eslint/scope-manager": "6.12.0",
"@typescript-eslint/type-utils": "6.12.0",
"@typescript-eslint/utils": "6.12.0",
"@typescript-eslint/visitor-keys": "6.12.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0",
"ignore": "^5.2.4",
@ -331,15 +331,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.11.0.tgz",
"integrity": "sha512-+whEdjk+d5do5nxfxx73oanLL9ghKO3EwM9kBCkUtWMRwWuPaFv9ScuqlYfQ6pAD6ZiJhky7TZ2ZYhrMsfMxVQ==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.12.0.tgz",
"integrity": "sha512-s8/jNFPKPNRmXEnNXfuo1gemBdVmpQsK1pcu+QIvuNJuhFzGrpD7WjOcvDc/+uEdfzSYpNu7U/+MmbScjoQ6vg==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "6.11.0",
"@typescript-eslint/types": "6.11.0",
"@typescript-eslint/typescript-estree": "6.11.0",
"@typescript-eslint/visitor-keys": "6.11.0",
"@typescript-eslint/scope-manager": "6.12.0",
"@typescript-eslint/types": "6.12.0",
"@typescript-eslint/typescript-estree": "6.12.0",
"@typescript-eslint/visitor-keys": "6.12.0",
"debug": "^4.3.4"
},
"engines": {
@ -359,13 +359,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.11.0.tgz",
"integrity": "sha512-0A8KoVvIURG4uhxAdjSaxy8RdRE//HztaZdG8KiHLP8WOXSk0vlF7Pvogv+vlJA5Rnjj/wDcFENvDaHb+gKd1A==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.12.0.tgz",
"integrity": "sha512-5gUvjg+XdSj8pcetdL9eXJzQNTl3RD7LgUiYTl8Aabdi8hFkaGSYnaS6BLc0BGNaDH+tVzVwmKtWvu0jLgWVbw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.11.0",
"@typescript-eslint/visitor-keys": "6.11.0"
"@typescript-eslint/types": "6.12.0",
"@typescript-eslint/visitor-keys": "6.12.0"
},
"engines": {
"node": "^16.0.0 || >=18.0.0"
@ -376,13 +376,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.11.0.tgz",
"integrity": "sha512-nA4IOXwZtqBjIoYrJcYxLRO+F9ri+leVGoJcMW1uqr4r1Hq7vW5cyWrA43lFbpRvQ9XgNrnfLpIkO3i1emDBIA==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.12.0.tgz",
"integrity": "sha512-WWmRXxhm1X8Wlquj+MhsAG4dU/Blvf1xDgGaYCzfvStP2NwPQh6KBvCDbiOEvaE0filhranjIlK/2fSTVwtBng==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "6.11.0",
"@typescript-eslint/utils": "6.11.0",
"@typescript-eslint/typescript-estree": "6.12.0",
"@typescript-eslint/utils": "6.12.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.0.1"
},
@ -403,9 +403,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.11.0.tgz",
"integrity": "sha512-ZbEzuD4DwEJxwPqhv3QULlRj8KYTAnNsXxmfuUXFCxZmO6CF2gM/y+ugBSAQhrqaJL3M+oe4owdWunaHM6beqA==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.12.0.tgz",
"integrity": "sha512-MA16p/+WxM5JG/F3RTpRIcuOghWO30//VEOvzubM8zuOOBYXsP+IfjoCXXiIfy2Ta8FRh9+IO9QLlaFQUU+10Q==",
"dev": true,
"engines": {
"node": "^16.0.0 || >=18.0.0"
@ -416,13 +416,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.11.0.tgz",
"integrity": "sha512-Aezzv1o2tWJwvZhedzvD5Yv7+Lpu1by/U1LZ5gLc4tCx8jUmuSCMioPFRjliN/6SJIvY6HpTtJIWubKuYYYesQ==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.12.0.tgz",
"integrity": "sha512-vw9E2P9+3UUWzhgjyyVczLWxZ3GuQNT7QpnIY3o5OMeLO/c8oHljGc8ZpryBMIyympiAAaKgw9e5Hl9dCWFOYw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.11.0",
"@typescript-eslint/visitor-keys": "6.11.0",
"@typescript-eslint/types": "6.12.0",
"@typescript-eslint/visitor-keys": "6.12.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@ -443,17 +443,17 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.11.0.tgz",
"integrity": "sha512-p23ibf68fxoZy605dc0dQAEoUsoiNoP3MD9WQGiHLDuTSOuqoTsa4oAy+h3KDkTcxbbfOtUjb9h3Ta0gT4ug2g==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.12.0.tgz",
"integrity": "sha512-LywPm8h3tGEbgfyjYnu3dauZ0U7R60m+miXgKcZS8c7QALO9uWJdvNoP+duKTk2XMWc7/Q3d/QiCuLN9X6SWyQ==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.12",
"@types/semver": "^7.5.0",
"@typescript-eslint/scope-manager": "6.11.0",
"@typescript-eslint/types": "6.11.0",
"@typescript-eslint/typescript-estree": "6.11.0",
"@typescript-eslint/scope-manager": "6.12.0",
"@typescript-eslint/types": "6.12.0",
"@typescript-eslint/typescript-estree": "6.12.0",
"semver": "^7.5.4"
},
"engines": {
@ -468,12 +468,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "6.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.11.0.tgz",
"integrity": "sha512-+SUN/W7WjBr05uRxPggJPSzyB8zUpaYo2hByKasWbqr3PM8AXfZt8UHdNpBS1v9SA62qnSSMF3380SwDqqprgQ==",
"version": "6.12.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.12.0.tgz",
"integrity": "sha512-rg3BizTZHF1k3ipn8gfrzDXXSFKyOEB5zxYXInQ6z0hUvmQlhaZQzK+YmHmNViMA9HzW5Q9+bPPt90bU6GQwyw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "6.11.0",
"@typescript-eslint/types": "6.12.0",
"eslint-visitor-keys": "^3.4.1"
},
"engines": {
@ -597,9 +597,9 @@
"dev": true
},
"node_modules/big-integer": {
"version": "1.6.51",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
"integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
"version": "1.6.52",
"resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz",
"integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==",
"dev": true,
"engines": {
"node": ">=0.6"
@ -838,15 +838,15 @@
}
},
"node_modules/eslint": {
"version": "8.53.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.53.0.tgz",
"integrity": "sha512-N4VuiPjXDUa4xVeV/GC/RV3hQW9Nw+Y463lkWaKKXKYMvmRiRDAtfpuPFLN+E1/6ZhyR8J2ig+eVREnYgUsiag==",
"version": "8.54.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-8.54.0.tgz",
"integrity": "sha512-NY0DfAkM8BIZDVl6PgSa1ttZbx3xHgJzSNJKYcQglem6CppHyMhRIQkBVSSMaSRnLhig3jsDbEzOjwCVt4AmmA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
"@eslint/eslintrc": "^2.1.3",
"@eslint/js": "8.53.0",
"@eslint/js": "8.54.0",
"@humanwhocodes/config-array": "^0.11.13",
"@humanwhocodes/module-importer": "^1.0.1",
"@nodelib/fs.walk": "^1.2.8",
@ -1274,9 +1274,9 @@
}
},
"node_modules/ignore": {
"version": "5.2.4",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz",
"integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==",
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.0.tgz",
"integrity": "sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==",
"dev": true,
"engines": {
"node": ">= 4"
@ -2242,9 +2242,9 @@
}
},
"node_modules/typescript": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.2.2.tgz",
"integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==",
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.2.tgz",
"integrity": "sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==",
"dev": true,
"bin": {
"tsc": "bin/tsc",
@ -2255,9 +2255,9 @@
}
},
"node_modules/undici": {
"version": "5.27.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz",
"integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==",
"version": "5.28.0",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.28.0.tgz",
"integrity": "sha512-gM12DkXhlAc5+/TPe60iy9P6ETgVfqTuRJ6aQ4w8RYu0MqKuXhaq3/b86GfzDQnNA3NUO6aUNdvevrKH59D0Nw==",
"dependencies": {
"@fastify/busboy": "^2.0.0"
},

View File

@ -28,15 +28,15 @@
"@google-github-actions/actions-utils": "^0.4.10"
},
"devDependencies": {
"@types/node": "^20.9.0",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@types/node": "^20.10.0",
"@typescript-eslint/eslint-plugin": "^6.12.0",
"@typescript-eslint/parser": "^6.12.0",
"@vercel/ncc": "^0.38.1",
"eslint": "^8.53.0",
"eslint": "^8.54.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.1",
"prettier": "^3.1.0",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
"typescript": "^5.3.2"
}
}

View File

@ -12,157 +12,210 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { HttpClient } from '@actions/http-client';
import { URLSearchParams } from 'url';
import {
GoogleAccessTokenParameters,
GoogleAccessTokenResponse,
GoogleIDTokenParameters,
GoogleIDTokenResponse,
} from './client/auth_client';
// Do not listen to the linter - this can NOT be rewritten as an ES6 import statement.
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version: appVersion } = require('../package.json');
import { HttpClient } from '@actions/http-client';
// userAgent is the default user agent.
const userAgent = `google-github-actions:auth/${appVersion}`;
import { Logger } from './logger';
import { expandEndpoint, userAgent } from './utils';
/**
* BaseClient is the default HTTP client for interacting with the IAM
* credentials API.
* GenerateAccessTokenParameters are the inputs to the generateAccessToken call.
*/
export class BaseClient {
/**
* client is the HTTP client.
*/
protected readonly client: HttpClient;
export interface GenerateAccessTokenParameters {
readonly serviceAccount: string;
readonly delegates?: string[];
readonly scopes?: string[];
readonly lifetime?: number;
}
constructor() {
this.client = new HttpClient(userAgent);
/**
* GenerateIDTokenParameters are the inputs to the generateIDToken call.
*/
export interface GenerateIDTokenParameters {
readonly serviceAccount: string;
readonly audience: string;
readonly delegates?: string[];
readonly includeEmail?: boolean;
}
/**
* IAMCredentialsClientParameters are the inputs to the IAM client.
*/
export interface IAMCredentialsClientParameters {
readonly authToken: string;
}
/**
* IAMCredentialsClient is a thin HTTP client around the Google Cloud IAM
* Credentials API.
*/
export class IAMCredentialsClient {
readonly #logger: Logger;
readonly #httpClient: HttpClient;
readonly #authToken: string;
readonly #universe: string = 'googleapis.com';
readonly #endpoints = {
iamcredentials: 'https://iamcredentials.{universe}/v1',
oauth2: 'https://oauth2.{universe}',
};
constructor(logger: Logger, opts: IAMCredentialsClientParameters) {
this.#logger = logger.withNamespace(this.constructor.name);
this.#httpClient = new HttpClient(userAgent);
this.#authToken = opts.authToken;
const endpoints = this.#endpoints;
for (const key of Object.keys(this.#endpoints) as Array<keyof typeof endpoints>) {
this.#endpoints[key] = expandEndpoint(this.#endpoints[key], this.#universe);
}
this.#logger.debug(`Computed endpoints`, this.#endpoints);
}
/**
* googleIDToken generates a Google Cloud ID token for the provided
* service account email or unique id.
* generateAccessToken generates a new OAuth 2.0 Access Token for a service
* account.
*/
async googleIDToken(
token: string,
{ serviceAccount, audience, delegates, includeEmail }: GoogleIDTokenParameters,
): Promise<GoogleIDTokenResponse> {
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
async generateAccessToken({
serviceAccount,
delegates,
scopes,
lifetime,
}: GenerateAccessTokenParameters): Promise<string> {
const pth = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
const data = {
delegates: delegates,
audience: audience,
includeEmail: includeEmail,
};
const headers = { Authorization: `Bearer ${this.#authToken}` };
const headers = {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
};
try {
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
}
const parsed = JSON.parse(body);
return {
token: parsed['token'],
};
} catch (err) {
throw new Error(`failed to generate Google Cloud ID token for ${serviceAccount}: ${err}`);
}
}
/**
* googleAccessToken generates a Google Cloud access token for the provided
* service account email or unique id.
*/
async googleAccessToken(
token: string,
{ serviceAccount, delegates, scopes, lifetime }: GoogleAccessTokenParameters,
): Promise<GoogleAccessTokenResponse> {
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
const data: Record<string, string | Array<string>> = {};
const body: Record<string, string | Array<string>> = {};
if (delegates && delegates.length > 0) {
data.delegates = delegates;
body.delegates = delegates;
}
if (scopes && scopes.length > 0) {
// Not a typo, the API expects the field to be "scope" (singular).
data.scope = scopes;
body.scope = scopes;
}
if (lifetime && lifetime > 0) {
data.lifetime = `${lifetime}s`;
body.lifetime = `${lifetime}s`;
}
const headers = {
'Authorization': `Bearer ${token}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
};
this.#logger.withNamespace('generateAccessToken').debug({
method: `POST`,
path: pth,
headers: headers,
body: body,
});
try {
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
const resp = await this.#httpClient.postJson<{ accessToken: string }>(pth, body, headers);
const statusCode = resp.statusCode || 500;
if (statusCode < 200 || statusCode > 299) {
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
}
const parsed = JSON.parse(body);
return {
accessToken: parsed['accessToken'],
expiration: parsed['expireTime'],
};
const result = resp.result;
if (!result) {
throw new Error(`Successfully called ${pth}, but the result was empty`);
}
return result.accessToken;
} catch (err) {
throw new Error(`Failed to generate Google Cloud access token for ${serviceAccount}: ${err}`);
throw new Error(
`Failed to generate Google Cloud OAuth 2.0 Access Token for ${serviceAccount}: ${err}`,
);
}
}
/**
* googleOAuthToken generates a Google Cloud OAuth token using the legacy
* OAuth endpoints.
*
* @param assertion A signed JWT.
*/
async googleOAuthToken(assertion: string): Promise<GoogleAccessTokenResponse> {
const pth = `https://oauth2.googleapis.com/token`;
async generateDomainWideDelegationAccessToken(assertion: string): Promise<string> {
const pth = `${this.#endpoints.oauth2}/token`;
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/x-www-form-urlencoded',
};
const data = new URLSearchParams();
data.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
data.append('assertion', assertion);
const body = new URLSearchParams();
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
body.append('assertion', assertion);
this.#logger.withNamespace('generateDomainWideDelegationAccessToken').debug({
method: `POST`,
path: pth,
headers: headers,
body: body,
});
try {
const resp = await this.client.request('POST', pth, data.toString(), headers);
const body = await resp.readBody();
const resp = await this.#httpClient.post(pth, body.toString(), headers);
const respBody = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
if (statusCode < 200 || statusCode > 299) {
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${respBody || '[no body]'}`);
}
const parsed = JSON.parse(body);
// Normalize the expiration to be a timestamp like the iamcredentials API.
// This API returns the number of seconds until expiration, so convert
// that into a date.
const expiration = new Date(new Date().getTime() + parsed['expires_in'] * 10000);
return {
accessToken: parsed['access_token'],
expiration: expiration.toISOString(),
};
const parsed = JSON.parse(respBody) as { accessToken: string };
return parsed.accessToken;
} catch (err) {
throw new Error(`Failed to generate Google Cloud OAuth token: ${err}`);
throw new Error(
`Failed to generate Google Cloud Domain Wide Delegation OAuth 2.0 Access Token: ${err}`,
);
}
}
/**
* generateIDToken generates a new OpenID Connect ID token for a service
* account.
*/
async generateIDToken({
serviceAccount,
audience,
delegates,
includeEmail,
}: GenerateIDTokenParameters): Promise<string> {
const pth = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
const headers = { Authorization: `Bearer ${this.#authToken}` };
const body: Record<string, string | string[] | boolean> = {
audience: audience,
includeEmail: includeEmail ? true : false,
};
if (delegates && delegates.length > 0) {
body.delegates = delegates;
}
this.#logger.withNamespace('generateIDToken').debug({
method: `POST`,
path: pth,
headers: headers,
body: body,
});
try {
const resp = await this.#httpClient.postJson<{ token: string }>(pth, body, headers);
const statusCode = resp.statusCode || 500;
if (statusCode < 200 || statusCode > 299) {
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
}
const result = resp.result;
if (!result) {
throw new Error(`Successfully called ${pth}, but the result was empty`);
}
return result.token;
} catch (err) {
throw new Error(
`Failed to generate Google Cloud OpenID Connect ID token for ${serviceAccount}: ${err}`,
);
}
}
}
export { AuthClient } from './client/auth_client';
export {
ServiceAccountKeyClientParameters,
ServiceAccountKeyClient,
} from './client/credentials_json_client';
export {
WorkloadIdentityFederationClientParameters,
WorkloadIdentityFederationClient,
} from './client/workload_identity_client';

View File

@ -13,83 +13,24 @@
// limitations under the License.
/**
* Defines the main interface for all clients that generate credentials.
* Client is the default HTTP client for interacting with the IAM credentials
* API.
*/
export interface AuthClient {
getAuthToken(): Promise<string>;
signJWT(unsignedJWT: string, delegates?: Array<string>): Promise<string>;
getProjectID(): Promise<string>;
getServiceAccount(): Promise<string>;
createCredentialsFile(outputDir: string): Promise<string>;
/**
* getToken() gets or generates the best token for the auth client.
*/
getToken(): Promise<string>;
/**
* Provided by BaseClient.
* createCredentialsFile creates a credential file (for use with gcloud and
* other Google Cloud tools) that instructs the tool how to perform identity
* federation.
*/
googleIDToken(token: string, params: GoogleIDTokenParameters): Promise<GoogleIDTokenResponse>;
googleAccessToken(
token: string,
params: GoogleAccessTokenParameters,
): Promise<GoogleAccessTokenResponse>;
googleOAuthToken(assertion: string): Promise<GoogleAccessTokenResponse>;
}
createCredentialsFile(outputPath: string): Promise<string>;
/**
* GoogleAccessTokenParameters are the parameters to generate a Google Cloud
* access token as described in:
*
* https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateAccessToken
*
* @param serviceAccount Optional email address or unique identifier of the
* service account.
* @param delegates Optional sequence of service accounts in the delegation
* chain.
* @param lifetime Optional validity period as a number representing the number
* of seconds.
/**
* signJWT signs a JWT using the auth provider.
*/
export interface GoogleAccessTokenParameters {
serviceAccount?: string;
delegates?: Array<string>;
scopes?: Array<string>;
lifetime?: number;
}
/**
* GoogleAccessTokenResponse is the response from generating an access token.
*
* @param accessToken OAuth 2.0 access token.
* @param expiration A timestamp in RFC3339 UTC "Zulu" format when the token
* expires.
*/
export interface GoogleAccessTokenResponse {
accessToken: string;
expiration: string;
}
/**
* GoogleIDTokenParameters are the parameters to generate a Google Cloud
* ID token as described in:
*
* https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/generateIdToken
*
* @param serviceAccount Email address or unique identifier of the service
* account.
* @param audience The audience for the token.
* @param delegates Optional sequence of service accounts in the delegation
* chain.
*/
export interface GoogleIDTokenParameters {
serviceAccount?: string;
audience: string;
delegates?: Array<string>;
includeEmail?: boolean;
}
/**
* GoogleIDTokenResponse is the response from generating an ID token.
*
* @param token ID token.
* expires.
*/
export interface GoogleIDTokenResponse {
token: string;
signJWT(claims: any): Promise<string>;
}

View File

@ -13,6 +13,7 @@
// limitations under the License.
import { createSign } from 'crypto';
import {
isServiceAccountKey,
parseCredential,
@ -22,123 +23,112 @@ import {
} from '@google-github-actions/actions-utils';
import { AuthClient } from './auth_client';
import { BaseClient } from '../base';
import { expandEndpoint } from '../utils';
import { Logger } from '../logger';
/**
* Available options to create the CredentialsJSONClient.
*
* @param projectID User-supplied value for project ID. If not provided, the
* project ID is extracted from the credentials JSON.
* @param credentialsJSON Raw JSON credentials blob.
* ServiceAccountKeyClientParameters is used as input to the
* ServiceAccountKeyClient.
*/
interface CredentialsJSONClientOptions {
projectID?: string;
credentialsJSON: string;
export interface ServiceAccountKeyClientParameters {
readonly serviceAccountKey: string;
}
/**
* CredentialsJSONClient is a client that accepts a service account key JSON
* credential.
* ServiceAccountKeyClient is an authentication client that expects a Service
* Account Key JSON file.
*/
export class CredentialsJSONClient extends BaseClient implements AuthClient {
readonly #projectID: string;
readonly #credentials: ServiceAccountKey;
export class ServiceAccountKeyClient implements AuthClient {
readonly #logger: Logger;
readonly #serviceAccountKey: ServiceAccountKey;
constructor(opts: CredentialsJSONClientOptions) {
super();
readonly #universe: string = 'googleapis.com';
readonly #endpoints = {
iamcredentials: 'https://iamcredentials.{universe}/v1',
};
readonly #audience: string;
const credentials = parseCredential(opts.credentialsJSON);
if (!isServiceAccountKey(credentials)) {
throw new Error(`Provided credential is not a valid service account key JSON`);
constructor(logger: Logger, opts: ServiceAccountKeyClientParameters) {
this.#logger = logger.withNamespace(this.constructor.name);
const serviceAccountKey = parseCredential(opts.serviceAccountKey);
if (!isServiceAccountKey(serviceAccountKey)) {
throw new Error(`Provided credential is not a valid Google Service Account Key JSON`);
}
this.#credentials = credentials;
this.#serviceAccountKey = serviceAccountKey;
this.#projectID = opts.projectID || this.#credentials.project_id;
const endpoints = this.#endpoints;
for (const key of Object.keys(this.#endpoints) as Array<keyof typeof endpoints>) {
this.#endpoints[key] = expandEndpoint(this.#endpoints[key], this.#universe);
}
this.#logger.debug(`Computed endpoints`, this.#endpoints);
this.#audience = new URL(this.#endpoints.iamcredentials).origin + `/`;
this.#logger.debug(`Computed audience`, this.#audience);
}
/**
* getAuthToken generates a token capable of calling the iamcredentials API.
* getToken generates a self-signed JWT that, by default, is capable of
* calling the iamcredentials API to mint OAuth 2.0 Access Tokens and ID
* Tokens. However, users can theoretically override the audience value and
* use the JWT to call other endpoints without calling iamcredentials.
*/
async getAuthToken(): Promise<string> {
const header = {
alg: 'RS256',
typ: 'JWT',
kid: this.#credentials.private_key_id,
};
async getToken(): Promise<string> {
try {
const now = Math.floor(new Date().getTime() / 1000);
const body = {
iss: this.#credentials.client_email,
sub: this.#credentials.client_email,
aud: 'https://iamcredentials.googleapis.com/',
const claims = {
iss: this.#serviceAccountKey.client_email,
sub: this.#serviceAccountKey.client_email,
aud: this.#audience,
iat: now,
exp: now + 3599,
};
const message = toBase64(JSON.stringify(header)) + '.' + toBase64(JSON.stringify(body));
this.#logger.withNamespace('getToken').debug({
claims: claims,
});
try {
const signer = createSign('RSA-SHA256');
signer.write(message);
signer.end();
const signature = signer.sign(this.#credentials.private_key);
return message + '.' + toBase64(signature);
return await this.signJWT(claims);
} catch (err) {
throw new Error(`Failed to sign auth token using ${await this.getServiceAccount()}: ${err}`);
throw new Error(
`Failed to sign auth token using ${this.#serviceAccountKey.client_email}: ${err}`,
);
}
}
/**
* signJWT signs the given JWT with the private key.
*
* @param unsignedJWT The JWT to sign.
* signJWT signs a JWT using the Service Account's private key.
*/
async signJWT(unsignedJWT: string): Promise<string> {
async signJWT(claims: any): Promise<string> {
const header = {
alg: 'RS256',
typ: 'JWT',
kid: this.#credentials.private_key_id,
alg: `RS256`,
typ: `JWT`,
kid: this.#serviceAccountKey.private_key_id,
};
const message = toBase64(JSON.stringify(header)) + '.' + toBase64(unsignedJWT);
const message = toBase64(JSON.stringify(header)) + `.` + toBase64(JSON.stringify(claims));
try {
const signer = createSign('RSA-SHA256');
this.#logger.withNamespace('signJWT').debug({
header: header,
claims: claims,
message: message,
});
const signer = createSign(`RSA-SHA256`);
signer.write(message);
signer.end();
const signature = signer.sign(this.#credentials.private_key);
const jwt = message + '.' + toBase64(signature);
return jwt;
} catch (err) {
throw new Error(`Failed to sign JWT using ${await this.getServiceAccount()}: ${err}`);
}
const signature = signer.sign(this.#serviceAccountKey.private_key);
return message + '.' + toBase64(signature);
}
/**
* getProjectID returns the project ID. If an override was given, the override
* is returned. Otherwise, this will be the project ID that was extracted from
* the service account key JSON.
*/
async getProjectID(): Promise<string> {
return this.#projectID;
}
/**
* getServiceAccount returns the service account email for the authentication,
* extracted from the Service Account Key JSON.
*/
async getServiceAccount(): Promise<string> {
return this.#credentials.client_email;
}
/**
* createCredentialsFile creates a Google Cloud credentials file that can be
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
* createCredentialsFile writes the Service Account Key JSON back to disk at
* the specified outputPath.
*/
async createCredentialsFile(outputPath: string): Promise<string> {
return await writeSecureFile(outputPath, JSON.stringify(this.#credentials));
this.#logger.withNamespace('createCredentialsFile').debug({ outputPath: outputPath });
return await writeSecureFile(outputPath, JSON.stringify(this.#serviceAccountKey));
}
}

View File

@ -12,211 +12,208 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { URL } from 'url';
import { HttpClient } from '@actions/http-client';
import { writeSecureFile } from '@google-github-actions/actions-utils';
import { AuthClient } from './auth_client';
import { BaseClient } from '../base';
import { expandEndpoint, userAgent } from '../utils';
import { Logger } from '../logger';
/**
* Available options to create the WorkloadIdentityClient.
*
* @param projectID User-supplied value for project ID. If not provided, the
* project ID is extracted from the service account email.
* @param providerID Full path (including project, location, etc) to the Google
* Cloud Workload Identity Provider.
* @param serviceAccount Email address or unique identifier of the service
* account to impersonate
* @param token GitHub OIDC token to use for exchanging with Workload Identity
* Federation.
* @param audience The value for the audience parameter in the generated GitHub
* Actions OIDC token, defaults to the value of workload_identity_provider
* WorkloadIdentityFederationClientParameters is used as input to the
* WorkloadIdentityFederationClient.
*/
interface WorkloadIdentityClientOptions {
projectID?: string;
providerID: string;
serviceAccount: string;
token: string;
audience: string;
oidcTokenRequestURL: string;
oidcTokenRequestToken: string;
export interface WorkloadIdentityFederationClientParameters {
readonly githubOIDCToken: string;
readonly githubOIDCTokenRequestURL: string;
readonly githubOIDCTokenRequestToken: string;
readonly githubOIDCTokenAudience: string;
readonly workloadIdentityProviderName: string;
readonly audience?: string;
readonly serviceAccount?: string;
}
/**
* WorkloadIdentityClient is a client that uses the GitHub Actions runtime to
* authentication via Workload Identity.
* WorkloadIdentityFederationClient is an authentication client that configures
* a Workload Identity authentication scheme.
*/
export class WorkloadIdentityClient extends BaseClient implements AuthClient {
readonly #projectID: string;
readonly #providerID: string;
readonly #serviceAccount: string;
readonly #token: string;
export class WorkloadIdentityFederationClient implements AuthClient {
readonly #logger: Logger;
readonly #httpClient: HttpClient;
readonly #githubOIDCToken: string;
readonly #githubOIDCTokenRequestURL: string;
readonly #githubOIDCTokenRequestToken: string;
readonly #githubOIDCTokenAudience: string;
readonly #workloadIdentityProviderName: string;
readonly #serviceAccount?: string;
#cachedToken?: string;
#cachedAt?: number;
readonly #universe: string = 'googleapis.com';
readonly #endpoints = {
iam: 'https://iam.{universe}/v1',
iamcredentials: 'https://iamcredentials.{universe}/v1',
sts: 'https://sts.{universe}/v1',
www: 'https://www.{universe}',
};
readonly #audience: string;
readonly #oidcTokenRequestURL: string;
readonly #oidcTokenRequestToken: string;
constructor(logger: Logger, opts: WorkloadIdentityFederationClientParameters) {
this.#logger = logger.withNamespace(this.constructor.name);
this.#httpClient = new HttpClient(userAgent);
constructor(opts: WorkloadIdentityClientOptions) {
super();
this.#providerID = opts.providerID;
this.#githubOIDCToken = opts.githubOIDCToken;
this.#githubOIDCTokenRequestURL = opts.githubOIDCTokenRequestURL;
this.#githubOIDCTokenRequestToken = opts.githubOIDCTokenRequestToken;
this.#githubOIDCTokenAudience = opts.githubOIDCTokenAudience;
this.#workloadIdentityProviderName = opts.workloadIdentityProviderName;
this.#serviceAccount = opts.serviceAccount;
this.#token = opts.token;
this.#audience = opts.audience;
this.#oidcTokenRequestURL = opts.oidcTokenRequestURL;
this.#oidcTokenRequestToken = opts.oidcTokenRequestToken;
const endpoints = this.#endpoints;
for (const key of Object.keys(this.#endpoints) as Array<keyof typeof endpoints>) {
this.#endpoints[key] = expandEndpoint(this.#endpoints[key], this.#universe);
}
this.#logger.debug(`Computed endpoints`, this.#endpoints);
this.#projectID =
opts.projectID || this.extractProjectIDFromServiceAccountEmail(this.#serviceAccount);
const iamHost = new URL(this.#endpoints.iam).host;
this.#audience = `//${iamHost}/${this.#workloadIdentityProviderName}`;
this.#logger.debug(`Computed audience`, this.#audience);
}
/**
* extractProjectIDFromServiceAccountEmail extracts the project ID from the
* service account email address.
* getToken gets a Google Cloud Federated Token that can call other Google
* Cloud APIs directly or impersonate an existing Service Account. Direct
* Workload Identity Federation will use the Federated Token directly.
* Workload Identity Federation through a Service Account will use
* impersonation.
*/
extractProjectIDFromServiceAccountEmail(str: string): string {
if (!str) {
return '';
async getToken(): Promise<string> {
const now = new Date().getTime();
if (this.#cachedToken && this.#cachedAt && now - this.#cachedAt > 60_000) {
this.#logger.debug(`Using cached token`);
return this.#cachedToken;
}
const [, dn] = str.split('@', 2);
if (!str.endsWith('.iam.gserviceaccount.com')) {
throw new Error(
`Service account email ${str} is not of the form ` +
`"[name]@[project].iam.gserviceaccount.com. You must manually ` +
`specify the "project_id" parameter in your GitHub Actions workflow.`,
);
}
const pth = `${this.#endpoints.sts}/token`;
const [project] = dn.split('.', 2);
return project;
}
/**
* getAuthToken generates a Google Cloud federated token using the provided
* OIDC token and Workload Identity Provider.
*/
async getAuthToken(): Promise<string> {
const pth = `https://sts.googleapis.com/v1/token`;
const data = {
audience: '//iam.googleapis.com/' + this.#providerID,
grantType: 'urn:ietf:params:oauth:grant-type:token-exchange',
requestedTokenType: 'urn:ietf:params:oauth:token-type:access_token',
scope: 'https://www.googleapis.com/auth/cloud-platform',
subjectTokenType: 'urn:ietf:params:oauth:token-type:jwt',
subjectToken: this.#token,
const body = {
audience: this.#audience,
grantType: `urn:ietf:params:oauth:grant-type:token-exchange`,
requestedTokenType: `urn:ietf:params:oauth:token-type:access_token`,
scope: `${this.#endpoints.www}/auth/cloud-platform`,
subjectTokenType: `urn:ietf:params:oauth:token-type:jwt`,
subjectToken: this.#githubOIDCToken,
};
const headers = {
'Accept': 'application/json',
'Content-Type': 'application/json',
};
this.#logger.withNamespace('getToken').debug({
method: `POST`,
path: pth,
body: body,
});
try {
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
const resp = await this.#httpClient.postJson<{ access_token: string }>(pth, body);
const statusCode = resp.statusCode || 500;
if (statusCode < 200 || statusCode > 299) {
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
}
const parsed = JSON.parse(body);
return parsed['access_token'];
const result = resp.result;
if (!result) {
throw new Error(`Successfully called ${pth}, but the result was empty`);
}
this.#cachedToken = result.access_token;
this.#cachedAt = now;
return result.access_token;
} catch (err) {
throw new Error(
`Failed to generate Google Cloud federated token for ${this.#providerID}: ${err}`,
`Failed to generate Google Cloud federated token for ${this.#audience}: ${err}`,
);
}
}
/**
* signJWT signs the given JWT using the IAM credentials endpoint.
*
* @param unsignedJWT The JWT to sign.
* @param delegates List of service account email address to use for
* impersonation in the delegation chain to sign the JWT.
* signJWT signs a JWT using the Service Account's private key.
*/
async signJWT(unsignedJWT: string, delegates?: Array<string>): Promise<string> {
const serviceAccount = await this.getServiceAccount();
const federatedToken = await this.getAuthToken();
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:signJwt`;
const data: Record<string, string | Array<string>> = {
payload: unsignedJWT,
};
if (delegates && delegates.length > 0) {
data.delegates = delegates;
async signJWT(claims: any): Promise<string> {
if (!this.#serviceAccount) {
throw new Error(`Cannot sign JWTs without specifying a service account`);
}
const pth = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:signJwt`;
const headers = {
'Accept': 'application/json',
'Authorization': `Bearer ${federatedToken}`,
'Content-Type': 'application/json',
Authorization: `Bearer ${this.getToken()}`,
};
const body = {
payload: claims,
};
this.#logger.withNamespace('signJWT').debug({
method: `POST`,
path: pth,
headers: headers,
body: body,
});
try {
const resp = await this.client.request('POST', pth, JSON.stringify(data), headers);
const body = await resp.readBody();
const statusCode = resp.message.statusCode || 500;
if (statusCode >= 400) {
throw new Error(`(${statusCode}) ${body}`);
const resp = await this.#httpClient.postJson<{ signedJwt: string }>(pth, body, headers);
const statusCode = resp.statusCode || 500;
if (statusCode < 200 || statusCode > 299) {
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${resp.result || '[no body]'}`);
}
const parsed = JSON.parse(body);
return parsed['signedJwt'];
const result = resp.result;
if (!result) {
throw new Error(`Successfully called ${pth}, but the result was empty`);
}
return result.signedJwt;
} catch (err) {
throw new Error(`Failed to sign JWT using ${serviceAccount}: ${err}`);
throw new Error(`Failed to sign JWT using ${this.#serviceAccount}: ${err}`);
}
}
/**
* getProjectID returns the project ID. If an override was given, the override
* is returned. Otherwise, this will be the project ID that was extracted from
* the service account key JSON.
*/
async getProjectID(): Promise<string> {
return this.#projectID;
}
/**
* getServiceAccount returns the service account email for the authentication,
* extracted from the input parameter.
*/
async getServiceAccount(): Promise<string> {
return this.#serviceAccount;
}
/**
* createCredentialsFile creates a Google Cloud credentials file that can be
* set as GOOGLE_APPLICATION_CREDENTIALS for gcloud and client libraries.
* createCredentialsFile writes a Workload Identity Federation credential file
* to disk at the specific outputPath.
*/
async createCredentialsFile(outputPath: string): Promise<string> {
const requestURL = new URL(this.#oidcTokenRequestURL);
const requestURL = new URL(this.#githubOIDCTokenRequestURL);
// Append the audience value to the request.
const params = requestURL.searchParams;
params.set('audience', this.#audience);
params.set('audience', this.#githubOIDCTokenAudience);
requestURL.search = params.toString();
const data = {
type: 'external_account',
audience: `//iam.googleapis.com/${this.#providerID}`,
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
token_url: 'https://sts.googleapis.com/v1/token',
service_account_impersonation_url: `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${this.#serviceAccount}:generateAccessToken`,
const data: Record<string, any> = {
type: `external_account`,
audience: this.#audience,
subject_token_type: `urn:ietf:params:oauth:token-type:jwt`,
token_url: `${this.#endpoints.sts}/token`,
credential_source: {
url: requestURL,
headers: {
Authorization: `Bearer ${this.#oidcTokenRequestToken}`,
Authorization: `Bearer ${this.#githubOIDCTokenRequestToken}`,
},
format: {
type: 'json',
subject_token_field_name: 'value',
type: `json`,
subject_token_field_name: `value`,
},
},
};
// Only request impersonation if a service account was given, otherwise use
// the WIF identity directly.
if (this.#serviceAccount) {
data.service_account_impersonation_url = `${this.#endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:generateAccessToken`;
}
this.#logger.withNamespace('createCredentialsFile').debug({ outputPath: outputPath });
return await writeSecureFile(outputPath, JSON.stringify(data));
}
}

122
src/logger.ts Normal file
View File

@ -0,0 +1,122 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import {
AnnotationProperties,
debug as logDebug,
error as logError,
info as logInfo,
notice as logNotice,
warning as logWarning,
} from '@actions/core';
/**
* LoggerFunction is the type signature of a log function for the GitHub Actions
* SDK.
*/
type LoggerFunction = (message: string, properties?: AnnotationProperties) => void;
/**
* Logger is a class that handles namespaced logging.
*/
export class Logger {
readonly #namespace?: string;
constructor(namespace?: string) {
this.#namespace = namespace;
}
withNamespace(namespace: string): Logger {
const { constructor } = Object.getPrototypeOf(this);
if (this.#namespace) {
return new constructor(`${this.#namespace}.${namespace}`);
}
return new constructor(namespace);
}
debug(...args: any[]) {
this.logMessage(logDebug, ...args);
}
error(...args: any[]) {
this.logMessage(logError, ...args);
}
info(...args: any[]) {
this.logMessage(logInfo, ...args);
}
notice(...args: any[]) {
this.logMessage(logNotice, ...args);
}
warning(...args: any[]) {
this.logMessage(logWarning, ...args);
}
protected logMessage(loggerFn: LoggerFunction, ...args: object[]) {
if (!args || args.length === 0) {
return;
}
let message = '';
if (this.#namespace) {
message += this.#namespace + ': ';
}
for (let i = 0; i < args.length; i++) {
const obj = args[i];
if (typeof obj === 'undefined' || obj === undefined || obj === null) {
continue;
}
if (typeof obj === 'string' || obj instanceof String) {
message += obj;
} else {
message += JSON.stringify(obj, null, 2);
}
if (i < args.length - 1) {
message += ', ';
}
}
loggerFn(message);
}
}
/**
* NullLogger is a logger that doesn't actually emit any output.
*/
export class NullLogger extends Logger {
debug(...args: any[]) {
this.logMessage(() => {}, ...args);
}
error(...args: any[]) {
this.logMessage(() => {}, ...args);
}
info(...args: any[]) {
this.logMessage(() => {}, ...args);
}
notice(...args: any[]) {
this.logMessage(() => {}, ...args);
}
warning(...args: any[]) {
this.logMessage(() => {}, ...args);
}
}

View File

@ -15,16 +15,13 @@
import { join as pathjoin } from 'path';
import {
debug as logDebug,
exportVariable,
getBooleanInput,
getIDToken,
getInput,
info as logInfo,
setFailed,
setOutput,
setSecret,
warning as logWarning,
} from '@actions/core';
import {
errorMessage,
@ -37,10 +34,19 @@ import {
withRetries,
} from '@google-github-actions/actions-utils';
import { WorkloadIdentityClient } from './client/workload_identity_client';
import { CredentialsJSONClient } from './client/credentials_json_client';
import { AuthClient } from './client/auth_client';
import { buildDomainWideDelegationJWT, generateCredentialsFilename } from './utils';
import {
AuthClient,
IAMCredentialsClient,
ServiceAccountKeyClient,
WorkloadIdentityFederationClient,
} from './base';
import { Logger } from './logger';
import {
buildDomainWideDelegationJWT,
computeProjectID,
computeServiceAccountEmail,
generateCredentialsFilename,
} from './utils';
const secretsWarning =
`If you are specifying input values via GitHub secrets, ensure the secret ` +
@ -57,9 +63,11 @@ const oidcWarning =
* Executes the main action.
*/
async function run(): Promise<void> {
const logger = new Logger();
// Warn if pinned to HEAD
if (isPinnedToHead()) {
logWarning(pinnedToHeadWarning('v1'));
logger.warning(pinnedToHeadWarning('v2'));
}
const retries = Number(getInput('retries'));
@ -69,7 +77,7 @@ async function run(): Promise<void> {
const backoffLimit = Number(getInput('backoff_limit')) || undefined;
try {
const mainWithRetries = withRetries(main, {
const mainWithRetries = withRetries(async () => main(logger), {
retries: retries,
backoff: backoff,
backoffLimit: backoffLimit,
@ -85,17 +93,25 @@ async function run(): Promise<void> {
/**
* Main wraps the main action logic into a function to be used as a parameter to the withRetries function.
*/
async function main() {
async function main(logger: Logger) {
// Load configuration.
const projectID = getInput('project_id');
const workloadIdentityProvider = getInput('workload_identity_provider');
const serviceAccount = getInput('service_account');
const audience = getInput('audience') || `https://iam.googleapis.com/${workloadIdentityProvider}`;
const credentialsJSON = getInput('credentials_json');
const createCredentialsFile = getBooleanInput('create_credentials_file');
const exportEnvironmentVariables = getBooleanInput('export_environment_variables');
const tokenFormat = getInput('token_format');
const delegates = parseCSV(getInput('delegates'));
const projectID = computeProjectID(
getInput(`project_id`),
getInput(`service_account`),
getInput(`credentials_json`),
);
const workloadIdentityProvider = getInput(`workload_identity_provider`);
const serviceAccount = computeServiceAccountEmail(
getInput(`service_account`),
getInput('credentials_json'),
);
const oidcTokenAudience =
getInput(`audience`) || `https://iam.googleapis.com/${workloadIdentityProvider}`;
const credentialsJSON = getInput(`credentials_json`);
const createCredentialsFile = getBooleanInput(`create_credentials_file`);
const exportEnvironmentVariables = getBooleanInput(`export_environment_variables`);
const tokenFormat = getInput(`token_format`);
const delegates = parseCSV(getInput(`delegates`));
// Ensure exactly one of workload_identity_provider and credentials_json was
// provided.
@ -107,19 +123,10 @@ async function main() {
);
}
// Ensure a service_account was provided if using WIF.
if (workloadIdentityProvider && !serviceAccount) {
throw new Error(
'The GitHub Action workflow must specify a "service_account" to ' +
'impersonate when using "workload_identity_provider"! ' +
secretsWarning,
);
}
// Instantiate the correct client based on the provided input parameters.
let client: AuthClient;
if (workloadIdentityProvider) {
logDebug(`Using workload identity provider "${workloadIdentityProvider}"`);
logger.debug(`Using workload identity provider "${workloadIdentityProvider}"`);
// If we're going to do the OIDC dance, we need to make sure these values
// are set. If they aren't, core.getIDToken() will fail and so will
@ -130,21 +137,19 @@ async function main() {
throw new Error(oidcWarning);
}
const token = await getIDToken(audience);
client = new WorkloadIdentityClient({
projectID: projectID,
providerID: workloadIdentityProvider,
const oidcToken = await getIDToken(oidcTokenAudience);
client = new WorkloadIdentityFederationClient(logger, {
githubOIDCToken: oidcToken,
githubOIDCTokenRequestURL: oidcTokenRequestURL,
githubOIDCTokenRequestToken: oidcTokenRequestToken,
githubOIDCTokenAudience: oidcTokenAudience,
workloadIdentityProviderName: workloadIdentityProvider,
serviceAccount: serviceAccount,
token: token,
audience: audience,
oidcTokenRequestToken: oidcTokenRequestToken,
oidcTokenRequestURL: oidcTokenRequestURL,
});
} else {
logDebug(`Using credentials JSON`);
client = new CredentialsJSONClient({
projectID: projectID,
credentialsJSON: credentialsJSON,
logger.debug(`Using credentials JSON`);
client = new ServiceAccountKeyClient(logger, {
serviceAccountKey: credentialsJSON,
});
}
@ -153,7 +158,7 @@ async function main() {
// fails, which means continue-on-error actions will still have the file
// available.
if (createCredentialsFile) {
logDebug(`Creating credentials file`);
logger.debug(`Creating credentials file`);
// Note: We explicitly and intentionally export to GITHUB_WORKSPACE
// instead of RUNNER_TEMP, because RUNNER_TEMP is not shared with
@ -180,7 +185,7 @@ async function main() {
// repository.
const githubWorkspaceIsEmpty = await isEmptyDir(githubWorkspace);
if (githubWorkspaceIsEmpty) {
logWarning(
logger.warning(
`The "create_credentials_file" option is true, but the current ` +
`GitHub workspace is empty. Did you forget to use ` +
`"actions/checkout" before this step? If you do not intend to ` +
@ -193,7 +198,7 @@ async function main() {
const outputFile = generateCredentialsFilename();
const outputPath = pathjoin(githubWorkspace, outputFile);
const credentialsPath = await client.createCredentialsFile(outputPath);
logInfo(`Created credentials file at "${credentialsPath}"`);
logger.info(`Created credentials file at "${credentialsPath}"`);
// Output to be available to future steps.
setOutput('credentials_file_path', credentialsPath);
@ -202,28 +207,47 @@ async function main() {
// CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE is picked up by gcloud to
// use a specific credential file (subject to change and equivalent to
// auth/credential_file_override).
exportVariableAndWarn('CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE', credentialsPath);
exportVariable('CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE', credentialsPath);
// GOOGLE_APPLICATION_CREDENTIALS is used by Application Default
// Credentials in all GCP client libraries.
exportVariableAndWarn('GOOGLE_APPLICATION_CREDENTIALS', credentialsPath);
exportVariable('GOOGLE_APPLICATION_CREDENTIALS', credentialsPath);
// GOOGLE_GHA_CREDS_PATH is used by other Google GitHub Actions.
exportVariableAndWarn('GOOGLE_GHA_CREDS_PATH', credentialsPath);
exportVariable('GOOGLE_GHA_CREDS_PATH', credentialsPath);
}
}
// Set the project ID environment variables to the computed values.
const computedProjectID = await client.getProjectID();
setOutput('project_id', computedProjectID);
if (!projectID) {
logger.warning(
`Unable to compute project ID from inputs, skipping export. Please ` +
`specify the "project_id" input directly.`,
);
} else {
setOutput('project_id', projectID);
if (exportEnvironmentVariables) {
exportVariableAndWarn('CLOUDSDK_CORE_PROJECT', computedProjectID);
exportVariableAndWarn('CLOUDSDK_PROJECT', computedProjectID);
exportVariableAndWarn('GCLOUD_PROJECT', computedProjectID);
exportVariableAndWarn('GCP_PROJECT', computedProjectID);
exportVariableAndWarn('GOOGLE_CLOUD_PROJECT', computedProjectID);
exportVariable('CLOUDSDK_CORE_PROJECT', projectID);
exportVariable('CLOUDSDK_PROJECT', projectID);
exportVariable('GCLOUD_PROJECT', projectID);
exportVariable('GCP_PROJECT', projectID);
exportVariable('GOOGLE_CLOUD_PROJECT', projectID);
}
}
// Attempt to generate a token. This will ensure the action correctly errors
// if the credentials are misconfigured. This is also required so the value
// can be set as an output for future authentication calls.
const authToken = await client.getToken();
logger.debug(`Successfully generated auth token`);
setSecret(authToken);
setOutput('auth_token', authToken);
// Create the credential client, we might not use it, but it's basically free.
const iamCredentialsClient = new IAMCredentialsClient(logger, {
authToken: authToken,
});
switch (tokenFormat) {
case '': {
@ -233,20 +257,28 @@ async function main() {
break;
}
case 'access_token': {
logDebug(`Creating access token`);
logger.debug(`Creating access token`);
const accessTokenLifetime = parseDuration(getInput('access_token_lifetime'));
const accessTokenScopes = parseCSV(getInput('access_token_scopes'));
const accessTokenSubject = getInput('access_token_subject');
const serviceAccount = await client.getServiceAccount();
// Ensure a service_account was provided if using WIF.
if (!serviceAccount) {
throw new Error(
'The GitHub Action workflow must specify a "service_account" to ' +
'use when generating an OAuth 2.0 Access Token. ' +
secretsWarning,
);
}
// If a subject was provided, use the traditional OAuth 2.0 flow to
// perform Domain-Wide Delegation. Otherwise, use the modern IAM
// Credentials endpoints.
let accessToken, expiration;
let accessToken;
if (accessTokenSubject) {
if (accessTokenLifetime > 3600) {
logInfo(
logger.info(
`An access token subject was specified, triggering Domain-Wide ` +
`Delegation flow. This flow does not support specifying an ` +
`access token lifetime of greater than 1 hour.`,
@ -259,39 +291,45 @@ async function main() {
accessTokenScopes,
accessTokenLifetime,
);
const signedJWT = await client.signJWT(unsignedJWT, delegates);
({ accessToken, expiration } = await client.googleOAuthToken(signedJWT));
const signedJWT = await client.signJWT(unsignedJWT);
accessToken = await iamCredentialsClient.generateDomainWideDelegationAccessToken(signedJWT);
} else {
const authToken = await client.getAuthToken();
({ accessToken, expiration } = await client.googleAccessToken(authToken, {
accessToken = await iamCredentialsClient.generateAccessToken({
serviceAccount,
delegates,
scopes: accessTokenScopes,
lifetime: accessTokenLifetime,
}));
});
}
setSecret(accessToken);
setOutput('access_token', accessToken);
setOutput('access_token_expiration', expiration);
break;
}
case 'id_token': {
logDebug(`Creating id token`);
logger.debug(`Creating id token`);
const idTokenAudience = getInput('id_token_audience', { required: true });
const idTokenIncludeEmail = getBooleanInput('id_token_include_email');
const serviceAccount = await client.getServiceAccount();
const authToken = await client.getAuthToken();
const { token } = await client.googleIDToken(authToken, {
// Ensure a service_account was provided if using WIF.
if (!serviceAccount) {
throw new Error(
'The GitHub Action workflow must specify a "service_account" to ' +
'use when generating an OAuth 2.0 Access Token. ' +
secretsWarning,
);
}
const idToken = await iamCredentialsClient.generateIDToken({
serviceAccount,
audience: idTokenAudience,
delegates,
includeEmail: idTokenIncludeEmail,
});
setSecret(token);
setOutput('id_token', token);
setSecret(idToken);
setOutput('id_token', idToken);
break;
}
default: {
@ -300,26 +338,4 @@ async function main() {
}
}
/**
* exportVariableAndWarn exports the given key as an environment variable set to
* the provided value. If a value already exists, it is overwritten and an
* warning is emitted.
*
* @param key Environment variable key.
* @param value Environment variable value.
*/
function exportVariableAndWarn(key: string, value: string) {
const existing = process.env[key];
if (existing && existing !== value) {
logWarning(
`Overwriting existing environment variable ${key}:
- ${JSON.stringify(existing)}
+ ${JSON.stringify(value)}
`.trim(),
);
}
exportVariable(key, value);
}
run();

View File

@ -12,23 +12,36 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { getBooleanInput, setFailed, info as logInfo } from '@actions/core';
import { getBooleanInput, setFailed } from '@actions/core';
import { errorMessage, forceRemove } from '@google-github-actions/actions-utils';
import { Logger } from './logger';
/**
* Executes the post action, documented inline.
*/
export async function run(): Promise<void> {
export async function run() {
const logger = new Logger();
try {
main(logger);
} catch (err) {
const msg = errorMessage(err);
setFailed(`google-github-actions/auth post failed with: ${msg}`);
}
}
async function main(logger: Logger) {
const createCredentials = getBooleanInput('create_credentials_file');
if (!createCredentials) {
logInfo(`Skipping credential cleanup - "create_credentials_file" is false.`);
logger.info(`Skipping credential cleanup - "create_credentials_file" is false.`);
return;
}
const cleanupCredentials = getBooleanInput('cleanup_credentials');
if (!cleanupCredentials) {
logInfo(`Skipping credential cleanup - "cleanup_credentials" is false.`);
logger.info(`Skipping credential cleanup - "cleanup_credentials" is false.`);
return;
}
@ -38,17 +51,13 @@ export async function run(): Promise<void> {
// another environment variable manually.
const credentialsPath = process.env['GOOGLE_GHA_CREDS_PATH'];
if (!credentialsPath) {
logInfo(`Skipping credential cleanup - $GOOGLE_GHA_CREDS_PATH is not set.`);
logger.info(`Skipping credential cleanup - $GOOGLE_GHA_CREDS_PATH is not set.`);
return;
}
// Remove the file.
await forceRemove(credentialsPath);
logInfo(`Removed exported credentials at "${credentialsPath}".`);
} catch (err) {
const msg = errorMessage(err);
setFailed(`google-github-actions/auth post failed with: ${msg}`);
}
logger.info(`Removed exported credentials at "${credentialsPath}".`);
}
run();

View File

@ -12,7 +12,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { randomFilename } from '@google-github-actions/actions-utils';
import {
isServiceAccountKey,
parseCredential,
randomFilename,
} from '@google-github-actions/actions-utils';
// Do not listen to the linter - this can NOT be rewritten as an ES6 import statement.
// eslint-disable-next-line @typescript-eslint/no-var-requires
export const { version: appVersion } = require('../package.json');
// userAgent is the default user agent.
export const userAgent = `google-github-actions:auth/${appVersion}`;
/**
* buildDomainWideDelegationJWT constructs an _unsigned_ JWT to be used for a
@ -50,6 +61,86 @@ export function buildDomainWideDelegationJWT(
return JSON.stringify(body);
}
/**
* computeProjectID attempts to compute the best project ID from the given
* inputs.
*/
export function computeProjectID(
projectID?: string,
serviceAccount?: string,
serviceAccountKeyJSON?: string,
): string | undefined {
if (projectID) {
return projectID;
}
// sa-name@<project-id>.iam.gserviceaccount.com
const fromEmail = projectIDFromServiceAccountEmail(serviceAccount);
if (fromEmail) {
return fromEmail;
}
// Extract from the key
if (serviceAccountKeyJSON) {
const credential = parseCredential(serviceAccountKeyJSON);
if (isServiceAccountKey(credential) && credential.project_id) {
return credential.project_id;
}
}
return undefined;
}
/**
* getServiceAccountEmail extracts the service account email from the given
* fields.
*/
export function computeServiceAccountEmail(
serviceAccountEmail?: string,
serviceAccountKeyJSON?: string,
): string | undefined {
if (serviceAccountEmail) {
return serviceAccountEmail;
}
if (serviceAccountKeyJSON) {
const credential = parseCredential(serviceAccountKeyJSON);
if (isServiceAccountKey(credential) && credential.client_email) {
return credential.client_email;
}
}
return undefined;
}
/**
* projectIDFromServiceAccountEmail attempts to extract the project ID from the
* service account email.
*/
export function projectIDFromServiceAccountEmail(serviceAccount?: string): string | null {
if (!serviceAccount) {
return null;
}
const emailParts = serviceAccount.split('@');
if (emailParts.length !== 2) {
return null;
}
const addressParts = emailParts[1].split('.');
if (addressParts.length < 2) {
return null;
}
return addressParts[0];
}
/**
* expandEndpoint expands the input url relative to the universe.
*/
export function expandEndpoint(input: string, universe: string): string {
return (input || '').replace(/{universe}/g, universe).replace(/\/+$/, '');
}
/**
* generateCredentialsFilename creates a predictable filename under which
* credentials are written. This string is the filename, not the filepath. It must match the format:

View File

@ -21,7 +21,8 @@ import { tmpdir } from 'os';
import { randomFilename } from '@google-github-actions/actions-utils';
import { CredentialsJSONClient } from '../../src/client/credentials_json_client';
import { NullLogger } from '../../src/logger';
import { ServiceAccountKeyClient } from '../../src/client/credentials_json_client';
// Yes, this is a real private key. No, it's not valid for authenticating
// Google Cloud.
@ -40,40 +41,40 @@ const credentialsJSON = `
}
`;
describe('CredentialsJSONClient', () => {
describe('ServiceAccountKeyClient', () => {
describe('#parseServiceAccountKeyJSON', () => {
it('throws exception on invalid json', async () => {
assert.rejects(async () => {
new CredentialsJSONClient({
credentialsJSON: 'invalid json',
new ServiceAccountKeyClient(new NullLogger(), {
serviceAccountKey: 'invalid json',
});
}, SyntaxError);
});
it('handles base64', async () => {
assert.rejects(async () => {
new CredentialsJSONClient({
credentialsJSON: 'base64',
new ServiceAccountKeyClient(new NullLogger(), {
serviceAccountKey: 'base64',
});
}, SyntaxError);
});
});
describe('#getAuthToken', () => {
it('signs a jwt', async () => {
const client = new CredentialsJSONClient({
credentialsJSON: credentialsJSON,
describe('#getToken', () => {
it('gets a token', async () => {
const client = new ServiceAccountKeyClient(new NullLogger(), {
serviceAccountKey: credentialsJSON,
});
const token = await client.getAuthToken();
const token = await client.getToken();
assert.ok(token);
});
});
describe('#signJWT', () => {
it('signs a jwt', async () => {
const client = new CredentialsJSONClient({
credentialsJSON: credentialsJSON,
const client = new ServiceAccountKeyClient(new NullLogger(), {
serviceAccountKey: credentialsJSON,
});
const token = await client.signJWT('thisismy.jwt');
@ -81,43 +82,11 @@ describe('CredentialsJSONClient', () => {
});
});
describe('#getProjectID', () => {
it('extracts project ID from the json', async () => {
const client = new CredentialsJSONClient({
credentialsJSON: credentialsJSON,
});
const result = await client.getProjectID();
assert.deepStrictEqual(result, 'my-project');
});
it('prefers the override if given', async () => {
const client = new CredentialsJSONClient({
projectID: 'my-other-project',
credentialsJSON: credentialsJSON,
});
const result = await client.getProjectID();
assert.deepStrictEqual(result, 'my-other-project');
});
});
describe('#getServiceAccount', () => {
it('extracts service account from the json', async () => {
const client = new CredentialsJSONClient({
credentialsJSON: credentialsJSON,
});
const result = await client.getServiceAccount();
assert.deepStrictEqual(result, 'my-service-account@my-project.iam.gserviceaccount.com');
});
});
describe('#createCredentialsFile', () => {
it('writes the file', async () => {
const outputFile = pathjoin(tmpdir(), randomFilename());
const client = new CredentialsJSONClient({
credentialsJSON: credentialsJSON,
const client = new ServiceAccountKeyClient(new NullLogger(), {
serviceAccountKey: credentialsJSON,
});
const exp = JSON.parse(credentialsJSON);

View File

@ -21,80 +21,54 @@ import { readFileSync } from 'fs';
import { randomFilename } from '@google-github-actions/actions-utils';
import { WorkloadIdentityClient } from '../../src/client/workload_identity_client';
describe('WorkloadIdentityClient', () => {
describe('#getProjectID', () => {
it('extracts project ID from the service account email', async () => {
const client = new WorkloadIdentityClient({
providerID: 'my-provider',
token: 'my-token',
serviceAccount: 'my-service@my-project.iam.gserviceaccount.com',
audience: 'my-aud',
oidcTokenRequestURL: 'https://example.com/',
oidcTokenRequestToken: 'token',
});
const result = await client.getProjectID();
assert.deepStrictEqual(result, 'my-project');
});
it('prefers the override if given', async () => {
const client = new WorkloadIdentityClient({
projectID: 'my-other-project',
providerID: 'my-provider',
token: 'my-token',
serviceAccount: 'my-service@my-project.iam.gserviceaccount.com',
audience: 'my-aud',
oidcTokenRequestURL: 'https://example.com/',
oidcTokenRequestToken: 'token',
});
const result = await client.getProjectID();
assert.deepStrictEqual(result, 'my-other-project');
});
it('throws an error when extraction fails', async () => {
assert.rejects(async () => {
return new WorkloadIdentityClient({
providerID: 'my-provider',
token: 'my-token',
serviceAccount: 'my-service@developers.google.com',
audience: 'my-aud',
oidcTokenRequestURL: 'https://example.com/',
oidcTokenRequestToken: 'token',
});
}, Error);
});
});
describe('#getServiceAccount', () => {
it('returns the provided value', async () => {
const client = new WorkloadIdentityClient({
projectID: 'my-project',
providerID: 'my-provider',
serviceAccount: 'my-service@my-project.iam.gserviceaccount.com',
token: 'my-token',
audience: 'my-aud',
oidcTokenRequestURL: 'https://example.com/',
oidcTokenRequestToken: 'token',
});
const result = await client.getServiceAccount();
assert.deepStrictEqual(result, 'my-service@my-project.iam.gserviceaccount.com');
});
});
import { NullLogger } from '../../src/logger';
import { WorkloadIdentityFederationClient } from '../../src/client/workload_identity_client';
describe('WorkloadIdentityFederationClient', () => {
describe('#createCredentialsFile', () => {
it('writes the file', async () => {
const outputFile = pathjoin(tmpdir(), randomFilename());
const client = new WorkloadIdentityClient({
projectID: 'my-project',
providerID: 'my-provider',
const client = new WorkloadIdentityFederationClient(new NullLogger(), {
githubOIDCToken: 'my-token',
githubOIDCTokenRequestURL: 'https://example.com/',
githubOIDCTokenRequestToken: 'token',
githubOIDCTokenAudience: 'my-aud',
workloadIdentityProviderName: 'my-provider',
});
const exp = {
audience: '//iam.googleapis.com/my-provider',
credential_source: {
format: {
subject_token_field_name: 'value',
type: 'json',
},
headers: {
Authorization: 'Bearer token',
},
url: 'https://example.com/?audience=my-aud',
},
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
token_url: 'https://sts.googleapis.com/v1/token',
type: 'external_account',
};
const pth = await client.createCredentialsFile(outputFile);
const data = readFileSync(pth);
const got = JSON.parse(data.toString('utf8'));
assert.deepStrictEqual(got, exp);
});
it('writes the file with impersonation', async () => {
const outputFile = pathjoin(tmpdir(), randomFilename());
const client = new WorkloadIdentityFederationClient(new NullLogger(), {
githubOIDCToken: 'my-token',
githubOIDCTokenRequestURL: 'https://example.com/',
githubOIDCTokenRequestToken: 'token',
githubOIDCTokenAudience: 'my-aud',
workloadIdentityProviderName: 'my-provider',
serviceAccount: 'my-service@my-project.iam.gserviceaccount.com',
token: 'my-token',
audience: 'my-aud',
oidcTokenRequestURL: 'https://example.com/',
oidcTokenRequestToken: 'token',
});
const exp = {

View File

@ -15,10 +15,17 @@
import { describe, it } from 'node:test';
import assert from 'node:assert';
import { buildDomainWideDelegationJWT, generateCredentialsFilename } from '../src/utils';
import {
buildDomainWideDelegationJWT,
computeProjectID,
computeServiceAccountEmail,
expandEndpoint,
generateCredentialsFilename,
projectIDFromServiceAccountEmail,
} from '../src/utils';
describe('Utils', () => {
describe('#buildDomainWideDelegationJWT', () => {
describe('Utils', async () => {
describe('#buildDomainWideDelegationJWT', async () => {
const cases = [
{
name: 'default',
@ -57,7 +64,152 @@ describe('Utils', () => {
});
});
describe('#generateCredentialsFilename', () => {
describe('#computeProjectID', async () => {
const cases = [
{
name: 'directly given',
projectID: 'my-project',
exp: 'my-project',
},
{
name: 'from service account email',
serviceAccountEmail: 'my-account@my-project.iam.gserviceaccount.com',
exp: 'my-project',
},
{
name: 'from json credential',
serviceAccountKeyJSON: '{"type":"service_account", "project_id": "my-project"}',
exp: 'my-project',
},
{
name: 'from json credential invalid',
serviceAccountKeyJSON: '{"nope": "foo@bar.com"}',
exp: undefined,
},
];
cases.forEach(async (tc) => {
it(tc.name, async () => {
const result = computeProjectID(
tc.projectID,
tc.serviceAccountEmail,
tc.serviceAccountKeyJSON,
);
assert.deepStrictEqual(result, tc.exp);
});
});
});
describe('#computeServiceAccountEmail', async () => {
const cases = [
{
name: 'directly given',
serviceAccountEmail: 'foo@bar.com',
exp: 'foo@bar.com',
},
{
name: 'from json credential',
serviceAccountKeyJSON: '{"type":"service_account", "client_email": "foo@bar.com"}',
exp: 'foo@bar.com',
},
{
name: 'invalid json credential',
serviceAccountKeyJSON: '{"nope": "foo@bar.com"}',
exp: undefined,
},
{
name: 'nothing',
exp: undefined,
},
];
cases.forEach(async (tc) => {
it(tc.name, async () => {
const result = computeServiceAccountEmail(tc.serviceAccountEmail, tc.serviceAccountKeyJSON);
assert.deepStrictEqual(result, tc.exp);
});
});
});
describe('#projectIDFromServiceAccountEmail', async () => {
const cases = [
{
name: 'empty',
input: '',
exp: null,
},
{
name: 'not an email',
input: 'not a service account',
exp: null,
},
{
name: 'invalid email',
input: 'foo@abc',
exp: null,
},
{
name: 'returns project',
input: 'test-sa@my-project.iam.gserviceaccount.com',
exp: 'my-project',
},
];
cases.forEach(async (tc) => {
it(tc.name, async () => {
const result = projectIDFromServiceAccountEmail(tc.input);
assert.deepStrictEqual(result, tc.exp);
});
});
});
describe('#expandEndpoint', async () => {
const cases = [
{
name: 'empty',
endpoint: '',
universe: '',
exp: '',
},
{
name: 'no match',
endpoint: 'https://www.googleapis.com',
universe: 'foobar',
exp: 'https://www.googleapis.com',
},
{
name: 'removes trailing slash',
endpoint: 'https://www.googleapis.com/',
exp: 'https://www.googleapis.com',
},
{
name: 'removes trailing slashes',
endpoint: 'https://www.googleapis.com/////',
exp: 'https://www.googleapis.com',
},
{
name: 'replaces {universe}',
endpoint: 'https://www.{universe}',
universe: 'foo.bar',
exp: 'https://www.foo.bar',
},
{
name: 'replaces multiple {universe}',
endpoint: 'https://www.{universe}.{universe}',
universe: 'foo.bar',
exp: 'https://www.foo.bar.foo.bar',
},
];
cases.forEach(async (tc) => {
it(tc.name, async () => {
const result = expandEndpoint(tc.endpoint, tc.universe || '');
assert.deepStrictEqual(result, tc.exp);
});
});
});
describe('#generateCredentialsFilename', async () => {
it('returns a string matching the regex', () => {
for (let i = 0; i < 10; i++) {
const filename = generateCredentialsFilename();