mirror of
https://github.com/google-github-actions/auth.git
synced 2026-03-13 18:17:12 -04:00
Compare commits
104 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc2174804b | ||
|
|
0dfce0c0f8 | ||
|
|
7c6bc770da | ||
|
|
42e4997ee3 | ||
|
|
5ea4dc1147 | ||
|
|
c200f3691d | ||
|
|
3a53be7e7c | ||
|
|
b7593ed2ef | ||
|
|
c1ee334b4f | ||
|
|
140bb5113f | ||
|
|
ab3132e2ad | ||
|
|
25b96bac99 | ||
|
|
0920706a19 | ||
|
|
ba79af0395 | ||
|
|
bfaa66bd66 | ||
|
|
d0822ad9bf | ||
|
|
7b53cdc2a3 | ||
|
|
a9cfddf5d2 | ||
|
|
b011f3988e | ||
|
|
71f986410d | ||
|
|
0cd8f2e4e2 | ||
|
|
332e0ba72f | ||
|
|
28d44ba259 | ||
|
|
83354cacbb | ||
|
|
6fc4af4b14 | ||
|
|
212f83afe8 | ||
|
|
8254fb75a3 | ||
|
|
d1b27fe5c4 | ||
|
|
c8788cc4c5 | ||
|
|
62cf5bd3e4 | ||
|
|
0a94a84ba5 | ||
|
|
699582eeaf | ||
|
|
6384b341b7 | ||
|
|
f112390a2d | ||
|
|
984b9cfee9 | ||
|
|
5e210ff4ed | ||
|
|
49ae1e804e | ||
|
|
71fee32a0b | ||
|
|
e0122d6a97 | ||
|
|
34baaec3f3 | ||
|
|
8d44d59719 | ||
|
|
d176447fc7 | ||
|
|
33e827c6cc | ||
|
|
55bd3a7c6e | ||
|
|
bf02f20c66 | ||
|
|
51342a1a91 | ||
|
|
ee1c1b641f | ||
|
|
ec485ac236 | ||
|
|
a6e2e39c0a | ||
|
|
b4f4057a10 | ||
|
|
39c96a3f1d | ||
|
|
5a50e58116 | ||
|
|
aaf2e69f95 | ||
|
|
f6de81663f | ||
|
|
a57dd04655 | ||
|
|
fb74905737 | ||
|
|
56562ddf6a | ||
|
|
5f7afaa53a | ||
|
|
82c1c583d7 | ||
|
|
5e5db2b28b | ||
|
|
b372ba41cf | ||
|
|
042a3056d6 | ||
|
|
68ea0b8bd6 | ||
|
|
67e9c72af6 | ||
|
|
0a2edc185b | ||
|
|
7c4e01fd00 | ||
|
|
097d292c04 | ||
|
|
fe9207673e | ||
|
|
f105ef0cdb | ||
|
|
cf96593e46 | ||
|
|
292d120a9f | ||
|
|
04900d1e97 | ||
|
|
43a59886fc | ||
|
|
e607103ba4 | ||
|
|
87b651ab4a | ||
|
|
8738ec4f61 | ||
|
|
ee81b78321 | ||
|
|
35b0e87d16 | ||
|
|
24b8285e35 | ||
|
|
286fc8e4ba | ||
|
|
e8df18b60c | ||
|
|
41d8fa4aaa | ||
|
|
14b54cc192 | ||
|
|
430ae13d31 | ||
|
|
f8751d9c29 | ||
|
|
7004cc6280 | ||
|
|
5431d4afba | ||
|
|
b0a71801d2 | ||
|
|
126054d1a9 | ||
|
|
483e21d723 | ||
|
|
1475a55569 | ||
|
|
d4421b6014 | ||
|
|
46affe9814 | ||
|
|
23620afd0f | ||
|
|
400b51b0c2 | ||
|
|
7c1d54bea7 | ||
|
|
abd9c242f6 | ||
|
|
dcb0567d1a | ||
|
|
ef5d53e30b | ||
|
|
d419231eb9 | ||
|
|
9cf496b575 | ||
|
|
44ae44c257 | ||
|
|
60569fa4c7 | ||
|
|
7e38fb10d3 |
11
.eslintrc.js
11
.eslintrc.js
@@ -1,11 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
parser: '@typescript-eslint/parser',
|
||||
plugins: ['@typescript-eslint'],
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:@typescript-eslint/eslint-recommended',
|
||||
'plugin:@typescript-eslint/recommended',
|
||||
'plugin:prettier/recommended',
|
||||
],
|
||||
};
|
||||
5
.github/actionlint.yml
vendored
Normal file
5
.github/actionlint.yml
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
paths:
|
||||
'**/*.yml':
|
||||
ignore:
|
||||
# https://github.com/rhysd/actionlint/issues/559
|
||||
- 'invalid runner name "node24"'
|
||||
36
.github/dependabot.yml
vendored
36
.github/dependabot.yml
vendored
@@ -1,20 +1,24 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
# 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.
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/"
|
||||
commit-message:
|
||||
prefix: "chore(deps): "
|
||||
rebase-strategy: "disabled"
|
||||
- package-ecosystem: 'npm'
|
||||
directory: '/'
|
||||
rebase-strategy: 'disabled'
|
||||
schedule:
|
||||
interval: "daily"
|
||||
ignore:
|
||||
- dependency-name: "*"
|
||||
update-types: [
|
||||
"version-update:semver-patch",
|
||||
"version-update:semver-minor",
|
||||
] # Security updates are unaffected by this setting
|
||||
interval: 'daily'
|
||||
commit-message:
|
||||
prefix: 'security: '
|
||||
open-pull-requests-limit: 0 # only check security updates
|
||||
|
||||
9
.github/workflows/draft-release.yml
vendored
9
.github/workflows/draft-release.yml
vendored
@@ -15,10 +15,11 @@ on:
|
||||
|
||||
jobs:
|
||||
draft-release:
|
||||
name: 'Draft release'
|
||||
uses: 'google-github-actions/.github/.github/workflows/draft-release.yml@v0'
|
||||
uses: 'google-github-actions/.github/.github/workflows/draft-release.yml@v3' # ratchet:exclude
|
||||
permissions:
|
||||
contents: 'read'
|
||||
pull-requests: 'write'
|
||||
with:
|
||||
version_strategy: '${{ github.event.inputs.version_strategy }}'
|
||||
# secrets must be explicitly passed to reusable workflows https://docs.github.com/en/enterprise-cloud@latest/actions/using-workflows/reusing-workflows#using-inputs-and-secrets-in-a-reusable-workflow
|
||||
secrets:
|
||||
ACTIONS_BOT_TOKEN: '${{ secrets.ACTIONS_BOT_TOKEN }}'
|
||||
ACTIONS_BOT_TOKEN: '${{ secrets.ACTIONS_BOT_TOKEN }}'
|
||||
|
||||
25
.github/workflows/publish.yml
vendored
Normal file
25
.github/workflows/publish.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: 'Publish immutable action version'
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types:
|
||||
- 'published'
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: 'ubuntu-latest'
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
packages: 'write'
|
||||
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
|
||||
|
||||
- name: 'Publish'
|
||||
id: 'publish'
|
||||
uses: 'actions/publish-immutable-action@4bc8754ffc40f27910afb20287dbbbb675a4e978' # ratchet:actions/publish-immutable-action@v0.0.4
|
||||
with:
|
||||
github-token: '${{ secrets.GITHUB_TOKEN }}'
|
||||
11
.github/workflows/release.yml
vendored
11
.github/workflows/release.yml
vendored
@@ -4,9 +4,14 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**/*'
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: "startsWith(github.event.head_commit.message, 'Release: v')"
|
||||
name: 'Release'
|
||||
uses: 'google-github-actions/.github/.github/workflows/release.yml@v0'
|
||||
uses: 'google-github-actions/.github/.github/workflows/release.yml@v3' # ratchet:exclude
|
||||
permissions:
|
||||
attestations: 'write'
|
||||
contents: 'write'
|
||||
packages: 'write'
|
||||
secrets:
|
||||
ACTIONS_BOT_TOKEN: '${{ secrets.ACTIONS_BOT_TOKEN }}'
|
||||
|
||||
295
.github/workflows/test.yml
vendored
295
.github/workflows/test.yml
vendored
@@ -1,115 +1,68 @@
|
||||
# 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.
|
||||
|
||||
name: 'Test'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**/*'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**/*'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
statuses: 'write'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
name: 'unit'
|
||||
runs-on: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- uses: 'actions/checkout@v3'
|
||||
- uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
|
||||
|
||||
- uses: 'actions/setup-node@v2'
|
||||
- uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: 'npm build'
|
||||
run: 'npm ci && npm run build'
|
||||
|
||||
- name: 'npm lint'
|
||||
run: 'npm run lint'
|
||||
|
||||
- name: 'npm test'
|
||||
run: 'npm run test'
|
||||
|
||||
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@v3'
|
||||
|
||||
- uses: 'actions/setup-node@v2'
|
||||
with:
|
||||
node-version: '16.x'
|
||||
|
||||
- name: 'npm build'
|
||||
run: 'npm ci && npm run build'
|
||||
|
||||
- id: 'auth-default'
|
||||
name: 'auth-default'
|
||||
uses: './'
|
||||
with:
|
||||
credentials_json: '${{ secrets.AUTH_SA_KEY_JSON }}'
|
||||
|
||||
- id: 'setup-gcloud'
|
||||
name: 'setup-gcloud'
|
||||
uses: 'google-github-actions/setup-gcloud@main'
|
||||
|
||||
- id: 'gcloud'
|
||||
name: 'gcloud'
|
||||
shell: 'bash'
|
||||
run: |-
|
||||
gcloud secrets versions access "latest" --secret "${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}"
|
||||
|
||||
- id: 'auth-access-token'
|
||||
name: 'auth-access-token'
|
||||
uses: './'
|
||||
with:
|
||||
credentials_json: '${{ secrets.AUTH_SA_KEY_B64 }}'
|
||||
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/${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}/versions/latest:access \
|
||||
--silent \
|
||||
--show-error \
|
||||
--fail \
|
||||
--header "Authorization: Bearer ${{ steps.auth-access-token.outputs.access_token }}"
|
||||
|
||||
- id: 'auth-id-token'
|
||||
name: 'auth-id-token'
|
||||
uses: './'
|
||||
with:
|
||||
credentials_json: '${{ secrets.AUTH_SA_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.AUTH_SA_KEY_JSON }}'
|
||||
|
||||
workload_identity_federation:
|
||||
if: ${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
|
||||
name: 'workload_identity_federation'
|
||||
#
|
||||
# Direct Workload Identity Federation
|
||||
#
|
||||
direct_workload_identity_federation:
|
||||
if: |-
|
||||
${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
|
||||
name: 'direct_workload_identity_federation'
|
||||
runs-on: '${{ matrix.os }}'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -123,11 +76,11 @@ jobs:
|
||||
id-token: 'write'
|
||||
|
||||
steps:
|
||||
- uses: 'actions/checkout@v3'
|
||||
- uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
|
||||
|
||||
- uses: 'actions/setup-node@v2'
|
||||
- uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: 'npm build'
|
||||
run: 'npm ci && npm run build'
|
||||
@@ -136,32 +89,150 @@ jobs:
|
||||
name: 'auth-default'
|
||||
uses: './'
|
||||
with:
|
||||
workload_identity_provider: '${{ secrets.WIF_PROVIDER_NAME }}'
|
||||
service_account: '${{ secrets.OIDC_AUTH_SA_EMAIL }}'
|
||||
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'
|
||||
|
||||
- id: 'gcloud'
|
||||
name: 'gcloud'
|
||||
shell: 'bash'
|
||||
- id: 'oauth-federated-token'
|
||||
name: 'oauth-federated-token'
|
||||
run: |-
|
||||
gcloud secrets versions access "latest" --secret "${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}"
|
||||
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-default.outputs.auth_token }}"
|
||||
|
||||
- uses: 'google-github-actions/setup-gcloud@main' # ratchet:exclude
|
||||
with:
|
||||
version: '>= 363.0.0'
|
||||
|
||||
- name: 'gcloud'
|
||||
run: |-
|
||||
gcloud secrets versions access "latest" --secret "${{ vars.SECRET_NAME }}"
|
||||
|
||||
|
||||
#
|
||||
# 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_through_service_account'
|
||||
runs-on: '${{ matrix.os }}'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os:
|
||||
- 'ubuntu-latest'
|
||||
- 'windows-latest'
|
||||
- 'macos-latest'
|
||||
|
||||
permissions:
|
||||
id-token: 'write'
|
||||
|
||||
steps:
|
||||
- uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
|
||||
|
||||
- uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: 'npm build'
|
||||
run: 'npm ci && npm run build'
|
||||
|
||||
- id: 'auth-default'
|
||||
name: 'auth-default'
|
||||
uses: './'
|
||||
with:
|
||||
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
|
||||
service_account: '${{ vars.SERVICE_ACCOUNT_EMAIL }}'
|
||||
|
||||
- uses: 'google-github-actions/setup-gcloud@main' # ratchet:exclude
|
||||
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:
|
||||
workload_identity_provider: '${{ secrets.WIF_PROVIDER_NAME }}'
|
||||
service_account: '${{ secrets.OIDC_AUTH_SA_EMAIL }}'
|
||||
workload_identity_provider: '${{ vars.WIF_PROVIDER_NAME }}'
|
||||
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@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
|
||||
|
||||
- uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- 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' # ratchet:exclude
|
||||
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/${{ secrets.OIDC_AUTH_TEST_SECRET_NAME }}/versions/latest:access \
|
||||
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 \
|
||||
@@ -171,38 +242,30 @@ jobs:
|
||||
name: 'auth-id-token'
|
||||
uses: './'
|
||||
with:
|
||||
workload_identity_provider: '${{ secrets.WIF_PROVIDER_NAME }}'
|
||||
service_account: '${{ secrets.OIDC_AUTH_SA_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'
|
||||
uses: './'
|
||||
with:
|
||||
retries: '2'
|
||||
backoff: '200'
|
||||
backoff_limit: '1000'
|
||||
workload_identity_provider: '${{ secrets.WIF_PROVIDER_NAME }}'
|
||||
service_account: '${{ secrets.OIDC_AUTH_SA_EMAIL }}'
|
||||
|
||||
#
|
||||
# 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 }}
|
||||
if: |-
|
||||
${{ github.event_name == 'push' || github.repository == github.event.pull_request.head.repo.full_name }}
|
||||
name: 'docker'
|
||||
runs-on: 'ubuntu-latest'
|
||||
strategy:
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: 'actions/checkout@v3'
|
||||
- uses: 'actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683' # ratchet:actions/checkout@v4
|
||||
|
||||
- uses: 'actions/setup-node@v2'
|
||||
- uses: 'actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020' # ratchet:actions/setup-node@v4
|
||||
with:
|
||||
node-version: '16.x'
|
||||
node-version-file: 'package.json'
|
||||
|
||||
- name: 'npm build'
|
||||
run: 'npm ci && npm run build'
|
||||
@@ -210,10 +273,10 @@ jobs:
|
||||
- name: 'auth-default'
|
||||
uses: './'
|
||||
with:
|
||||
credentials_json: '${{ secrets.AUTH_SA_KEY_JSON }}'
|
||||
credentials_json: '${{ secrets.SERVICE_ACCOUNT_KEY_JSON }}'
|
||||
|
||||
- name: 'docker'
|
||||
uses: 'docker://alpine:3'
|
||||
uses: 'docker://index.docker.io/library/alpine@sha256:4bcff63911fcb4448bd4fdacec207030997caf25e9bea4045fa6c8c44de311d1' # ratchet:docker://alpine:3
|
||||
with:
|
||||
entrypoint: '/bin/sh'
|
||||
args: '-euc "test -n "${GOOGLE_APPLICATION_CREDENTIALS}" && test -r "${GOOGLE_APPLICATION_CREDENTIALS}"'
|
||||
|
||||
16
.github/workflows/troubleshooting.yml
vendored
16
.github/workflows/troubleshooting.yml
vendored
@@ -1,3 +1,17 @@
|
||||
# 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.
|
||||
|
||||
name: 'Troubleshooting'
|
||||
|
||||
on:
|
||||
@@ -13,7 +27,7 @@ jobs:
|
||||
runs-on: 'ubuntu-latest'
|
||||
|
||||
steps:
|
||||
- uses: 'actions/github-script@v6'
|
||||
- uses: 'actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea' # ratchet:actions/github-script@v7
|
||||
with:
|
||||
script: |-
|
||||
const msg =
|
||||
|
||||
50
action.yml
50
action.yml
@@ -56,7 +56,7 @@ inputs:
|
||||
description: |-
|
||||
If true, the action will securely generate a credentials file which can be
|
||||
used for authentication via gcloud and Google Cloud SDKs.
|
||||
default: true
|
||||
default: 'true'
|
||||
required: false
|
||||
export_environment_variables:
|
||||
description: |-
|
||||
@@ -79,7 +79,7 @@ inputs:
|
||||
If false, the action will not export any environment variables, meaning
|
||||
future steps are unlikely to be automatically authenticated to Google
|
||||
Cloud.
|
||||
default: true
|
||||
default: 'true'
|
||||
required: false
|
||||
token_format:
|
||||
description: |-
|
||||
@@ -94,12 +94,26 @@ inputs:
|
||||
impersonation in the chain.
|
||||
default: ''
|
||||
required: false
|
||||
universe:
|
||||
description: |-
|
||||
The Google Cloud universe to use for constructing API endpoints. The
|
||||
default universe is "googleapis.com", which corresponds to
|
||||
https://cloud.google.com. Trusted Partner Cloud and Google Distributed
|
||||
Hosted Cloud should set this to their universe address.
|
||||
required: false
|
||||
default: 'googleapis.com'
|
||||
request_reason:
|
||||
description: |-
|
||||
An optional Reason Request System Parameter for each API call made by the
|
||||
GitHub Action. This will inject the "X-Goog-Request-Reason" HTTP header,
|
||||
which will provide user-supplied information in Google Cloud audit logs.
|
||||
required: false
|
||||
cleanup_credentials:
|
||||
description: |-
|
||||
If true, the action will remove any created credentials from the
|
||||
filesystem upon completion. This only applies if "create_credentials_file"
|
||||
is true.
|
||||
default: true
|
||||
default: 'true'
|
||||
required: false
|
||||
|
||||
# access token params
|
||||
@@ -124,24 +138,6 @@ inputs:
|
||||
default: ''
|
||||
required: false
|
||||
|
||||
# retries
|
||||
retries:
|
||||
description: |-
|
||||
Number of times to retry a failed authentication attempt. This is useful
|
||||
for automated pipelines that may execute before IAM permissions are fully propogated.
|
||||
default: '0'
|
||||
required: false
|
||||
backoff:
|
||||
description: |-
|
||||
Delay time before trying another authentication attempt. This
|
||||
is implemented using a fibonacci backoff method (e.g. 1-1-2-3-5).
|
||||
This value defaults to 100 milliseconds when retries are greater than 0.
|
||||
required: false
|
||||
backoff_limit:
|
||||
description: |-
|
||||
Limits the retry backoff to the specified value.
|
||||
required: false
|
||||
|
||||
# id token params
|
||||
id_token_audience:
|
||||
description: |-
|
||||
@@ -155,7 +151,7 @@ inputs:
|
||||
generated token. If true, the token will contain "email" and
|
||||
"email_verified" claims. This is only valid when "token_format" is
|
||||
"id_token".
|
||||
default: false
|
||||
default: 'false'
|
||||
required: false
|
||||
|
||||
outputs:
|
||||
@@ -166,14 +162,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
|
||||
@@ -184,6 +180,6 @@ branding:
|
||||
color: 'blue'
|
||||
|
||||
runs:
|
||||
using: 'node16'
|
||||
using: 'node24'
|
||||
main: 'dist/main/index.js'
|
||||
post: 'dist/post/index.js'
|
||||
|
||||
4
dist/main/index.js
vendored
4
dist/main/index.js
vendored
File diff suppressed because one or more lines are too long
4
dist/post/index.js
vendored
4
dist/post/index.js
vendored
File diff suppressed because one or more lines are too long
257
docs/EXAMPLES.md
Normal file
257
docs/EXAMPLES.md
Normal file
@@ -0,0 +1,257 @@
|
||||
# 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@v3'
|
||||
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@v3'
|
||||
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'
|
||||
|
||||
# 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@v3'
|
||||
# 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@v3'
|
||||
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@v3'
|
||||
with:
|
||||
project_id: 'my-project'
|
||||
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
|
||||
|
||||
- name: 'Set up Cloud SDK'
|
||||
uses: 'google-github-actions/setup-gcloud@v2'
|
||||
```
|
||||
|
||||
### 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].
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 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@v3'
|
||||
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.
|
||||
|
||||
> [!IMPORTANT]
|
||||
>
|
||||
> 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@v3'
|
||||
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 }}"
|
||||
|
||||
# Example of using ID token in Python code
|
||||
- id: 'python-example'
|
||||
run: |-
|
||||
python -c "
|
||||
import os
|
||||
import requests
|
||||
|
||||
# ID token is available as environment variable
|
||||
id_token = os.environ.get('GOOGLE_ID_TOKEN', '${{ steps.auth.outputs.id_token }}')
|
||||
|
||||
# Use the token to invoke a Cloud Run service
|
||||
response = requests.get(
|
||||
'https://myapp-uvehjacqzq.a.run.app',
|
||||
headers={'Authorization': f'Bearer {id_token}'}
|
||||
)
|
||||
print(response.text)
|
||||
"
|
||||
```
|
||||
|
||||
### Using Default Credentials with Scopes in Python
|
||||
|
||||
When using Workload Identity Federation with Python libraries, you may need to
|
||||
add scopes before refreshing credentials:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
job_id:
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
|
||||
steps:
|
||||
- uses: 'actions/checkout@v4'
|
||||
|
||||
- id: 'auth'
|
||||
uses: 'google-github-actions/auth@v3'
|
||||
with:
|
||||
workload_identity_provider: 'projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/my-provider'
|
||||
service_account: 'my-service-account@my-project.iam.gserviceaccount.com'
|
||||
|
||||
- id: 'python-auth'
|
||||
run: |-
|
||||
python -c "
|
||||
from google.auth import default
|
||||
from google.auth.transport.requests import Request
|
||||
|
||||
# Get default credentials
|
||||
credentials, project = default()
|
||||
|
||||
# Add scopes before refreshing for impersonation
|
||||
credentials = credentials.with_scopes(
|
||||
['https://www.googleapis.com/auth/cloud-platform']
|
||||
)
|
||||
|
||||
# Refresh to get the token
|
||||
credentials.refresh(request=Request())
|
||||
|
||||
# Now you can use the credentials
|
||||
print(f'Access token: {credentials.token}')
|
||||
if hasattr(credentials, 'id_token'):
|
||||
print(f'ID token: {credentials.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
|
||||
47
docs/SECURITY_CONSIDERATIONS.md
Normal file
47
docs/SECURITY_CONSIDERATIONS.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# Security Considerations
|
||||
|
||||
There are important risks to consider when mapping GitHub Actions OIDC token
|
||||
claims.
|
||||
|
||||
|
||||
## Use Unique Mapping Values
|
||||
|
||||
Many of the claims embedded in the GitHub Actions OIDC token are not guaranteed
|
||||
to be unique, and tokens issued by other GitHub organizations or repositories
|
||||
may contain the same values, allowing them to establish an identity. To protect
|
||||
against this situation, always use an Attribute Condition to restrict access to
|
||||
tokens issued by your GitHub organization.
|
||||
|
||||
```cel
|
||||
assertion.repository_owner == 'my-github-org'
|
||||
```
|
||||
|
||||
Never use a "*" in an IAM Binding unless you absolutely know what you are doing!
|
||||
|
||||
|
||||
## Use GitHub's Numeric, Immutable Values
|
||||
|
||||
Using "name" fields in Attribute Conditions or IAM Bindings like `repository` and `repository_owner` increase the chances of [cybersquatting][] and [typosquatting][] attacks. If you delete your GitHub repository or GitHub organization, someone could claim that same name and establish an identity. To protect against this situation, use the numeric `*_id` fields instead, which GitHub guarantees to be unique and never re-used.
|
||||
|
||||
To get your numeric organization ID:
|
||||
|
||||
```sh
|
||||
ORG="my-org" # TODO: replace with your org
|
||||
curl -sfL -H "Accept: application/json" "https://api.github.com/orgs/${ORG}" | jq .id
|
||||
```
|
||||
|
||||
To get your numeric repository ID:
|
||||
|
||||
```sh
|
||||
REPO="my-org/my-repo" # TODO: replace with your full repo including the org
|
||||
curl -sfL -H "Accept: application/json" "https://api.github.com/repos/${REPO}" | jq .id
|
||||
```
|
||||
|
||||
These can be used in an Attribute Condition:
|
||||
|
||||
```cel
|
||||
assertion.repository_owner_id == '1342004' && assertion.repository_id == '260064828'
|
||||
```
|
||||
|
||||
[cybersquatting]: https://en.wikipedia.org/wiki/Cybersquatting
|
||||
[typosquatting]: https://en.wikipedia.org/wiki/Typosquatting
|
||||
@@ -2,44 +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:
|
||||
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 the GitHub Action.
|
||||
|
||||
```yaml
|
||||
- uses: 'google-github-actions/auth@v0'
|
||||
with:
|
||||
# ...
|
||||
token_format: 'access_token'
|
||||
```
|
||||
> **⚠️ WARNING!** Enabling debug logging increases the chances of a secret
|
||||
> being accidentally 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.
|
||||
|
||||
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. 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. Look at the [debug logs][debug-logs] to see exactly which step is failing.
|
||||
Ensure you are using the latest version of that GitHub Action.
|
||||
|
||||
1. Make sure you use `actions/checkout@v3` **before** the `auth` action in your
|
||||
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 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. Look at the [debug logs][debug-logs] 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@v3'
|
||||
```
|
||||
|
||||
1. Ensure the value for `workload_identity_provider` is the full _Provider_
|
||||
name, **not** the _Pool_ name:
|
||||
@@ -49,38 +38,68 @@ some common sources of errors:
|
||||
+ projects/NUMBER/locations/global/workloadIdentityPools/POOL/providers/PROVIDER
|
||||
```
|
||||
|
||||
1. Ensure the `workload_identity_provider` uses the Google Cloud Project
|
||||
**number**. Workload Identity Federation does not accept Google Cloud
|
||||
Project IDs.
|
||||
|
||||
```diff
|
||||
- projects/my-project/locations/global/workloadIdentityPools/my-pool/providers/my-provider
|
||||
+ projects/1234567890/locations/global/workloadIdentityPools/my-pool/providers/my-provider
|
||||
```
|
||||
|
||||
1. Ensure that you have the correct `permissions:` for the job in your
|
||||
workflow, per the [usage](../README.md#usage) docs:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: 'read'
|
||||
id-token: 'write'
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
> **ℹ️ 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 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 includes "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 hinting at the root problem.
|
||||
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.
|
||||
|
||||
**We do not have control over GitHub Actions outside of the
|
||||
`google-github-actions` GitHub organization.**
|
||||
|
||||
|
||||
## Subject exceeds the 127 byte limit
|
||||
@@ -161,6 +180,37 @@ ways to fix this issue:
|
||||
5. Push
|
||||
```
|
||||
|
||||
## Issuer in ID Token does not match the expected ones
|
||||
|
||||
If you get an error like:
|
||||
|
||||
```text
|
||||
The issuer in ID Token https://github.<company>.net/_services/token does not match the expected ones: https://token.actions.githubusercontent.com/
|
||||
```
|
||||
|
||||
it means that the OIDC token's issuer and the Attribute Mapping do not match.
|
||||
There are a few common reasons why this happens:
|
||||
|
||||
1. You made a typographical error. If you are using the public version of
|
||||
GitHub (https://github.com), the value for the `oidc.issuerUri` should be
|
||||
`https://token.actions.githubusercontent.com`.
|
||||
|
||||
1. You are using a GitHub Enterprise _Cloud_ installation and your GitHub
|
||||
administrator has configured a [unique token
|
||||
URL](https://docs.github.com/en/enterprise-cloud@latest/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#switching-to-a-unique-token-url).
|
||||
Use that URL for `oidc.issuerUri` instead of the public value. You must
|
||||
contact your GitHub administrator for assistance - our team does not have
|
||||
visibility into how your GitHub Enterprise Cloud instance is configured.
|
||||
|
||||
1. You are using a GitHub Enterprise _Server_ installation. In this case, you
|
||||
must contact your GitHub administrator to get the URL for OIDC token
|
||||
verification. This is usually `https://github.company.com/_services/token`,
|
||||
but it can be customized by the installation. Furthermore, your GitHub
|
||||
administrator may have disabled this functionality. You must contact your
|
||||
GitHub administrator for assistance - our team does not have visibility
|
||||
into how your GitHub Enterprise Server instance is configured.
|
||||
|
||||
|
||||
<a name="aggressive-replacement"></a>
|
||||
|
||||
## Aggressive *** replacement in logs
|
||||
@@ -180,6 +230,80 @@ tool like `jq`:
|
||||
cat credentials.json | jq -r tostring
|
||||
```
|
||||
|
||||
<a name="cannot-refresh"></a>
|
||||
|
||||
## Cannot refresh credentials to retrieve an ID token
|
||||
|
||||
If you get an error like:
|
||||
|
||||
```text
|
||||
google.auth.exceptions.RefreshError: ('Unable to acquire impersonated credentials', '{"error": {"code": 400, "message": "Request contains an invalid argument.", "status": "INVALID_ARGUMENT"}}')
|
||||
```
|
||||
|
||||
when trying to refresh credentials in Python code to get an ID token, this is
|
||||
usually because the credentials are missing required scopes. The Google Auth
|
||||
library requires scopes to be set when refreshing credentials for impersonation.
|
||||
|
||||
To fix this issue, add the required scopes before refreshing:
|
||||
|
||||
```python
|
||||
from google.auth import default
|
||||
from google.auth.transport.requests import Request
|
||||
|
||||
credentials, project = default()
|
||||
|
||||
# Add scopes before refreshing
|
||||
credentials = credentials.with_scopes(
|
||||
["https://www.googleapis.com/auth/cloud-platform"]
|
||||
)
|
||||
credentials.refresh(request=Request())
|
||||
|
||||
# Now you can access the ID token
|
||||
print(credentials.id_token)
|
||||
```
|
||||
|
||||
Alternatively, you can use the `token_format` parameter of this action to
|
||||
generate an ID token directly:
|
||||
|
||||
```yaml
|
||||
- uses: 'google-github-actions/auth@v3'
|
||||
with:
|
||||
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
|
||||
service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
|
||||
token_format: 'id_token'
|
||||
id_token_audience: 'https://example.com'
|
||||
```
|
||||
|
||||
This will export the ID token as an environment variable that you can use in
|
||||
your Python code.
|
||||
|
||||
## 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
|
||||
@@ -188,3 +312,4 @@ cat credentials.json | jq -r tostring
|
||||
[wif-byte-limit]: https://cloud.google.com/iam/docs/configuring-workload-identity-federation
|
||||
[cal]: https://cloud.google.com/logging/docs/audit/configure-data-access
|
||||
[github-secrets]: https://docs.github.com/en/actions/security-guides/encrypted-secrets
|
||||
[oidc-debugger]: https://github.com/github/actions-oidc-debugger
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 34 KiB |
111
docs/google-github-actions-auth-service-account-key-export.svg
Normal file
111
docs/google-github-actions-auth-service-account-key-export.svg
Normal 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 |
@@ -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 |
18
eslint.config.mjs
Normal file
18
eslint.config.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import js from '@eslint/js';
|
||||
import ts from 'typescript-eslint';
|
||||
import tsParser from '@typescript-eslint/parser';
|
||||
|
||||
import prettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
|
||||
export default ts.config(
|
||||
js.configs.recommended,
|
||||
ts.configs.eslintRecommended,
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parser: tsParser,
|
||||
},
|
||||
},
|
||||
{ ignores: ['dist/', '**/*.js'] },
|
||||
prettierRecommended,
|
||||
);
|
||||
3906
package-lock.json
generated
3906
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
44
package.json
44
package.json
@@ -1,13 +1,17 @@
|
||||
{
|
||||
"name": "@google-github-actions/auth",
|
||||
"version": "0.8.3",
|
||||
"version": "3.0.0",
|
||||
"description": "Authenticate to Google Cloud using OIDC tokens or JSON service account keys.",
|
||||
"main": "dist/main/index.js",
|
||||
"scripts": {
|
||||
"build": "ncc build -m src/main.ts -o dist/main && ncc build -m src/post.ts -o dist/post",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"format": "prettier --write **/*.ts",
|
||||
"test": "mocha -r ts-node/register -t 120s 'tests/**/*.test.ts'"
|
||||
"lint": "eslint .",
|
||||
"format": "eslint . --fix",
|
||||
"test": "node --require ts-node/register --test-reporter spec --test tests/**/*.test.ts"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 24.x",
|
||||
"npm": ">= 11.x"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -23,24 +27,22 @@
|
||||
"author": "GoogleCloudPlatform",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@actions/core": "^1.10.0",
|
||||
"@actions/http-client": "^2.0.1",
|
||||
"@google-github-actions/actions-utils": "^0.4.3"
|
||||
"@actions/core": "^1.11.1",
|
||||
"@actions/http-client": "^2.2.3",
|
||||
"@google-github-actions/actions-utils": "^1.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.3",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/node": "^18.8.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.0",
|
||||
"@typescript-eslint/parser": "^5.40.0",
|
||||
"@vercel/ncc": "^0.34.0",
|
||||
"chai": "^4.3.6",
|
||||
"eslint": "^8.25.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"mocha": "^10.0.0",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.4"
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.34.0",
|
||||
"@types/node": "^24.3.0",
|
||||
"@vercel/ncc": "^0.38.3",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint": "^9.34.0",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript-eslint": "^8.41.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.41.0",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
|
||||
156
src/base.ts
156
src/base.ts
@@ -1,156 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
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');
|
||||
|
||||
// userAgent is the default user agent.
|
||||
const userAgent = `google-github-actions:auth/${appVersion}`;
|
||||
|
||||
/**
|
||||
* BaseClient is the default HTTP client for interacting with the IAM
|
||||
* credentials API.
|
||||
*/
|
||||
export class BaseClient {
|
||||
/**
|
||||
* client is the HTTP client.
|
||||
*/
|
||||
protected readonly client: HttpClient;
|
||||
|
||||
constructor() {
|
||||
this.client = new HttpClient(userAgent);
|
||||
}
|
||||
|
||||
/**
|
||||
* googleIDToken generates a Google Cloud ID token for the provided
|
||||
* service account email or unique id.
|
||||
*/
|
||||
async googleIDToken(
|
||||
token: string,
|
||||
{ serviceAccount, audience, delegates, includeEmail }: GoogleIDTokenParameters,
|
||||
): Promise<GoogleIDTokenResponse> {
|
||||
const pth = `https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
|
||||
|
||||
const data = {
|
||||
delegates: delegates,
|
||||
audience: audience,
|
||||
includeEmail: includeEmail,
|
||||
};
|
||||
|
||||
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>> = {};
|
||||
if (delegates && delegates.length > 0) {
|
||||
data.delegates = delegates;
|
||||
}
|
||||
if (scopes && scopes.length > 0) {
|
||||
// Not a typo, the API expects the field to be "scope" (singular).
|
||||
data.scope = scopes;
|
||||
}
|
||||
if (lifetime && lifetime > 0) {
|
||||
data.lifetime = `${lifetime}s`;
|
||||
}
|
||||
|
||||
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 {
|
||||
accessToken: parsed['accessToken'],
|
||||
expiration: parsed['expireTime'],
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to generate Google Cloud 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`;
|
||||
|
||||
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);
|
||||
|
||||
try {
|
||||
const resp = await this.client.request('POST', pth, data.toString(), 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);
|
||||
|
||||
// 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(),
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to generate Google Cloud OAuth token: ${err}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* Defines the main interface for all clients that generate credentials.
|
||||
*/
|
||||
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>;
|
||||
|
||||
/**
|
||||
* Provided by BaseClient.
|
||||
*/
|
||||
googleIDToken(token: string, params: GoogleIDTokenParameters): Promise<GoogleIDTokenResponse>;
|
||||
googleAccessToken(
|
||||
token: string,
|
||||
params: GoogleAccessTokenParameters,
|
||||
): Promise<GoogleAccessTokenResponse>;
|
||||
googleOAuthToken(assertion: string): Promise<GoogleAccessTokenResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
101
src/client/client.ts
Normal file
101
src/client/client.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
// 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 { HttpClient } from '@actions/http-client';
|
||||
|
||||
import { expandUniverseEndpoints } from '@google-github-actions/actions-utils';
|
||||
|
||||
import { Logger } from '../logger';
|
||||
import { userAgent } from '../utils';
|
||||
|
||||
/**
|
||||
* AuthClient is the default HTTP client for interacting with the IAM credentials
|
||||
* API.
|
||||
*/
|
||||
export interface AuthClient {
|
||||
/**
|
||||
* getToken() gets or generates the best token for the auth client.
|
||||
*/
|
||||
getToken(): Promise<string>;
|
||||
|
||||
/**
|
||||
* createCredentialsFile creates a credential file (for use with gcloud and
|
||||
* other Google Cloud tools) that instructs the tool how to perform identity
|
||||
* federation.
|
||||
*/
|
||||
createCredentialsFile(outputPath: string): Promise<string>; // eslint-disable-line no-unused-vars
|
||||
|
||||
/**
|
||||
* signJWT signs a JWT using the auth provider.
|
||||
*/
|
||||
signJWT(claims: any): Promise<string>; // eslint-disable-line no-unused-vars
|
||||
}
|
||||
|
||||
export interface ClientParameters {
|
||||
logger: Logger;
|
||||
universe: string;
|
||||
requestReason?: string;
|
||||
}
|
||||
|
||||
export abstract class Client {
|
||||
protected readonly _logger: Logger;
|
||||
protected readonly _httpClient: HttpClient;
|
||||
private readonly _requestReason: string | undefined;
|
||||
|
||||
protected readonly _endpoints = {
|
||||
iam: 'https://iam.{universe}/v1',
|
||||
iamcredentials: 'https://iamcredentials.{universe}/v1',
|
||||
oauth2: 'https://oauth2.{universe}',
|
||||
sts: 'https://sts.{universe}/v1',
|
||||
www: 'https://www.{universe}',
|
||||
};
|
||||
|
||||
constructor(child: string, opts: ClientParameters) {
|
||||
this._logger = opts.logger.withNamespace(child);
|
||||
|
||||
// Create the http client with our user agent.
|
||||
this._httpClient = new HttpClient(userAgent, undefined, {
|
||||
allowRedirects: true,
|
||||
allowRetries: true,
|
||||
keepAlive: true,
|
||||
maxRedirects: 5,
|
||||
maxRetries: 3,
|
||||
});
|
||||
|
||||
this._endpoints = expandUniverseEndpoints(this._endpoints, opts.universe);
|
||||
this._requestReason = opts.requestReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* _headers returns any added headers to apply to HTTP API calls.
|
||||
*/
|
||||
protected _headers(): Record<string, string> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (this._requestReason) {
|
||||
headers['X-Goog-Request-Reason'] = this._requestReason;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
}
|
||||
export { IAMCredentialsClient, IAMCredentialsClientParameters } from './iamcredentials';
|
||||
|
||||
export {
|
||||
ServiceAccountKeyClient,
|
||||
ServiceAccountKeyClientParameters,
|
||||
} from './service_account_key_json';
|
||||
|
||||
export {
|
||||
WorkloadIdentityFederationClient,
|
||||
WorkloadIdentityFederationClientParameters,
|
||||
} from './workload_identity_federation';
|
||||
@@ -1,132 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import { createSign } from 'crypto';
|
||||
import {
|
||||
isServiceAccountKey,
|
||||
parseCredential,
|
||||
ServiceAccountKey,
|
||||
toBase64,
|
||||
writeSecureFile,
|
||||
} from '@google-github-actions/actions-utils';
|
||||
|
||||
import { AuthClient } from './auth_client';
|
||||
import { BaseClient } from '../base';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
interface CredentialsJSONClientOptions {
|
||||
projectID?: string;
|
||||
credentialsJSON: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* CredentialsJSONClient is a client that accepts a service account key JSON
|
||||
* credential.
|
||||
*/
|
||||
export class CredentialsJSONClient extends BaseClient implements AuthClient {
|
||||
readonly #projectID: string;
|
||||
readonly #credentials: ServiceAccountKey;
|
||||
|
||||
constructor(opts: CredentialsJSONClientOptions) {
|
||||
super();
|
||||
|
||||
const credentials = parseCredential(opts.credentialsJSON);
|
||||
if (!isServiceAccountKey(credentials)) {
|
||||
throw new Error(`Provided credential is not a valid service account key JSON`);
|
||||
}
|
||||
this.#credentials = credentials;
|
||||
|
||||
this.#projectID = opts.projectID || this.#credentials.project_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* getAuthToken generates a token capable of calling the iamcredentials API.
|
||||
*/
|
||||
async getAuthToken(): Promise<string> {
|
||||
const header = {
|
||||
alg: 'RS256',
|
||||
typ: 'JWT',
|
||||
kid: this.#credentials.private_key_id,
|
||||
};
|
||||
|
||||
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/',
|
||||
iat: now,
|
||||
exp: now + 3599,
|
||||
};
|
||||
|
||||
const message = toBase64(JSON.stringify(header)) + '.' + toBase64(JSON.stringify(body));
|
||||
|
||||
try {
|
||||
const signer = createSign('RSA-SHA256');
|
||||
signer.write(message);
|
||||
signer.end();
|
||||
|
||||
const signature = signer.sign(this.#credentials.private_key);
|
||||
return message + '.' + toBase64(signature);
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to sign auth token using ${await this.getServiceAccount()}: ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* signJWT signs the given JWT with the private key.
|
||||
*
|
||||
* @param unsignedJWT The JWT to sign.
|
||||
*/
|
||||
async signJWT(unsignedJWT: string): Promise<string> {
|
||||
const header = {
|
||||
alg: 'RS256',
|
||||
typ: 'JWT',
|
||||
kid: this.#credentials.private_key_id,
|
||||
};
|
||||
|
||||
const message = toBase64(JSON.stringify(header)) + '.' + toBase64(unsignedJWT);
|
||||
|
||||
try {
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async createCredentialsFile(outputPath: string): Promise<string> {
|
||||
return await writeSecureFile(outputPath, JSON.stringify(this.#credentials));
|
||||
}
|
||||
}
|
||||
208
src/client/iamcredentials.ts
Normal file
208
src/client/iamcredentials.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
// 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 { URLSearchParams } from 'url';
|
||||
|
||||
import { errorMessage } from '@google-github-actions/actions-utils';
|
||||
|
||||
import { Client, ClientParameters } from './client';
|
||||
|
||||
/**
|
||||
* GenerateAccessTokenParameters are the inputs to the generateAccessToken call.
|
||||
*/
|
||||
export interface GenerateAccessTokenParameters {
|
||||
readonly serviceAccount: string;
|
||||
readonly delegates?: string[];
|
||||
readonly scopes?: string[];
|
||||
readonly lifetime?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 extends ClientParameters {
|
||||
readonly authToken: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* IAMCredentialsClient is a thin HTTP client around the Google Cloud IAM
|
||||
* Credentials API.
|
||||
*/
|
||||
export class IAMCredentialsClient extends Client {
|
||||
readonly #authToken: string;
|
||||
|
||||
constructor(opts: IAMCredentialsClientParameters) {
|
||||
super('IAMCredentialsClient', opts);
|
||||
|
||||
this.#authToken = opts.authToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* generateAccessToken generates a new OAuth 2.0 Access Token for a service
|
||||
* account.
|
||||
*/
|
||||
async generateAccessToken({
|
||||
serviceAccount,
|
||||
delegates,
|
||||
scopes,
|
||||
lifetime,
|
||||
}: GenerateAccessTokenParameters): Promise<string> {
|
||||
const logger = this._logger.withNamespace('generateAccessToken');
|
||||
|
||||
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateAccessToken`;
|
||||
|
||||
const headers = Object.assign(this._headers(), {
|
||||
Authorization: `Bearer ${this.#authToken}`,
|
||||
});
|
||||
|
||||
const body: Record<string, string | Array<string>> = {};
|
||||
if (delegates && delegates.length > 0) {
|
||||
body.delegates = delegates;
|
||||
}
|
||||
if (scopes && scopes.length > 0) {
|
||||
// Not a typo, the API expects the field to be "scope" (singular).
|
||||
body.scope = scopes;
|
||||
}
|
||||
if (lifetime && lifetime > 0) {
|
||||
body.lifetime = `${lifetime}s`;
|
||||
}
|
||||
|
||||
logger.debug(`Built request`, {
|
||||
method: `POST`,
|
||||
path: pth,
|
||||
headers: headers,
|
||||
body: body,
|
||||
});
|
||||
|
||||
try {
|
||||
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 result = resp.result;
|
||||
if (!result) {
|
||||
throw new Error(`Successfully called ${pth}, but the result was empty`);
|
||||
}
|
||||
return result.accessToken;
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err);
|
||||
throw new Error(
|
||||
`Failed to generate Google Cloud OAuth 2.0 Access Token for ${serviceAccount}: ${msg}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async generateDomainWideDelegationAccessToken(assertion: string): Promise<string> {
|
||||
const logger = this._logger.withNamespace('generateDomainWideDelegationAccessToken');
|
||||
|
||||
const pth = `${this._endpoints.oauth2}/token`;
|
||||
|
||||
const headers = Object.assign(this._headers(), {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
});
|
||||
|
||||
const body = new URLSearchParams();
|
||||
body.append('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
|
||||
body.append('assertion', assertion);
|
||||
|
||||
logger.debug(`Built request`, {
|
||||
method: `POST`,
|
||||
path: pth,
|
||||
headers: headers,
|
||||
body: body.toString(),
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await this._httpClient.post(pth, body.toString(), headers);
|
||||
const respBody = await resp.readBody();
|
||||
const statusCode = resp.message.statusCode || 500;
|
||||
if (statusCode < 200 || statusCode > 299) {
|
||||
throw new Error(`Failed to call ${pth}: HTTP ${statusCode}: ${respBody || '[no body]'}`);
|
||||
}
|
||||
const parsed = JSON.parse(respBody) as { access_token: string };
|
||||
return parsed.access_token;
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err);
|
||||
throw new Error(
|
||||
`Failed to generate Google Cloud Domain Wide Delegation OAuth 2.0 Access Token: ${msg}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* generateIDToken generates a new OpenID Connect ID token for a service
|
||||
* account.
|
||||
*/
|
||||
async generateIDToken({
|
||||
serviceAccount,
|
||||
audience,
|
||||
delegates,
|
||||
includeEmail,
|
||||
}: GenerateIDTokenParameters): Promise<string> {
|
||||
const logger = this._logger.withNamespace('generateIDToken');
|
||||
|
||||
const pth = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${serviceAccount}:generateIdToken`;
|
||||
|
||||
const headers = Object.assign(this._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;
|
||||
}
|
||||
|
||||
logger.debug(`Built request`, {
|
||||
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) {
|
||||
const msg = errorMessage(err);
|
||||
throw new Error(
|
||||
`Failed to generate Google Cloud OpenID Connect ID token for ${serviceAccount}: ${msg}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
139
src/client/service_account_key_json.ts
Normal file
139
src/client/service_account_key_json.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
// 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 { createSign } from 'crypto';
|
||||
|
||||
import {
|
||||
errorMessage,
|
||||
isServiceAccountKey,
|
||||
parseCredential,
|
||||
ServiceAccountKey,
|
||||
toBase64,
|
||||
writeSecureFile,
|
||||
} from '@google-github-actions/actions-utils';
|
||||
|
||||
import { AuthClient, Client, ClientParameters } from './client';
|
||||
|
||||
/**
|
||||
* ServiceAccountKeyClientParameters is used as input to the
|
||||
* ServiceAccountKeyClient.
|
||||
*/
|
||||
export interface ServiceAccountKeyClientParameters extends ClientParameters {
|
||||
readonly serviceAccountKey: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* ServiceAccountKeyClient is an authentication client that expects a Service
|
||||
* Account Key JSON file.
|
||||
*/
|
||||
export class ServiceAccountKeyClient extends Client implements AuthClient {
|
||||
readonly #serviceAccountKey: ServiceAccountKey;
|
||||
readonly #audience: string;
|
||||
|
||||
constructor(opts: ServiceAccountKeyClientParameters) {
|
||||
super('ServiceAccountKeyClient', opts);
|
||||
|
||||
const serviceAccountKey = parseCredential(opts.serviceAccountKey);
|
||||
if (!isServiceAccountKey(serviceAccountKey)) {
|
||||
throw new Error(`Provided credential is not a valid Google Service Account Key JSON`);
|
||||
}
|
||||
this.#serviceAccountKey = serviceAccountKey;
|
||||
this._logger.debug(`Parsed service account key`, serviceAccountKey.client_email);
|
||||
|
||||
this.#audience = new URL(this._endpoints.iamcredentials).origin + `/`;
|
||||
this._logger.debug(`Computed audience`, this.#audience);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 getToken(): Promise<string> {
|
||||
const logger = this._logger.withNamespace('getToken');
|
||||
|
||||
const now = Math.floor(new Date().getTime() / 1000);
|
||||
|
||||
const claims = {
|
||||
iss: this.#serviceAccountKey.client_email,
|
||||
sub: this.#serviceAccountKey.client_email,
|
||||
aud: this.#audience,
|
||||
iat: now,
|
||||
exp: now + 3599,
|
||||
};
|
||||
|
||||
logger.debug(`Built jwt`, {
|
||||
claims: claims,
|
||||
});
|
||||
|
||||
try {
|
||||
return await this.signJWT(claims);
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err);
|
||||
throw new Error(
|
||||
`Failed to sign auth token using ${this.#serviceAccountKey.client_email}: ${msg}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* signJWT signs a JWT using the Service Account's private key.
|
||||
*/
|
||||
async signJWT(claims: any): Promise<string> {
|
||||
const logger = this._logger.withNamespace('signJWT');
|
||||
|
||||
const header = {
|
||||
alg: `RS256`,
|
||||
typ: `JWT`,
|
||||
kid: this.#serviceAccountKey.private_key_id,
|
||||
};
|
||||
|
||||
const message = toBase64(JSON.stringify(header)) + `.` + toBase64(JSON.stringify(claims));
|
||||
|
||||
logger.debug(`Built jwt`, {
|
||||
header: header,
|
||||
claims: claims,
|
||||
message: message,
|
||||
});
|
||||
|
||||
try {
|
||||
const signer = createSign(`RSA-SHA256`);
|
||||
signer.write(message);
|
||||
signer.end();
|
||||
|
||||
const signature = signer.sign(this.#serviceAccountKey.private_key);
|
||||
return message + '.' + toBase64(signature);
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err);
|
||||
throw new Error(
|
||||
`Failed to sign jwt using private key for ${this.#serviceAccountKey.client_email}: ${msg}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* createCredentialsFile writes the Service Account Key JSON back to disk at
|
||||
* the specified outputPath.
|
||||
*/
|
||||
async createCredentialsFile(outputPath: string): Promise<string> {
|
||||
const logger = this._logger.withNamespace('createCredentialsFile');
|
||||
|
||||
logger.debug(`Creating credentials`, {
|
||||
outputPath: outputPath,
|
||||
});
|
||||
|
||||
return await writeSecureFile(outputPath, JSON.stringify(this.#serviceAccountKey));
|
||||
}
|
||||
}
|
||||
@@ -1,212 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
import { URL } from 'url';
|
||||
import { writeSecureFile } from '@google-github-actions/actions-utils';
|
||||
|
||||
import { AuthClient } from './auth_client';
|
||||
import { BaseClient } from '../base';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
interface WorkloadIdentityClientOptions {
|
||||
projectID?: string;
|
||||
providerID: string;
|
||||
serviceAccount: string;
|
||||
token: string;
|
||||
audience: string;
|
||||
|
||||
oidcTokenRequestURL: string;
|
||||
oidcTokenRequestToken: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WorkloadIdentityClient is a client that uses the GitHub Actions runtime to
|
||||
* authentication via Workload Identity.
|
||||
*/
|
||||
export class WorkloadIdentityClient extends BaseClient implements AuthClient {
|
||||
readonly #projectID: string;
|
||||
readonly #providerID: string;
|
||||
readonly #serviceAccount: string;
|
||||
readonly #token: string;
|
||||
readonly #audience: string;
|
||||
|
||||
readonly #oidcTokenRequestURL: string;
|
||||
readonly #oidcTokenRequestToken: string;
|
||||
|
||||
constructor(opts: WorkloadIdentityClientOptions) {
|
||||
super();
|
||||
|
||||
this.#providerID = opts.providerID;
|
||||
this.#serviceAccount = opts.serviceAccount;
|
||||
this.#token = opts.token;
|
||||
this.#audience = opts.audience;
|
||||
|
||||
this.#oidcTokenRequestURL = opts.oidcTokenRequestURL;
|
||||
this.#oidcTokenRequestToken = opts.oidcTokenRequestToken;
|
||||
|
||||
this.#projectID =
|
||||
opts.projectID || this.extractProjectIDFromServiceAccountEmail(this.#serviceAccount);
|
||||
}
|
||||
|
||||
/**
|
||||
* extractProjectIDFromServiceAccountEmail extracts the project ID from the
|
||||
* service account email address.
|
||||
*/
|
||||
extractProjectIDFromServiceAccountEmail(str: string): string {
|
||||
if (!str) {
|
||||
return '';
|
||||
}
|
||||
|
||||
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 [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 headers = {
|
||||
'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 parsed['access_token'];
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
`Failed to generate Google Cloud federated token for ${this.#providerID}: ${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.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
const headers = {
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${federatedToken}`,
|
||||
'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 parsed['signedJwt'];
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to sign JWT using ${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.
|
||||
*/
|
||||
async createCredentialsFile(outputPath: string): Promise<string> {
|
||||
const requestURL = new URL(this.#oidcTokenRequestURL);
|
||||
|
||||
// Append the audience value to the request.
|
||||
const params = requestURL.searchParams;
|
||||
params.set('audience', this.#audience);
|
||||
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`,
|
||||
credential_source: {
|
||||
url: requestURL,
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.#oidcTokenRequestToken}`,
|
||||
},
|
||||
format: {
|
||||
type: 'json',
|
||||
subject_token_field_name: 'value',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return await writeSecureFile(outputPath, JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
216
src/client/workload_identity_federation.ts
Normal file
216
src/client/workload_identity_federation.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
// 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 { errorMessage, writeSecureFile } from '@google-github-actions/actions-utils';
|
||||
|
||||
import { AuthClient, Client, ClientParameters } from './client';
|
||||
|
||||
/**
|
||||
* WorkloadIdentityFederationClientParameters is used as input to the
|
||||
* WorkloadIdentityFederationClient.
|
||||
*/
|
||||
export interface WorkloadIdentityFederationClientParameters extends ClientParameters {
|
||||
readonly githubOIDCToken: string;
|
||||
readonly githubOIDCTokenRequestURL: string;
|
||||
readonly githubOIDCTokenRequestToken: string;
|
||||
readonly githubOIDCTokenAudience: string;
|
||||
readonly workloadIdentityProviderName: string;
|
||||
readonly audience?: string;
|
||||
readonly serviceAccount?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* WorkloadIdentityFederationClient is an authentication client that configures
|
||||
* a Workload Identity authentication scheme.
|
||||
*/
|
||||
export class WorkloadIdentityFederationClient extends Client implements AuthClient {
|
||||
readonly #githubOIDCToken: string;
|
||||
readonly #githubOIDCTokenRequestURL: string;
|
||||
readonly #githubOIDCTokenRequestToken: string;
|
||||
readonly #githubOIDCTokenAudience: string;
|
||||
readonly #workloadIdentityProviderName: string;
|
||||
readonly #serviceAccount?: string;
|
||||
readonly #audience: string;
|
||||
|
||||
#cachedToken?: string;
|
||||
#cachedAt?: number;
|
||||
|
||||
constructor(opts: WorkloadIdentityFederationClientParameters) {
|
||||
super('WorkloadIdentityFederationClient', opts);
|
||||
|
||||
this.#githubOIDCToken = opts.githubOIDCToken;
|
||||
this.#githubOIDCTokenRequestURL = opts.githubOIDCTokenRequestURL;
|
||||
this.#githubOIDCTokenRequestToken = opts.githubOIDCTokenRequestToken;
|
||||
this.#githubOIDCTokenAudience = opts.githubOIDCTokenAudience;
|
||||
this.#workloadIdentityProviderName = opts.workloadIdentityProviderName;
|
||||
this.#serviceAccount = opts.serviceAccount;
|
||||
|
||||
const iamHost = new URL(this._endpoints.iam).host;
|
||||
this.#audience = `//${iamHost}/${this.#workloadIdentityProviderName}`;
|
||||
this._logger.debug(`Computed audience`, this.#audience);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
async getToken(): Promise<string> {
|
||||
const logger = this._logger.withNamespace(`getToken`);
|
||||
|
||||
const now = new Date().getTime();
|
||||
if (this.#cachedToken && this.#cachedAt && now - this.#cachedAt < 30_000) {
|
||||
logger.debug(`Using cached token`, {
|
||||
now: now,
|
||||
cachedAt: this.#cachedAt,
|
||||
});
|
||||
return this.#cachedToken;
|
||||
}
|
||||
|
||||
const pth = `${this._endpoints.sts}/token`;
|
||||
|
||||
const headers = Object.assign(this._headers(), {});
|
||||
|
||||
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,
|
||||
};
|
||||
|
||||
logger.debug(`Built request`, {
|
||||
method: `POST`,
|
||||
path: pth,
|
||||
headers: headers,
|
||||
body: body,
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await this._httpClient.postJson<{ access_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`);
|
||||
}
|
||||
|
||||
this.#cachedToken = result.access_token;
|
||||
this.#cachedAt = now;
|
||||
return result.access_token;
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err);
|
||||
throw new Error(
|
||||
`Failed to generate Google Cloud federated token for ${this.#audience}: ${msg}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* signJWT signs a JWT using the Service Account's private key.
|
||||
*/
|
||||
async signJWT(claims: any): Promise<string> {
|
||||
const logger = this._logger.withNamespace(`signJWT`);
|
||||
|
||||
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 = Object.assign(this._headers(), {
|
||||
Authorization: `Bearer ${await this.getToken()}`,
|
||||
});
|
||||
|
||||
const body = {
|
||||
payload: claims,
|
||||
};
|
||||
|
||||
logger.debug(`Built request`, {
|
||||
method: `POST`,
|
||||
path: pth,
|
||||
headers: headers,
|
||||
body: body,
|
||||
});
|
||||
|
||||
try {
|
||||
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 result = resp.result;
|
||||
if (!result) {
|
||||
throw new Error(`Successfully called ${pth}, but the result was empty`);
|
||||
}
|
||||
return result.signedJwt;
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err);
|
||||
throw new Error(`Failed to sign JWT using ${this.#serviceAccount}: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* createCredentialsFile writes a Workload Identity Federation credential file
|
||||
* to disk at the specific outputPath.
|
||||
*/
|
||||
async createCredentialsFile(outputPath: string): Promise<string> {
|
||||
const logger = this._logger.withNamespace(`createCredentialsFile`);
|
||||
|
||||
const requestURL = new URL(this.#githubOIDCTokenRequestURL);
|
||||
|
||||
// Append the audience value to the request.
|
||||
const params = requestURL.searchParams;
|
||||
params.set('audience', this.#githubOIDCTokenAudience);
|
||||
requestURL.search = params.toString();
|
||||
|
||||
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.#githubOIDCTokenRequestToken}`,
|
||||
},
|
||||
format: {
|
||||
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) {
|
||||
const impersonationURL = `${this._endpoints.iamcredentials}/projects/-/serviceAccounts/${this.#serviceAccount}:generateAccessToken`;
|
||||
logger.debug(`Enabling service account impersonation via ${impersonationURL}`);
|
||||
data.service_account_impersonation_url = impersonationURL;
|
||||
}
|
||||
|
||||
logger.debug(`Creating credentials`, {
|
||||
outputPath: outputPath,
|
||||
});
|
||||
|
||||
return await writeSecureFile(outputPath, JSON.stringify(data));
|
||||
}
|
||||
}
|
||||
123
src/logger.ts
Normal file
123
src/logger.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
// 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.
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
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);
|
||||
}
|
||||
}
|
||||
558
src/main.ts
558
src/main.ts
@@ -1,34 +1,52 @@
|
||||
'use strict';
|
||||
// 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 { 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,
|
||||
exactlyOneOf,
|
||||
isEmptyDir,
|
||||
isPinnedToHead,
|
||||
parseCSV,
|
||||
parseMultilineCSV,
|
||||
parseBoolean,
|
||||
parseDuration,
|
||||
pinnedToHeadWarning,
|
||||
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 './client/client';
|
||||
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 ` +
|
||||
@@ -41,269 +59,285 @@ const oidcWarning =
|
||||
`GitHub Actions workflow permissions are incorrect, or this job is being ` +
|
||||
`run from a fork. For more information, please see https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token`;
|
||||
|
||||
/**
|
||||
* Executes the main action.
|
||||
*/
|
||||
async function run(): Promise<void> {
|
||||
export async function run(logger: Logger) {
|
||||
// Warn if pinned to HEAD
|
||||
if (isPinnedToHead()) {
|
||||
logWarning(pinnedToHeadWarning('v0'));
|
||||
logger.warning(pinnedToHeadWarning('v2'));
|
||||
}
|
||||
|
||||
const retries = Number(getInput('retries'));
|
||||
|
||||
// set to undefined when not provided [avoids Number('') -> 0]
|
||||
const backoff = Number(getInput('backoff')) || undefined;
|
||||
const backoffLimit = Number(getInput('backoff_limit')) || undefined;
|
||||
|
||||
try {
|
||||
const mainWithRetries = withRetries(main, {
|
||||
retries: retries,
|
||||
backoff: backoff,
|
||||
backoffLimit: backoffLimit,
|
||||
// Load configuration.
|
||||
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 = parseBoolean(getInput(`create_credentials_file`));
|
||||
const exportEnvironmentVariables = parseBoolean(getInput(`export_environment_variables`));
|
||||
const tokenFormat = getInput(`token_format`);
|
||||
const delegates = parseMultilineCSV(getInput(`delegates`));
|
||||
const universe = getInput(`universe`);
|
||||
const requestReason = getInput(`request_reason`);
|
||||
|
||||
// Ensure exactly one of workload_identity_provider and credentials_json was
|
||||
// provided.
|
||||
if (!exactlyOneOf(workloadIdentityProvider, credentialsJSON)) {
|
||||
throw new Error(
|
||||
'The GitHub Action workflow must specify exactly one of ' +
|
||||
'"workload_identity_provider" or "credentials_json"! ' +
|
||||
secretsWarning,
|
||||
);
|
||||
}
|
||||
|
||||
// Instantiate the correct client based on the provided input parameters.
|
||||
let client: AuthClient;
|
||||
if (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
|
||||
// generating the credentials file.
|
||||
const oidcTokenRequestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
const oidcTokenRequestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
if (!oidcTokenRequestToken || !oidcTokenRequestURL) {
|
||||
throw new Error(oidcWarning);
|
||||
}
|
||||
|
||||
const oidcToken = await withRetries(
|
||||
async (): Promise<string> => {
|
||||
return await getIDToken(oidcTokenAudience);
|
||||
},
|
||||
{ retries: 3 },
|
||||
)();
|
||||
client = new WorkloadIdentityFederationClient({
|
||||
logger: logger,
|
||||
universe: universe,
|
||||
requestReason: requestReason,
|
||||
|
||||
githubOIDCToken: oidcToken,
|
||||
githubOIDCTokenRequestURL: oidcTokenRequestURL,
|
||||
githubOIDCTokenRequestToken: oidcTokenRequestToken,
|
||||
githubOIDCTokenAudience: oidcTokenAudience,
|
||||
workloadIdentityProviderName: workloadIdentityProvider,
|
||||
serviceAccount: serviceAccount,
|
||||
});
|
||||
} else {
|
||||
logger.debug(`Using credentials JSON`);
|
||||
client = new ServiceAccountKeyClient({
|
||||
logger: logger,
|
||||
universe: universe,
|
||||
requestReason: requestReason,
|
||||
|
||||
serviceAccountKey: credentialsJSON,
|
||||
});
|
||||
}
|
||||
|
||||
// Always write the credentials file first, before trying to generate
|
||||
// tokens. This will ensure the file is written even if token generation
|
||||
// fails, which means continue-on-error actions will still have the file
|
||||
// available.
|
||||
if (createCredentialsFile) {
|
||||
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
|
||||
// Docker-based actions on the filesystem. Exporting to GITHUB_WORKSPACE
|
||||
// ensures that the exported credentials are automatically available to
|
||||
// Docker-based actions without user modification.
|
||||
//
|
||||
// This has the unintended side-effect of leaking credentials over time,
|
||||
// because GITHUB_WORKSPACE is not automatically cleaned up on self-hosted
|
||||
// runners. To mitigate this issue, this action defines a post step to
|
||||
// remove any created credentials.
|
||||
const githubWorkspace = process.env.GITHUB_WORKSPACE;
|
||||
if (!githubWorkspace) {
|
||||
throw new Error('$GITHUB_WORKSPACE is not set');
|
||||
}
|
||||
|
||||
// There have been a number of issues where users have not used the
|
||||
// "actions/checkout" step before our action. Our action relies on the
|
||||
// creation of that directory; worse, if a user puts "actions/checkout"
|
||||
// after our action, it will delete the exported credential. This
|
||||
// following code does a small check to see if there are any files in the
|
||||
// directory. It emits a warning if there are no files, since there may be
|
||||
// legitimate use cases for authenticating without checking out the
|
||||
// repository.
|
||||
const githubWorkspaceIsEmpty = await isEmptyDir(githubWorkspace);
|
||||
if (githubWorkspaceIsEmpty) {
|
||||
logger.info(
|
||||
`⚠️ 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 ` +
|
||||
`share authentication with future steps in this job, set ` +
|
||||
`"create_credentials_file" to false.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Create credentials file.
|
||||
const outputFile = generateCredentialsFilename();
|
||||
const outputPath = pathjoin(githubWorkspace, outputFile);
|
||||
const credentialsPath = await client.createCredentialsFile(outputPath);
|
||||
logger.info(`Created credentials file at "${credentialsPath}"`);
|
||||
|
||||
// Output to be available to future steps.
|
||||
setOutput('credentials_file_path', credentialsPath);
|
||||
|
||||
if (exportEnvironmentVariables) {
|
||||
// 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).
|
||||
exportVariable('CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE', credentialsPath);
|
||||
|
||||
// GOOGLE_APPLICATION_CREDENTIALS is used by Application Default
|
||||
// Credentials in all GCP client libraries.
|
||||
exportVariable('GOOGLE_APPLICATION_CREDENTIALS', credentialsPath);
|
||||
|
||||
// GOOGLE_GHA_CREDS_PATH is used by other Google GitHub Actions.
|
||||
exportVariable('GOOGLE_GHA_CREDS_PATH', credentialsPath);
|
||||
}
|
||||
}
|
||||
|
||||
// Set the project ID environment variables to the computed values.
|
||||
if (!projectID) {
|
||||
logger.info(
|
||||
`⚠️ Failed to compute a project ID from the given inputs. Neither the ` +
|
||||
`"project_id" output nor any environment variables will be ` +
|
||||
`exported. If you require these values in other steps, specify the ` +
|
||||
`"project_id" input directly.`,
|
||||
);
|
||||
} else {
|
||||
setOutput('project_id', projectID);
|
||||
|
||||
if (exportEnvironmentVariables) {
|
||||
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: logger,
|
||||
universe: universe,
|
||||
|
||||
authToken: authToken,
|
||||
});
|
||||
|
||||
await mainWithRetries();
|
||||
switch (tokenFormat) {
|
||||
case '': {
|
||||
break;
|
||||
}
|
||||
case null: {
|
||||
break;
|
||||
}
|
||||
case 'access_token': {
|
||||
logger.debug(`Creating access token`);
|
||||
|
||||
const accessTokenLifetime = parseDuration(getInput('access_token_lifetime'));
|
||||
const accessTokenScopes = parseMultilineCSV(getInput('access_token_scopes'));
|
||||
const accessTokenSubject = getInput('access_token_subject');
|
||||
|
||||
// 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,
|
||||
);
|
||||
}
|
||||
|
||||
let accessToken: string;
|
||||
|
||||
// If a subject was provided, use the traditional OAuth 2.0 flow to
|
||||
// perform Domain-Wide Delegation. Otherwise, use the modern IAM
|
||||
// Credentials endpoints.
|
||||
if (accessTokenSubject) {
|
||||
logger.debug(`Using Domain-Wide Delegation flow`);
|
||||
|
||||
if (accessTokenLifetime > 3600) {
|
||||
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.`,
|
||||
);
|
||||
}
|
||||
|
||||
const unsignedJWT = buildDomainWideDelegationJWT(
|
||||
serviceAccount,
|
||||
accessTokenSubject,
|
||||
accessTokenScopes,
|
||||
accessTokenLifetime,
|
||||
);
|
||||
const signedJWT = await client.signJWT(unsignedJWT);
|
||||
accessToken =
|
||||
await iamCredentialsClient.generateDomainWideDelegationAccessToken(signedJWT);
|
||||
} else {
|
||||
logger.debug(`Using normal access token flow`);
|
||||
accessToken = await iamCredentialsClient.generateAccessToken({
|
||||
serviceAccount,
|
||||
delegates,
|
||||
scopes: accessTokenScopes,
|
||||
lifetime: accessTokenLifetime,
|
||||
});
|
||||
}
|
||||
|
||||
setSecret(accessToken);
|
||||
setOutput('access_token', accessToken);
|
||||
break;
|
||||
}
|
||||
case 'id_token': {
|
||||
logger.debug(`Creating id token`);
|
||||
|
||||
const idTokenAudience = getInput('id_token_audience', { required: true });
|
||||
const idTokenIncludeEmail = parseBoolean(getInput('id_token_include_email'));
|
||||
|
||||
// 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(idToken);
|
||||
setOutput('id_token', idToken);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown token format "${tokenFormat}"`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err);
|
||||
setFailed(`google-github-actions/auth failed with: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main wraps the main action logic into a function to be used as a parameter to the withRetries function.
|
||||
*/
|
||||
async function main() {
|
||||
// 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'));
|
||||
|
||||
// Ensure exactly one of workload_identity_provider and credentials_json was
|
||||
// provided.
|
||||
if (!exactlyOneOf(workloadIdentityProvider, credentialsJSON)) {
|
||||
throw new Error(
|
||||
'The GitHub Action workflow must specify exactly one of ' +
|
||||
'"workload_identity_provider" or "credentials_json"! ' +
|
||||
secretsWarning,
|
||||
);
|
||||
}
|
||||
|
||||
// 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}"`);
|
||||
|
||||
// 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
|
||||
// generating the credentials file.
|
||||
const oidcTokenRequestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
||||
const oidcTokenRequestURL = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
||||
if (!oidcTokenRequestToken || !oidcTokenRequestURL) {
|
||||
throw new Error(oidcWarning);
|
||||
}
|
||||
|
||||
const token = await getIDToken(audience);
|
||||
client = new WorkloadIdentityClient({
|
||||
projectID: projectID,
|
||||
providerID: workloadIdentityProvider,
|
||||
serviceAccount: serviceAccount,
|
||||
token: token,
|
||||
audience: audience,
|
||||
oidcTokenRequestToken: oidcTokenRequestToken,
|
||||
oidcTokenRequestURL: oidcTokenRequestURL,
|
||||
});
|
||||
} else {
|
||||
logDebug(`Using credentials JSON`);
|
||||
client = new CredentialsJSONClient({
|
||||
projectID: projectID,
|
||||
credentialsJSON: credentialsJSON,
|
||||
});
|
||||
}
|
||||
|
||||
// Always write the credentials file first, before trying to generate
|
||||
// tokens. This will ensure the file is written even if token generation
|
||||
// fails, which means continue-on-error actions will still have the file
|
||||
// available.
|
||||
if (createCredentialsFile) {
|
||||
logDebug(`Creating credentials file`);
|
||||
|
||||
// Note: We explicitly and intentionally export to GITHUB_WORKSPACE
|
||||
// instead of RUNNER_TEMP, because RUNNER_TEMP is not shared with
|
||||
// Docker-based actions on the filesystem. Exporting to GITHUB_WORKSPACE
|
||||
// ensures that the exported credentials are automatically available to
|
||||
// Docker-based actions without user modification.
|
||||
//
|
||||
// This has the unintended side-effect of leaking credentials over time,
|
||||
// because GITHUB_WORKSPACE is not automatically cleaned up on self-hosted
|
||||
// runners. To mitigate this issue, this action defines a post step to
|
||||
// remove any created credentials.
|
||||
const githubWorkspace = process.env.GITHUB_WORKSPACE;
|
||||
if (!githubWorkspace) {
|
||||
throw new Error('$GITHUB_WORKSPACE is not set');
|
||||
}
|
||||
|
||||
// There have been a number of issues where users have not used the
|
||||
// "actions/checkout" step before our action. Our action relies on the
|
||||
// creation of that directory; worse, if a user puts "actions/checkout"
|
||||
// after our action, it will delete the exported credential. This
|
||||
// following code does a small check to see if there are any files in the
|
||||
// directory. It emits a warning if there are no files, since there may be
|
||||
// legitimate use cases for authenticating without checking out the
|
||||
// repository.
|
||||
const githubWorkspaceIsEmpty = await isEmptyDir(githubWorkspace);
|
||||
if (githubWorkspaceIsEmpty) {
|
||||
logWarning(
|
||||
`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 ` +
|
||||
`share authentication with future steps in this job, set ` +
|
||||
`"create_credentials_file" to false.`,
|
||||
);
|
||||
}
|
||||
|
||||
// Create credentials file.
|
||||
const outputFile = generateCredentialsFilename();
|
||||
const outputPath = pathjoin(githubWorkspace, outputFile);
|
||||
const credentialsPath = await client.createCredentialsFile(outputPath);
|
||||
logInfo(`Created credentials file at "${credentialsPath}"`);
|
||||
|
||||
// Output to be available to future steps.
|
||||
setOutput('credentials_file_path', credentialsPath);
|
||||
|
||||
if (exportEnvironmentVariables) {
|
||||
// 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);
|
||||
|
||||
// GOOGLE_APPLICATION_CREDENTIALS is used by Application Default
|
||||
// Credentials in all GCP client libraries.
|
||||
exportVariableAndWarn('GOOGLE_APPLICATION_CREDENTIALS', credentialsPath);
|
||||
|
||||
// GOOGLE_GHA_CREDS_PATH is used by other Google GitHub Actions.
|
||||
exportVariableAndWarn('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 (exportEnvironmentVariables) {
|
||||
exportVariableAndWarn('CLOUDSDK_CORE_PROJECT', computedProjectID);
|
||||
exportVariableAndWarn('CLOUDSDK_PROJECT', computedProjectID);
|
||||
exportVariableAndWarn('GCLOUD_PROJECT', computedProjectID);
|
||||
exportVariableAndWarn('GCP_PROJECT', computedProjectID);
|
||||
exportVariableAndWarn('GOOGLE_CLOUD_PROJECT', computedProjectID);
|
||||
}
|
||||
|
||||
switch (tokenFormat) {
|
||||
case '': {
|
||||
break;
|
||||
}
|
||||
case null: {
|
||||
break;
|
||||
}
|
||||
case 'access_token': {
|
||||
logDebug(`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();
|
||||
|
||||
// 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;
|
||||
if (accessTokenSubject) {
|
||||
if (accessTokenLifetime > 3600) {
|
||||
logInfo(
|
||||
`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.`,
|
||||
);
|
||||
}
|
||||
|
||||
const unsignedJWT = buildDomainWideDelegationJWT(
|
||||
serviceAccount,
|
||||
accessTokenSubject,
|
||||
accessTokenScopes,
|
||||
accessTokenLifetime,
|
||||
);
|
||||
const signedJWT = await client.signJWT(unsignedJWT, delegates);
|
||||
({ accessToken, expiration } = await client.googleOAuthToken(signedJWT));
|
||||
} else {
|
||||
const authToken = await client.getAuthToken();
|
||||
({ accessToken, expiration } = await client.googleAccessToken(authToken, {
|
||||
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`);
|
||||
|
||||
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, {
|
||||
serviceAccount,
|
||||
audience: idTokenAudience,
|
||||
delegates,
|
||||
includeEmail: idTokenIncludeEmail,
|
||||
});
|
||||
setSecret(token);
|
||||
setOutput('id_token', token);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown token format "${tokenFormat}"`);
|
||||
}
|
||||
}
|
||||
if (require.main === module) {
|
||||
run(new Logger());
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
const old = JSON.stringify(existing);
|
||||
logWarning(`Overwriting existing environment variable ${key} (was: ${old})`);
|
||||
}
|
||||
|
||||
exportVariable(key, value);
|
||||
}
|
||||
|
||||
run();
|
||||
|
||||
44
src/post.ts
44
src/post.ts
@@ -1,42 +1,56 @@
|
||||
'use strict';
|
||||
// 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 { getBooleanInput, setFailed, info as logInfo } from '@actions/core';
|
||||
import { errorMessage, forceRemove } from '@google-github-actions/actions-utils';
|
||||
import { getInput, setFailed } from '@actions/core';
|
||||
|
||||
/**
|
||||
* Executes the post action, documented inline.
|
||||
*/
|
||||
export async function run(): Promise<void> {
|
||||
import { errorMessage, forceRemove, parseBoolean } from '@google-github-actions/actions-utils';
|
||||
|
||||
import { Logger } from './logger';
|
||||
|
||||
export async function run(logger: Logger) {
|
||||
try {
|
||||
const createCredentials = getBooleanInput('create_credentials_file');
|
||||
const createCredentials = parseBoolean(getInput('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');
|
||||
const cleanupCredentials = parseBoolean(getInput('cleanup_credentials'));
|
||||
if (!cleanupCredentials) {
|
||||
logInfo(`Skipping credential cleanup - "cleanup_credentials" is false.`);
|
||||
logger.info(`Skipping credential cleanup - "cleanup_credentials" is false.`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Look up the credentials path, if one exists. Note that we only check the
|
||||
// environment variable set by our action, since we don't want to
|
||||
// accidentially clean up if someone set GOOGLE_APPLICATION_CREDENTIALS or
|
||||
// accidentally clean up if someone set GOOGLE_APPLICATION_CREDENTIALS or
|
||||
// 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}".`);
|
||||
logger.info(`Removed exported credentials at "${credentialsPath}".`);
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err);
|
||||
setFailed(`google-github-actions/auth post failed with: ${msg}`);
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
if (require.main === module) {
|
||||
run(new Logger());
|
||||
}
|
||||
|
||||
99
src/utils.ts
99
src/utils.ts
@@ -1,6 +1,28 @@
|
||||
'use strict';
|
||||
// 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 { 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.
|
||||
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
|
||||
@@ -38,6 +60,79 @@ 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];
|
||||
}
|
||||
|
||||
/**
|
||||
* generateCredentialsFilename creates a predictable filename under which
|
||||
* credentials are written. This string is the filename, not the filepath. It must match the format:
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
'use strict';
|
||||
// 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 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { join as pathjoin } from 'path';
|
||||
import { readFileSync } from 'fs';
|
||||
@@ -9,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/service_account_key_json';
|
||||
|
||||
// Yes, this is a real private key. No, it's not valid for authenticating
|
||||
// Google Cloud.
|
||||
@@ -28,97 +41,69 @@ const credentialsJSON = `
|
||||
}
|
||||
`;
|
||||
|
||||
describe('CredentialsJSONClient', () => {
|
||||
describe('#parseServiceAccountKeyJSON', () => {
|
||||
it('throws exception on invalid json', async () => {
|
||||
const fn = (): CredentialsJSONClient => {
|
||||
return new CredentialsJSONClient({
|
||||
credentialsJSON: 'invalid json',
|
||||
});
|
||||
};
|
||||
|
||||
expect(fn).to.throw(SyntaxError);
|
||||
});
|
||||
|
||||
it('handles base64', async () => {
|
||||
const fn = (): CredentialsJSONClient => {
|
||||
return new CredentialsJSONClient({
|
||||
credentialsJSON: Buffer.from('{}').toString('base64'),
|
||||
});
|
||||
};
|
||||
|
||||
expect(fn).to.not.throw(SyntaxError);
|
||||
});
|
||||
test('#parseServiceAccountKeyJSON', { concurrency: true }, async (suite) => {
|
||||
await suite.test('throws exception on invalid json', async () => {
|
||||
await assert.rejects(async () => {
|
||||
new ServiceAccountKeyClient({
|
||||
logger: new NullLogger(),
|
||||
universe: 'googleapis.com',
|
||||
serviceAccountKey: 'invalid json',
|
||||
});
|
||||
}, SyntaxError);
|
||||
});
|
||||
|
||||
describe('#getAuthToken', () => {
|
||||
it('signs a jwt', async () => {
|
||||
const client = new CredentialsJSONClient({
|
||||
credentialsJSON: credentialsJSON,
|
||||
await suite.test('handles base64', async () => {
|
||||
await assert.rejects(async () => {
|
||||
new ServiceAccountKeyClient({
|
||||
logger: new NullLogger(),
|
||||
universe: 'googleapis.com',
|
||||
serviceAccountKey: 'base64',
|
||||
});
|
||||
|
||||
const token = await client.getAuthToken();
|
||||
expect(token).to.be;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#signJWT', () => {
|
||||
it('signs a jwt', async () => {
|
||||
const client = new CredentialsJSONClient({
|
||||
credentialsJSON: credentialsJSON,
|
||||
});
|
||||
|
||||
const token = await client.signJWT('thisismy.jwt');
|
||||
expect(token).to.be;
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getProjectID', () => {
|
||||
it('extracts project ID from the json', async () => {
|
||||
const client = new CredentialsJSONClient({
|
||||
credentialsJSON: credentialsJSON,
|
||||
});
|
||||
|
||||
const result = await client.getProjectID();
|
||||
expect(result).to.eq('my-project');
|
||||
});
|
||||
|
||||
it('prefers the override if given', async () => {
|
||||
const client = new CredentialsJSONClient({
|
||||
projectID: 'my-other-project',
|
||||
credentialsJSON: credentialsJSON,
|
||||
});
|
||||
|
||||
const result = await client.getProjectID();
|
||||
expect(result).to.eq('my-other-project');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#getServiceAccount', () => {
|
||||
it('extracts service account from the json', async () => {
|
||||
const client = new CredentialsJSONClient({
|
||||
credentialsJSON: credentialsJSON,
|
||||
});
|
||||
|
||||
const result = await client.getServiceAccount();
|
||||
expect(result).to.eq('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 exp = JSON.parse(credentialsJSON);
|
||||
|
||||
const pth = await client.createCredentialsFile(outputFile);
|
||||
const data = readFileSync(pth);
|
||||
const got = JSON.parse(data.toString('utf8'));
|
||||
|
||||
expect(got).to.deep.equal(exp);
|
||||
});
|
||||
}, SyntaxError);
|
||||
});
|
||||
});
|
||||
|
||||
test('#getToken', { concurrency: true }, async (suite) => {
|
||||
await suite.test('gets a token', async () => {
|
||||
const client = new ServiceAccountKeyClient({
|
||||
logger: new NullLogger(),
|
||||
universe: 'googleapis.com',
|
||||
serviceAccountKey: credentialsJSON,
|
||||
});
|
||||
|
||||
const token = await client.getToken();
|
||||
assert.ok(token);
|
||||
});
|
||||
});
|
||||
|
||||
test('#signJWT', { concurrency: true }, async (suite) => {
|
||||
await suite.test('signs a jwt', async () => {
|
||||
const client = new ServiceAccountKeyClient({
|
||||
logger: new NullLogger(),
|
||||
universe: 'googleapis.com',
|
||||
serviceAccountKey: credentialsJSON,
|
||||
});
|
||||
|
||||
const token = await client.signJWT('thisismy.jwt');
|
||||
assert.ok(token);
|
||||
});
|
||||
});
|
||||
|
||||
test('#createCredentialsFile', { concurrency: true }, async (suite) => {
|
||||
await suite.test('writes the file', async () => {
|
||||
const outputFile = pathjoin(tmpdir(), randomFilename());
|
||||
const client = new ServiceAccountKeyClient({
|
||||
logger: new NullLogger(),
|
||||
universe: 'googleapis.com',
|
||||
serviceAccountKey: credentialsJSON,
|
||||
});
|
||||
|
||||
const exp = JSON.parse(credentialsJSON);
|
||||
|
||||
const pth = await client.createCredentialsFile(outputFile);
|
||||
const data = readFileSync(pth);
|
||||
const got = JSON.parse(data.toString('utf8'));
|
||||
|
||||
assert.deepStrictEqual(got, exp);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,19 @@
|
||||
'use strict';
|
||||
// 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 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { tmpdir } from 'os';
|
||||
import { join as pathjoin } from 'path';
|
||||
@@ -9,107 +21,84 @@ import { readFileSync } from 'fs';
|
||||
|
||||
import { randomFilename } from '@google-github-actions/actions-utils';
|
||||
|
||||
import { WorkloadIdentityClient } from '../../src/client/workload_identity_client';
|
||||
import { NullLogger } from '../../src/logger';
|
||||
import { WorkloadIdentityFederationClient } from '../../src/client/workload_identity_federation';
|
||||
|
||||
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',
|
||||
});
|
||||
test('#createCredentialsFile', { concurrency: true }, async (suite) => {
|
||||
await suite.test('writes the file', async () => {
|
||||
const outputFile = pathjoin(tmpdir(), randomFilename());
|
||||
const client = new WorkloadIdentityFederationClient({
|
||||
logger: new NullLogger(),
|
||||
universe: 'googleapis.com',
|
||||
|
||||
const result = await client.getProjectID();
|
||||
expect(result).to.eq('my-project');
|
||||
githubOIDCToken: 'my-token',
|
||||
githubOIDCTokenRequestURL: 'https://example.com/',
|
||||
githubOIDCTokenRequestToken: 'token',
|
||||
githubOIDCTokenAudience: 'my-aud',
|
||||
workloadIdentityProviderName: 'my-provider',
|
||||
});
|
||||
|
||||
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();
|
||||
expect(result).to.eq('my-other-project');
|
||||
});
|
||||
|
||||
it('throws an error when extraction fails', async () => {
|
||||
const fn = () => {
|
||||
return new WorkloadIdentityClient({
|
||||
providerID: 'my-provider',
|
||||
token: 'my-token',
|
||||
serviceAccount: 'my-service@developers.google.com',
|
||||
audience: 'my-aud',
|
||||
oidcTokenRequestURL: 'https://example.com/',
|
||||
oidcTokenRequestToken: 'token',
|
||||
});
|
||||
};
|
||||
return expect(fn).to.throw(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();
|
||||
expect(result).to.eq('my-service@my-project.iam.gserviceaccount.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('#createCredentialsFile', () => {
|
||||
it('writes the file', async () => {
|
||||
const outputFile = pathjoin(tmpdir(), randomFilename());
|
||||
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 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',
|
||||
const exp = {
|
||||
audience: '//iam.googleapis.com/my-provider',
|
||||
credential_source: {
|
||||
format: {
|
||||
subject_token_field_name: 'value',
|
||||
type: 'json',
|
||||
},
|
||||
service_account_impersonation_url:
|
||||
'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-service@my-project.iam.gserviceaccount.com:generateAccessToken',
|
||||
subject_token_type: 'urn:ietf:params:oauth:token-type:jwt',
|
||||
token_url: 'https://sts.googleapis.com/v1/token',
|
||||
type: 'external_account',
|
||||
};
|
||||
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'));
|
||||
const pth = await client.createCredentialsFile(outputFile);
|
||||
const data = readFileSync(pth);
|
||||
const got = JSON.parse(data.toString('utf8'));
|
||||
|
||||
expect(got).to.deep.equal(exp);
|
||||
assert.deepStrictEqual(got, exp);
|
||||
});
|
||||
|
||||
await suite.test('writes the file with impersonation', async () => {
|
||||
const outputFile = pathjoin(tmpdir(), randomFilename());
|
||||
const client = new WorkloadIdentityFederationClient({
|
||||
logger: new NullLogger(),
|
||||
universe: 'googleapis.com',
|
||||
|
||||
githubOIDCToken: 'my-token',
|
||||
githubOIDCTokenRequestURL: 'https://example.com/',
|
||||
githubOIDCTokenRequestToken: 'token',
|
||||
githubOIDCTokenAudience: 'my-aud',
|
||||
workloadIdentityProviderName: 'my-provider',
|
||||
serviceAccount: 'my-service@my-project.iam.gserviceaccount.com',
|
||||
});
|
||||
|
||||
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',
|
||||
},
|
||||
service_account_impersonation_url:
|
||||
'https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/my-service@my-project.iam.gserviceaccount.com:generateAccessToken',
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,66 +1,171 @@
|
||||
'use strict';
|
||||
// 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 'mocha';
|
||||
import { expect } from 'chai';
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { buildDomainWideDelegationJWT, generateCredentialsFilename } from '../src/utils';
|
||||
import {
|
||||
buildDomainWideDelegationJWT,
|
||||
computeProjectID,
|
||||
computeServiceAccountEmail,
|
||||
generateCredentialsFilename,
|
||||
projectIDFromServiceAccountEmail,
|
||||
} from '../src/utils';
|
||||
|
||||
describe('Utils', () => {
|
||||
describe('#buildDomainWideDelegationJWT', () => {
|
||||
const cases = [
|
||||
{
|
||||
name: 'default',
|
||||
serviceAccount: 'my-service@example.com',
|
||||
lifetime: 1000,
|
||||
},
|
||||
{
|
||||
name: 'with subject',
|
||||
serviceAccount: 'my-service@example.com',
|
||||
subject: 'my-subject',
|
||||
lifetime: 1000,
|
||||
},
|
||||
{
|
||||
name: 'with scopes',
|
||||
serviceAccount: 'my-service@example.com',
|
||||
scopes: ['scope1', 'scope2'],
|
||||
lifetime: 1000,
|
||||
},
|
||||
];
|
||||
test('#buildDomainWideDelegationJWT', { concurrency: true }, async (suite) => {
|
||||
const cases = [
|
||||
{
|
||||
name: 'default',
|
||||
serviceAccount: 'my-service@example.com',
|
||||
lifetime: 1000,
|
||||
},
|
||||
{
|
||||
name: 'with subject',
|
||||
serviceAccount: 'my-service@example.com',
|
||||
subject: 'my-subject',
|
||||
lifetime: 1000,
|
||||
},
|
||||
{
|
||||
name: 'with scopes',
|
||||
serviceAccount: 'my-service@example.com',
|
||||
scopes: ['scope1', 'scope2'],
|
||||
lifetime: 1000,
|
||||
},
|
||||
];
|
||||
|
||||
cases.forEach((tc) => {
|
||||
it(tc.name, async () => {
|
||||
const val = buildDomainWideDelegationJWT(
|
||||
tc.serviceAccount,
|
||||
tc.subject,
|
||||
tc.scopes,
|
||||
tc.lifetime,
|
||||
);
|
||||
for await (const tc of cases) {
|
||||
await suite.test(tc.name, async () => {
|
||||
const val = buildDomainWideDelegationJWT(
|
||||
tc.serviceAccount,
|
||||
tc.subject,
|
||||
tc.scopes,
|
||||
tc.lifetime,
|
||||
);
|
||||
|
||||
const body = JSON.parse(val);
|
||||
expect(body.iss).to.eq(tc.serviceAccount);
|
||||
expect(body.aud).to.eq('https://oauth2.googleapis.com/token');
|
||||
|
||||
if (tc.subject) {
|
||||
expect(body.sub).to.eq(tc.subject);
|
||||
} else {
|
||||
expect(body.sub).to.not.be;
|
||||
}
|
||||
|
||||
if (tc.scopes) {
|
||||
expect(body.scope).to.eq(tc.scopes.join(' '));
|
||||
} else {
|
||||
expect(body.scope).to.not.be;
|
||||
}
|
||||
});
|
||||
const body = JSON.parse(val);
|
||||
assert.deepStrictEqual(body.iss, tc.serviceAccount);
|
||||
assert.deepStrictEqual(body.aud, 'https://oauth2.googleapis.com/token');
|
||||
assert.deepStrictEqual(body.sub, tc.subject);
|
||||
assert.deepStrictEqual(body.scope, tc.scopes?.join(' '));
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('#generateCredentialsFilename', () => {
|
||||
it('returns a string matching the regex', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const filename = generateCredentialsFilename();
|
||||
expect(filename).to.match(/gha-creds-[0-9a-z]{16}\.json/);
|
||||
}
|
||||
test('#computeProjectID', { concurrency: true }, async (suite) => {
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
for await (const tc of cases) {
|
||||
await suite.test(tc.name, async () => {
|
||||
const result = computeProjectID(
|
||||
tc.projectID,
|
||||
tc.serviceAccountEmail,
|
||||
tc.serviceAccountKeyJSON,
|
||||
);
|
||||
assert.deepStrictEqual(result, tc.exp);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('#computeServiceAccountEmail', { concurrency: true }, async (suite) => {
|
||||
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,
|
||||
},
|
||||
];
|
||||
|
||||
for await (const tc of cases) {
|
||||
await suite.test(tc.name, async () => {
|
||||
const result = computeServiceAccountEmail(tc.serviceAccountEmail, tc.serviceAccountKeyJSON);
|
||||
assert.deepStrictEqual(result, tc.exp);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('#projectIDFromServiceAccountEmail', { concurrency: true }, async (suite) => {
|
||||
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',
|
||||
},
|
||||
];
|
||||
|
||||
for await (const tc of cases) {
|
||||
await suite.test(tc.name, async () => {
|
||||
const result = projectIDFromServiceAccountEmail(tc.input);
|
||||
assert.deepStrictEqual(result, tc.exp);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('#generateCredentialsFilename', { concurrency: true }, async (suite) => {
|
||||
await suite.test('returns a string matching the regex', () => {
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const filename = generateCredentialsFilename();
|
||||
assert.match(filename, /gha-creds-[0-9a-z]{16}\.json/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"alwaysStrict": true,
|
||||
"target": "es6",
|
||||
"target": "es2022",
|
||||
"module": "commonjs",
|
||||
"lib": [
|
||||
"es6"
|
||||
],
|
||||
"lib": ["es2022"],
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": ["node_modules", "**/*.test.ts"]
|
||||
"exclude": ["node_modules/", "tests/"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user